diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json deleted file mode 100644 index d141c7702c..0000000000 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ /dev/null @@ -1,1713 +0,0 @@ -{ - "$schema": "../$schema.json", - "general": { - "ascending": "Ascending", - "descending": "Descending", - "add": "Add", - "start": "Start", - "end": "End", - "open": "Open", - "close": "Close", - "apply": "Apply", - "range": "Range", - "search": "Search", - "of": "of", - "results": "results", - "pages": "pages", - "next": "Next", - "prev": "Prev", - "is": "is", - "timeline": "Timeline", - "success": "Success", - "warning": "Warning", - "error": "Error", - "select": "Select", - "selected": "Selected", - "enabled": "Enabled", - "disabled": "Disabled", - "expired": "Expired", - "active": "Active", - "revoked": "Revoked", - "admin": "Admin", - "store": "Store", - "details": "Details", - "items_one": "{{count}} item", - "items_other": "{{count}} items", - "countSelected": "{{count}} selected", - "countOfTotalSelected": "{{count}} of {{total}} selected", - "plusCount": "+ {{count}}", - "plusCountMore": "+ {{count}} more", - "areYouSure": "Are you sure?", - "noRecordsFound": "No records found", - "typeToConfirm": "Please type {val} to confirm:", - "noResultsTitle": "No results", - "noResultsMessage": "Try changing the filters or search query", - "noSearchResults": "No search results", - "noSearchResultsFor": "No search results for <0>'{{query}}'", - "noRecordsTitle": "No records", - "noRecordsMessage": "There are no records to show", - "unsavedChangesTitle": "Are you sure you want to leave this page?", - "unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.", - "includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved." - }, - "validation": { - "mustBeInt": "The value must be a whole number.", - "mustBePositive": "The value must be a positive number." - }, - "nav": { - "general": "General", - "developer": "Developer", - "extensions": "Extensions", - "settings": "Settings" - }, - "actions": { - "save": "Save", - "select": "Select", - "saveAsDraft": "Save as draft", - "publish": "Publish", - "create": "Create", - "delete": "Delete", - "remove": "Remove", - "revoke": "Revoke", - "cancel": "Cancel", - "enable": "Enable", - "disable": "Disable", - "complete": "Complete", - "viewDetails": "View details", - "back": "Back", - "close": "Close", - "continue": "Continue", - "confirm": "Confirm", - "edit": "Edit", - "download": "Download", - "clearAll": "Clear all", - "apply": "Apply", - "add": "Add" - }, - "filters": { - "date": { - "today": "Today", - "lastSevenDays": "Last 7 days", - "lastThirtyDays": "Last 30 days", - "lastNinetyDays": "Last 90 days", - "lastTwelveMonths": "Last 12 months", - "custom": "Custom", - "from": "From", - "to": "To" - }, - "compare": { - "lessThan": "Less than", - "greaterThan": "Greater than", - "exact": "Exact", - "range": "Range", - "lessThanLabel": "less than {{value}}", - "greaterThanLabel": "greater than {{value}}", - "andLabel": "and" - }, - "addFilter": "Add filter" - }, - "errorBoundary": { - "badRequestTitle": "Bad request", - "badRequestMessage": "The request was invalid.", - "notFoundTitle": "Not found", - "notFoundMessage": "The page you are looking for does not exist.", - "internalServerErrorTitle": "Internal server error", - "internalServerErrorMessage": "An error occurred on the server.", - "defaultTitle": "An error occurred", - "defaultMessage": "An error occurred while rendering this page." - }, - "addresses": { - "shippingAddress": { - "header": "Shipping Address", - "editHeader": "Edit Shipping Address", - "editLabel": "Shipping address", - "label": "Shipping address" - }, - "billingAddress": { - "header": "Billing Address", - "editHeader": "Edit Billing Address", - "editLabel": "Billing address", - "label": "Billing address", - "sameAsShipping": "Same as shipping address" - }, - "contactHeading": "Contact", - "locationHeading": "Location" - }, - "email": { - "editHeader": "Edit Email", - "editLabel": "Email", - "label": "Email" - }, - "transferOwnership": { - "header": "Transfer Ownership", - "label": "Transfer ownership", - "details": { - "order": "Order details", - "draft": "Draft details" - }, - "currentOwner": { - "label": "Current owner", - "hint": "The current owner of the order." - }, - "newOwner": { - "label": "New owner", - "hint": "The new owner to transfer the order to." - }, - "validation": { - "mustBeDifferent": "The new owner must be different from the current owner.", - "required": "New owner is required." - } - }, - "sales_channels": { - "availableIn": "Available in <0>{{x}} of <1>{{y}} sales channels" - }, - "products": { - "domain": "Products", - "create": { - "header": "Create Product", - "hint": "Create a new product to sell in your store.", - "tabs": { - "details": "Details", - "variants": "Variants" - }, - "variants": { - "header": "Variants", - "productVariants": { - "label": "Product variants", - "hint": "Variants left unchecked won't be created. This ranking will affect how the variants are ranked in your frontend.", - "alert": "Add options to create variants." - }, - "productOptions": { - "label": "Product options", - "hint": "Define the options for the product, e.g. color, size, etc." - } - }, - "successToast": "Product {{title}} was successfully created." - }, - "deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.", - "variants": "Variants", - "attributes": "Attributes", - "editProduct": "Edit Product", - "editAttributes": "Edit Attributes", - "organization": "Organize", - "editOrganization": "Edit Organization", - "editOptions": "Edit Options", - "editPrices": "Edit prices", - "media": { - "label": "Media", - "editHint": "Add media to the product to showcase it in your storefront.", - "uploadImagesLabel": "Upload images", - "uploadImagesHint": "Drag and drop images here or click to upload.", - "invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.", - "deleteWarning_one": "You are about to delete {{count}} image. This action cannot be undone.", - "deleteWarning_other": "You are about to delete {{count}} images. This action cannot be undone.", - "deleteWarningWithThumbnail_one": "You are about to delete {{count}} image including the thumbnail. This action cannot be undone.", - "deleteWarningWithThumbnail_other": "You are about to delete {{count}} images including the thumbnail. This action cannot be undone.", - "thumbnailTooltip": "Thumbnail", - "galleryLabel": "Gallery", - "downloadImageLabel": "Download current image", - "deleteImageLabel": "Delete current image", - "noMediaLabel": "The product has no associated media." - }, - "discountableHint": "When unchecked discounts will not be applied to this product.", - "noSalesChannels": "Not available in any sales channels", - "variantCount_one": "{{count}} variant", - "variantCount_other": "{{count}} variants", - "deleteVariantWarning": "You are about to delete the variant {{title}}. This action cannot be undone.", - "productStatus": { - "draft": "Draft", - "published": "Published", - "proposed": "Proposed", - "rejected": "Rejected" - }, - "fields": { - "title": { - "label": "Title", - "hint": "Give your product a short and clear title.<0/>50-60 characters is the recommended length for search engines." - }, - "subtitle": { - "label": "Subtitle" - }, - "handle": { - "label": "Handle", - "tooltip": "The handle is used to reference the product in your storefront. If not specified, the handle will be generated from the product title." - }, - "description": { - "label": "Description", - "hint": "Give your product a short and clear description.<0/>120-160 characters is the recommended length for search engines." - }, - "discountable": { - "label": "Discountable", - "hint": "When unchecked discounts will not be applied to this product" - }, - "type": { - "label": "Type" - }, - "collection": { - "label": "Collection" - }, - "categories": { - "label": "Categories" - }, - "tags": { - "label": "Tags" - }, - "sales_channels": { - "label": "Sales channels", - "hint": "This product will only be available in the default sales channel if left untouched" - }, - "countryOrigin": { - "label": "Country of origin" - }, - "material": { - "label": "Material" - }, - "width": { - "label": "Width" - }, - "length": { - "label": "Length" - }, - "height": { - "label": "Height" - }, - "weight": { - "label": "Weight" - }, - "options": { - "label": "Product options", - "hint": "Options are used to define the color, size, etc. of the product", - "add": "Add option", - "optionTitle": "Option title", - "variations": "Variations (comma-separated)" - }, - "variants": { - "label": "Product variants", - "hint": "Variants left unchecked won't be created, This ranking will affect how the variants are ranked in your frontend." - }, - "mid_code": { - "label": "Mid code" - }, - "hs_code": { - "label": "HS code" - } - }, - "variant": { - "edit": { - "header": "Edit Variant" - }, - "create": { - "header": "Create Variant" - }, - "inventory": { - "header": "Stock & Inventory", - "editItemDetails": "Edit item details", - "manageInventoryLabel": "Manage inventory", - "manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.", - "allowBackordersLabel": "Allow backorders", - "allowBackordersHint": "When enabled the variant can be sold even if the inventory level is below zero.", - "toast": { - "levelsBatch": "Inventory levels updated.", - "update": "Inventory item updated successfully", - "updateLevel": "Inventory level updated successfully" - } - } - }, - "options": { - "header": "Options", - "edit": { - "header": "Edit Option", - "successToast": "Option {{title}} was successfully updated." - }, - "create": { - "header": "Create Option", - "successToast": "Option {{title}} was successfully created." - } - }, - "toasts": { - "delete": { - "success": { - "header": "Product deleted", - "description": "{{title}} was successfully deleted." - }, - "error": { - "header": "Failed to delete product" - } - } - } - }, - "collections": { - "domain": "Collections", - "createCollection": "Create Collection", - "createCollectionHint": "Create a new collection to organize your products.", - "editCollection": "Edit Collection", - "handleTooltip": "The handle is used to reference the collection in your storefront. If not specified, the handle will be generated from the collection title.", - "deleteWarning": "You are about to delete the collection {{title}}. This action cannot be undone.", - "removeSingleProductWarning": "You are about to remove the product {{title}} from the collection. This action cannot be undone.", - "removeProductsWarning_one": "You are about to remove {{count}} product from the collection. This action cannot be undone.", - "removeProductsWarning_other": "You are about to remove {{count}} products from the collection. This action cannot be undone." - }, - "categories": { - "domain": "Categories", - "organization": { - "header": "Organization", - "pathLabel": "Path", - "pathExpandTooltip": "Show full path", - "childrenLabel": "Children" - }, - "fields": { - "visibility": "Visibility", - "active": "Active", - "inactive": "Inactive", - "internal": "Internal", - "public": "Public" - } - }, - "inventory": { - "domain": "Inventory", - "reserved": "Reserved", - "available": "Available", - "locationLevels": "Location levels", - "associatedVariants": "Associated variants", - "manageLocations": "Manage locations", - "deleteWarning": "You are about to delete an inventory item. This action cannot be undone.", - "reservation": { - "header": "Reservation of {{itemName}}", - "editItemDetails": "Edit item details", - "orderID": "Order ID", - "description": "Description", - "location": "Location", - "inStockAtLocation": "In stock at this location", - "availableAtLocation": "Available at this location", - "reservedAtLocation": "Reserved at this location", - "reservedAmount": "Reserve amount", - "create": "Create reservation", - "itemToReserve": "Item to reserve", - "quantityPlaceholder": "How many do you want to reserve?", - "descriptionPlaceholder": "What type of reservation is this?", - "successToast": "Reservation was successfully created.", - "updateSuccessToast": "Reservation was successfully updated.", - "deleteSuccessToast": "Reservation was successfully deleted." - } - }, - "giftCards": { - "domain": "Gift Cards", - "editGiftCard": "Edit Gift Card", - "createGiftCard": "Create Gift Card", - "createGiftCardHint": "Manually create a gift card that can be used as a payment method in your store.", - "selectRegionFirst": "Select a region first", - "deleteGiftCardWarning": "You are about to delete the gift card {{code}}. This action cannot be undone.", - "balanceHigherThanValue": "The balance cannot be higher than the original amount.", - "balanceLowerThanZero": "The balance cannot be negative.", - "expiryDateHint": "Countries have different laws regarding gift card expiry dates. Make sure to check local regulations before setting an expiry date.", - "regionHint": "Changing the region of the gift card will also change its currency, potentially affecting its monetary value.", - "enabledHint": "Specify if the gift card is enabled or disabled.", - "balance": "Balance", - "currentBalance": "Current balance", - "initialBalance": "Initial balance", - "personalMessage": "Personal message", - "recipient": "Recipient" - }, - "customers": { - "domain": "Customers", - "create": { - "header": "Create Customer", - "hint": "Create a new customer to manage their details.", - "successToast": "Customer {{email}} was successfully created." - }, - "edit": { - "header": "Edit Customer", - "emailDisabledTooltip": "The email address cannot be changed for registered customers.", - "successToast": "Customer {{email}} was successfully updated." - }, - "delete": { - "title": "Delete Customer", - "description": "You are about to delete the customer {{email}}. This action cannot be undone.", - "successToast": "Customer {{email}} was successfully deleted." - }, - "fields": { - "guest": "Guest", - "registered": "Registered", - "groups": "Groups" - } - }, - "customerGroups": { - "domain": "Customer Groups", - "create": { - "header": "Create Customer Group", - "hint": "Create a new customer group to segment your customers.", - "successToast": "Customer group {{name}} was successfully created." - }, - "edit": { - "header": "Edit Customer Group", - "successToast": "Customer group {{name}} was successfully updated." - }, - "delete": { - "title": "Delete Customer Group", - "description": "You are about to delete the customer group {{name}}. This action cannot be undone.", - "successToast": "Customer group {{name}} was successfully deleted." - }, - "customers": { - "alreadyAddedTooltip": "The customer has already been added to the group.", - "add": { - "successToast_one": "Customer was successfully added to the group.", - "successToast_other": "Customers were successfully added to the group." - }, - "remove": { - "title_one": "Remove customer", - "title_other": "Remove customers", - "description_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.", - "description_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone." - } - } - }, - "orders": { - "domain": "Orders", - "cancelWarning": "You are about to cancel the order {{id}}. This action cannot be undone.", - "onDateFromSalesChannel": "{{date}} from {{salesChannel}}", - "summary": { - "requestReturn": "Request return", - "allocateItems": "Allocate items", - "editItems": "Edit items" - }, - "payment": { - "title": "Payments", - "isReadyToBeCaptured": "Payment {{id}} is ready to be captured.", - "totalPaidByCustomer": "Total paid by customer", - "capture": "Capture", - "refund": "Refund", - "statusLabel": "Payment status", - "statusTitle": "Payment Status", - "status": { - "notPaid": "Not paid", - "awaiting": "Awaiting", - "captured": "Captured", - "partiallyRefunded": "Partially refunded", - "refunded": "Refunded", - "canceled": "Canceled", - "requiresAction": "Requires action" - } - }, - "edits": { - "title": "Edit order", - "currentItems": "Current items", - "currentItemsDescription": "Adjust item quantity or remove.", - "addItemsDescription": "You can add new items to the order.", - "addItems": "Add items", - "amountPaid": "Amount paid", - "newTotal": "New total", - "differenceDue": "Difference due" - }, - "returns": { - "details": "Details", - "chooseItems": "Choose items", - "refundAmount": "Refund amount", - "locationDescription": "Choose which location you want to return the items to.", - "shippingDescription": "Choose which method you want to use for this return.", - "noInventoryLevel": "No inventory level", - "sendNotification": "Send notification", - "sendNotificationHint": "Notify customer of created return.", - "customRefund": "Custom refund", - "shippingPriceTooltip1": "Custom refund is enabled", - "noShippingOptions": "There are no shipping options for the region", - "shippingPriceTooltip2": "Shipping needs to be selected", - "customRefundHint": "If you want to refund something else instead of the total refund.", - "customShippingPrice": "Custom shipping", - "customShippingPriceHint": "Custom shipping cost.", - "noInventoryLevelDesc": "The selected location does not have an inventory level for the selected items. The return can be requested but can’t be received until an inventory level is created for the selected location.", - "refundableAmountLabel": "Refundable amount", - "refundableAmountHeader": "Refundable Amount", - "returnableQuantityLabel": "Returnable quantity", - "returnableQuantityHeader": "Returnable Quantity" - }, - "reservations": { - "allocatedLabel": "Allocated", - "notAllocatedLabel": "Not allocated" - }, - "fulfillment": { - "cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.", - "unfulfilledItems": "Unfulfilled Items", - "statusLabel": "Fulfillment status", - "statusTitle": "Fulfillment Status", - "fulfillItems": "Fulfill items", - "awaitingFulfillmentBadge": "Awaiting fulfillment", - "number": "Fulfillment #{{number}}", - "itemsToFulfill": "Items to fulfill", - "create": "Create Fulfillment", - "available": "Available", - "inStock": "In stock", - "itemsToFulfillDesc": "Choose items and quantities to fulfill", - "locationDescription": "Choose which location you want to fulfill items from.", - "error": { - "wrongQuantity": "Only one item is available for fulfillment", - "wrongQuantity_other": "Quantity should be a number between 1 and {{number}}", - "noItems": "No items to fulfill." - }, - "status": { - "notFulfilled": "Not fulfilled", - "partiallyFulfilled": "Partially fulfilled", - "fulfilled": "Fulfilled", - "partiallyShipped": "Partially shipped", - "shipped": "Shipped", - "partiallyReturned": "Partially returned", - "returned": "Returned", - "canceled": "Canceled", - "requiresAction": "Requires action" - }, - "toast": { - "created": "Fulfillment created successfully", - "canceled": "Fulfillment successfully canceled", - "fulfillmentShipped": "Cannot cancel an already shipped fulfillment" - }, - "trackingLabel": "Tracking", - "shippingFromLabel": "Shipping from", - "itemsLabel": "Items" - }, - "refund": { - "title": "Create Refund", - "sendNotificationHint": "Notify customers about the created refund.", - "systemPayment": "System payment", - "systemPaymentDesc": "One or more of your payments is a system payment. Be aware, that captures and refunds are not handled by Medusa for such payments.", - "error": { - "amountToLarge": "Cannot refund more than the original order amount.", - "amountNegative": "Refund amount must be a positive number.", - "reasonRequired": "Please select a refund reason." - } - }, - "customer": { - "contactLabel": "Contact", - "editEmail": "Edit email", - "transferOwnership": "Transfer ownership", - "editBillingAddress": "Edit billing address", - "editShippingAddress": "Edit shipping address" - }, - "activity": { - "header": "Activity", - "showMoreActivities_one": "Show {{count}} more activity", - "showMoreActivities_other": "Show {{count}} more activities", - "comment": { - "label": "Comment", - "placeholder": "Leave a comment", - "addButtonText": "Add comment", - "deleteButtonText": "Delete comment" - }, - "events": { - "placed": { - "title": "Order placed", - "fromSalesChannel": "from {{salesChannel}}" - }, - "canceled": { - "title": "Order canceled" - }, - "payment": { - "awaiting": "Awaiting payment", - "captured": "Payment captured", - "canceled": "Payment canceled" - }, - "fulfillment": { - "created": "Fulfillment created", - "canceled": "Fulfillment canceled", - "shipped": "Fulfillment shipped", - "itemsFulfilledFrom_one": "{{count}} item fulfilled from {{location}}", - "itemsFulfilledFrom_other": "{{count}} items fulfilled from {{location}}", - "itemsFulfilled_one": "{{count}} item fulfilled", - "itemsFulfilled_other": "{{count}} items fulfilled" - }, - "return": { - "created": "Return created" - }, - "note": { - "comment": "Comment", - "byLine": "by {{author}}" - } - } - } - }, - "draftOrders": { - "domain": "Draft Orders", - "deleteWarning": "You are about to delete the draft order {{id}}. This action cannot be undone.", - "paymentLinkLabel": "Payment link", - "cartIdLabel": "Cart ID", - "markAsPaid": { - "label": "Mark as paid", - "warningTitle": "Mark as Paid", - "warningDescription": "You are about to mark the draft order as paid. This action cannot be undone, and collecting payment will not be possible later." - }, - "status": { - "open": "Open", - "completed": "Completed" - }, - "create": { - "createDraftOrder": "Create Draft Order", - "createDraftOrderHint": "Create a new draft order to manage the details of an order before it is placed.", - "chooseRegionHint": "Choose region", - "existingItemsLabel": "Existing items", - "existingItemsHint": "Add existing products to the draft order.", - "customItemsLabel": "Custom items", - "customItemsHint": "Add custom items to the draft order.", - "addExistingItemsAction": "Add existing items", - "addCustomItemAction": "Add custom item", - "noCustomItemsAddedLabel": "No custom items added yet", - "noExistingItemsAddedLabel": "No existing items added yet", - "chooseRegionTooltip": "Choose a region first", - "useExistingCustomerLabel": "Use existing customer", - "addShippingMethodsAction": "Add shipping methods", - "unitPriceOverrideLabel": "Unit price override", - "shippingOptionLabel": "Shipping option", - "shippingOptionHint": "Choose the shipping option for the draft order.", - "shippingPriceOverrideLabel": "Shipping price override", - "shippingPriceOverrideHint": "Override the shipping price for the draft order.", - "sendNotificationLabel": "Send notification", - "sendNotificationHint": "Send a notification to the customer when the draft order is created." - }, - "validation": { - "requiredEmailOrCustomer": "Email or customer is required.", - "requiredItems": "At least one item is required.", - "invalidEmail": "Email must be a valid email address." - } - }, - "shipping": { - "title": "Locations & Shipping", - "domain": "Locations & Shipping", - "description": "Choose where you ship and how much you charge for shipping at checkout. Define shipping options specific for your locations.", - "createLocation": "Create location", - "createLocationDetailsHint": "Specify the details of the location.", - "deleteLocation": "Delete location", - "from": "Shipping from", - "add": "Add shipping", - "connectProvider": "Connect provider", - "addZone": "Add shipping zone", - "enablePickup": "Enable pickup", - "enableDelivery": "Enable delivery", - "deleteLocation": { - "label": "Delete Location", - "confirm": "Are you sure you want to delete {{name}} location", - "success": "{{name}} location successfully deleted" - }, - "noRecords": { - "action": "Add Location", - "title": "No inventory locations", - "message": "Please create an invnetory location first." - }, - "create": { - "title": "Add shipping for {{location}}", - "delivery": "Delivery", - "pickup": "Pickup", - "type": "Shipping type" - }, - "fulfillmentSet": { - "placeholder": "Not covered by any shipping zones.", - "salesChannels": "Connected Sales Channels", - "delete": "Delete shipping", - "disableWarning": "Are you sure that you wnat to disable \"{{name}}\"? This will delete all assocciated service zones and shipping options.", - "create": { - "title": "Add service zone for {{fulfillmentSet}}" - }, - "toast": { - "disable": "\"{{name}}\" disabled" - }, - "addZone": "Add service zone", - "pickup": { - "title": "Pick up", - "enable": "Enable pickup", - "offers": "Offers pick up in" - }, - "delivery": { - "title": "Shipping", - "enable": "Enable delivery", - "offers": "Offers shippping to" - } - }, - "serviceZone": { - "create": { - "title": "Add service zone for {{fulfillmentSet}}", - "subtitle": "Service zone", - "description": "A service zone is a geographical region that can be shipped to from a specific location. You can later on add any number of shipping options to this zone. ", - "zoneName": "Zone name" - }, - "edit": { - "title": "Edit Service Zone" - }, - "editAreasTitle": "Manage {{zone}} areas", - "deleteWarning": "Are you sure you want to delete \"{{name}}\". This will also delete all assocciated shipping options.", - "toast": { - "delete": "Zone \"{{name}}\" deleted successfully." - }, - "manageAreas": "Manage areas", - "editPrices": "Edit prices", - "editOption": "Edit option", - "optionsLength_one": "shipping option", - "optionsLength_other": "shipping options", - "returnOptionsLength_one": "return option", - "returnOptionsLength_other": "return options", - "shippingOptionsPlaceholder": "Not covered by any shipping options.", - "addOption": "Add option", - "shippingOptions": "Shipping options", - "returnOptions": "Return options", - "areas": { - "title": "Areas affected by this rule", - "description": "Select the geographical areas where this shipping zone should apply.", - "manage": "Manage areas", - "error": "Please select at least one country for this service zone." - } - }, - "shippingOptions": { - "create": { - "title": "Create a shipping option for {{zone}}", - "subtitle": "General information", - "description": "To start selling, all you need is a name and a price", - "details": "Details", - "pricing": "Pricing", - "allocation": "Shipping amount", - "fixed": "Fixed", - "fixedDescription": "Shipping option's price is always the same amount.", - "enable": "Show publicly", - "enableDescription": "When disabled, the shipping option can only be applied by admins.", - "calculated": "Calculated", - "calculatedDescription": "Shipping option's price is calculated by the fulfillment provider.", - "profile": "Shipping profile" - }, - "deleteWarning": "Are you sure you want to delete \"{{name}}\"?", - "toast": { - "delete": "Shipping option \"{{name}}\" deleted successfully." - }, - "inStore": "Store", - "edit": { - "title": "Edit Shipping Option", - "provider": "Fulfillment provider" - } - }, - "returnOptions": { - "create": { - "title": "Create a return option for {{zone}}" - } - }, - "salesChannels": { - "title": "Connected Sales Channels", - "placeholder": "No connected channels yet.", - "connectChannels": "Connect Channels" - } - }, - "shippingProfile": { - "domain": "Shipping Profiles", - "create": { - "header": "Create Shipping Profile", - "hint": "Create a new shipping profile to group products with similar shipping requirements.", - "successToast": "Shipping profile {{name}} was successfully created." - }, - "delete": { - "title": "Delete Shipping Profile", - "description": "You are about to delete the shipping profile {{name}}. This action cannot be undone.", - "successToast": "Shipping profile {{name}} was successfully deleted." - }, - "tooltip": { - "type": "Enter shipping profile type, for example: Express, Freight, etc." - } - }, - "discounts": { - "domain": "Discounts", - "startDate": "Start date", - "createDiscountTitle": "Create Discount", - "validDuration": "Duration of the discount", - "redemptionsLimit": "Redemptions limit", - "endDate": "End date", - "type": "Discount type", - "percentageDiscount": "Percentage discount", - "freeShipping": "Free shipping", - "fixedDiscount": "Fixed discount", - "fixedAmount": "Fixed amount", - "validRegions": "Valid regions", - "deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.", - "editDiscountDetails": "Edit Discount Details", - "editDiscountConfiguration": "Edit Discount Configuration", - "hasStartDate": "Discount has a start date", - "hasEndDate": "Discount has an expiry date", - "startDateHint": "Schedule the discount to activate in the future.", - "endDateHint": "Schedule the discount to deactivate in the future.", - "codeHint": "Discount code applies from when you hit the publish button and forever if left untouched.", - "hasUsageLimit": "Limit the number of redemptions?", - "usageLimitHint": "Limit applies across all customers, not per customer.", - "titleHint": "The code your customers will enter during checkout. This will appear on your customer's invoice.\nUppercase letters and numbers only.", - "hasDurationLimit": "Availability duration", - "durationHint": "Set the duration of the discount", - "chooseValidRegions": "Choose valid regions", - "conditionsHint": "Create conditions to apply on the discount", - "isTemplateDiscount": "Is this a template discount?", - "percentageDescription": "Discount applied in %", - "fixedDescription": "Amount discount", - "shippingDescription": "Override delivery amount", - "selectRegionFirst": "Select a region first", - "templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.", - "conditions": { - "editHeader": "Edit Discount Conditions", - "editHint": "Specify conditions for when the discount can be applied to a cart.", - "manageTypesAction": "Manage condition types", - "including": { - "products_one": "Discount applies to <0/> product", - "products_other": "Discount applies to <0/> products", - "customer_groups_one": "Discount applies to <0/> customer group", - "customer_groups_other": "Discount applies to <0/> customer groups", - "product_tags_one": "Discount applies to <0/> tag", - "product_tags_other": "Discount applies to <0/> tags", - "product_collections_one": "Discount applies to <0/> product collection", - "product_collections_other": "Discount applies to <0/> product collections", - "product_types_one": "Discount applies to <0/> product type", - "product_types_other": "Discount applies to <0/> product types" - }, - "excluding": { - "products": "Discount applies to <1>all products except <0/>", - "customer_groups": "Discount applies to <1>all customer groups except <0/>", - "product_tags": "Discount applies to <1>all product tags except <0/>", - "product_collections": "Discount applies to <1>all product collections except <0/>", - "product_types": "Discount applies to <1>all product types except <0/>" - }, - "edit": { - "appliesTo": "Discount applies to", - "except": { - "products_one": "product, except", - "products_other": "products, except", - "product_tags_one": "product tag, except", - "product_tags_other": "product tags, except", - "product_types_one": "product type, except", - "product_types_other": "product types, except", - "product_collections_one": "product collection, except", - "product_collections_other": "product collections, except", - "customer_groups_one": "customer group, except", - "customer_groups_other": "customer groups, except" - } - } - }, - "discountStatus": { - "scheduled": "Scheduled", - "expired": "Expired", - "active": "Active", - "disabled": "Disabled" - } - }, - "taxRates": { - "domain": "Tax Rates", - "fields": { - "isCombinable": "Is combinable?", - "appliesTo": "Tax Rate applies to", - "customer_groups": "Customer Group", - "product_collections": "Product Collection", - "product_tags": "Product Tag", - "product_types": "Product Type", - "products": "Product" - }, - "edit": { - "title": "Edit Tax Rate", - "description": "Edits tax rate for a tax region" - }, - "create": { - "title": "Create Tax Rate Override", - "description": "Creates tax rate overrides for a tax region" - } - }, - "taxRegions": { - "domain": "Tax Regions", - "description": "Manage your region's tax structure", - "create": { - "title": "Create Tax Region", - "description": "Creates a tax region with default tax rate" - }, - "create-child": { - "title": "Create Default Rate for Province", - "description": "Creates a tax region for a province with default tax rate" - }, - "removeWarning": "You are about to remove {{tax_region_name}}. This action cannot be undone.", - "fields": { - "rate": { - "name": "Rate", - "hint": "Tax rate to apply for a region or province" - }, - "is_combinable": { - "name": "Is combinable", - "hint": "If this tax rate can be combined with the default rate from province or parent" - } - } - }, - "promotions": { - "domain": "Promotions", - "sections": { - "details": "Promotion Details" - }, - "fields": { - "method": "Method", - "type": "Type", - "value_type": "Value Type", - "value": "Value", - "campaign": "Campaign", - "allocation": "Allocation", - "addCondition": "Add condition", - "clearAll": "Clear all", - "amount": { - "tooltip": "Select the currency code to enable setting the amount" - }, - "conditions": { - "rules": { - "title": "Who can use this code?", - "description": "Is the customer allowed to add the promotion code? Discount code can be used by all customers if left untouched. Choose between attributes, operators, and values to set up the conditions." - }, - "target-rules": { - "title": "What needs to be in the cart to unlock the promotion?", - "description": "If these conditions match, we enable a promotion action on the target items. Choose between attributes, operators, and values to set up the conditions." - }, - "buy-rules": { - "title": "What will the promotion be applied to?", - "description": "The promotion will be applied to items that match these conditions" - } - } - }, - "errors": { - "requiredField": "Required field" - }, - "create": {}, - "edit": { - "title": "Edit Promotion Details", - "rules": { - "title": "Edit rules" - }, - "target-rules": { - "title": "Edit target rules" - }, - "buy-rules": { - "title": "Edit buy rules" - } - }, - "addToCampaign": { - "title": "Add Promotion To Campaign" - }, - "campaign_currency": { - "tooltip": "Currency is carried over from the promotion. Change it on the promotions tab." - }, - "form": { - "required": "Required", - "and": "AND", - "selectAttribute": "Select Attribute", - "campaign": { - "existing": { - "title": "Existing Campaign", - "description": "Would you like to add promotion to an existing campaign?" - }, - "new": { - "title": "New Campaign", - "description": "Would you like to create a new campaign with this promotion?" - }, - "none": { - "title": "Without Campaign", - "description": "Proceed without associating promotion with campaign" - } - }, - "status": { - "title": "Status" - }, - "method": { - "code": { - "title": "Promotion code", - "description": "Customers must enter this at checkout" - }, - "automatic": { - "title": "Automatic", - "description": "Customers will see this at checkout" - } - }, - "max_quantity": { - "title": "Maximum Quantity", - "description": "Maximum quantity of items this promotion applies to" - }, - "type": { - "standard": { - "title": "Standard", - "description": "A standard promotion" - }, - "buyget": { - "title": "Buy Get", - "description": "Buy X get Y promotion" - } - }, - "allocation": { - "each": { - "title": "Each", - "description": "Applies value on each item" - }, - "across": { - "title": "Across", - "description": "Applies value across items" - } - }, - "code": { - "title": "Code", - "description": "The code your customers will enter during checkout." - }, - "value": { - "title": "Value" - }, - "value_type": { - "fixed": { - "title": "Fixed amount", - "description": "eg. 100" - }, - "percentage": { - "title": "Percentage", - "description": "eg. 8%" - } - } - }, - "deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.", - "createPromotionTitle": "Create Promotion", - "type": "Promotion type" - }, - "campaigns": { - "domain": "Campaigns", - "details": "Campaign details", - "status": { - "active": "active", - "expired": "expired", - "scheduled": "scheduled" - }, - "delete": { - "title": "Are you sure?", - "description": "You are about to delete the campaign '{{name}}'. This action cannot be undone.", - "successToast": "Campaign '{{name}}' was successfully created." - }, - "edit": { - "header": "Edit Campaign", - "successToast": "Campaign '{{name}}' was successfully updated." - }, - "create": { - "hint": "Create a promotional campaign", - "header": "Create Campaign", - "successToast": "Campaign '{{name}}' was successfully created." - }, - "fields": { - "name": "Name", - "identifier": "Identifier", - "start_date": "Start date", - "end_date": "End date", - "total_spend": "Budget spent", - "total_used": "Budget used", - "budget_limit": "Budget limit", - "campaign_id": { - "hint": "A list of campaigns with the same currency code as the promotion" - } - }, - "budget": { - "create": { - "hint": "Create a budget for the campaign", - "header": "Campaign Budget" - }, - "details": "Campaign budget", - "fields": { - "type": "Type", - "currency": "Currency", - "limit": "Limit", - "used": "Used" - }, - "type": { - "spend": { - "title": "Spend", - "description": "Limit usage based on a currency value" - }, - "usage": { - "title": "Usage", - "description": "Limit usage based on how many times its used" - } - } - }, - "promotions": { - "remove": { - "title": "Remove promotion from campaign", - "description": "You are about to remove {{count}} promotion(s) from the campaign. This action cannot be undone." - }, - "alreadyAdded": "This promotion has already been added to the campaign.", - "alreadyAddedDiffCampaign": "This promotion has already been added to a different campaign ({{name}}).", - "currencyMismatch": "Currency of the promotion and campaign doesn't match", - "toast": { - "success": "Successfully added {{count}} promotion(s) to campaign" - } - }, - "deleteCampaignWarning": "You are about to delete the campaign {{name}}. This action cannot be undone.", - "totalSpend": "<0>{{amount}} <1>{{currency}}" - }, - "pricing": { - "domain": "Pricing", - "create": { - "header": "Create Price List", - "hint": "Create a new price list to manage the prices of your products." - }, - "edit": { - "header": "Edit Price List" - }, - "configuration": { - "header": "Configuration", - "editHeader": "Edit Price List Configuration" - }, - "warnings": { - "delete": "You are about to delete the price list {{name}}. This action cannot be undone." - }, - "status": { - "draft": "Draft", - "expired": "Expired", - "active": "Active", - "scheduled": "Scheduled" - }, - "type": { - "sale": "Sale", - "override": "Override" - }, - "products": { - "deleteProductsPricesWarning_one": "You are about to delete {{count}} product price. This action cannot be undone.", - "deleteProductsPricesWarning_other": "You are about to delete {{count}} product prices. This action cannot be undone." - }, - "prices": { - "addPrices": "Add prices", - "editPrices": "Edit prices" - }, - "table": { - "pricesHeader": "Prices" - }, - "fields": { - "typeHint": "Choose the type of price you want to create.", - "saleTypeHint": "Sale prices are temporary price changes for products.", - "overrideTypeHint": "Overrides are usually used to create customer-specific prices.", - "startDateLabel": "Price list has a start date?", - "startDateHint": "Schedule the price list to activate in the future.", - "endDateLabel": "Price list has an expiry date?", - "endDateHint": "Schedule the price list to deactivate in the future.", - "customerAvailabilityLabel": "Customer availability", - "customerAvailabilityHint": "Choose which customer groups the price list should be applied to.", - "customerAvailabilityNoSelectionLabel": "No customer groups selected", - "priceOverridesLabel": "Price overrides" - }, - "actions": { - "addCustomerGroups": "Add customer groups" - } - }, - "profile": { - "domain": "Profile", - "manageYourProfileDetails": "Manage your profile details", - "fields": { - "languageLabel": "Language", - "usageInsightsLabel": "Usage insights" - }, - "edit": { - "header": "Edit Profile", - "languageHint": "The language you want to use in the admin dashboard. This will not change the language of your store.", - "languagePlaceholder": "Select language", - "usageInsightsHint": "Share usage insights and help us improve Medusa. You can read more about what we collect and how we use it in our <0>documentation." - }, - "toast": { - "edit": "Profiles changes saved" - } - }, - "users": { - "domain": "Users", - "editUser": "Edit User", - "inviteUser": "Invite User", - "inviteUserHint": "Invite a new user to your store.", - "sendInvite": "Send invite", - "pendingInvites": "Pending Invites", - "deleteInviteWarning": "You are about to delete the invite for {{email}}. This action cannot be undone.", - "resendInvite": "Resend invite", - "copyInviteLink": "Copy invite link", - "expiredOnDate": "Expired on {{date}}", - "validFromUntil": "Valid from <0>{{from}} - <1>{{until}}", - "acceptedOnDate": "Accepted on {{date}}", - "inviteStatus": { - "accepted": "Accepted", - "pending": "Pending", - "expired": "Expired" - }, - "roles": { - "admin": "Admin", - "developer": "Developer", - "member": "Member" - }, - "deleteUserWarning": "You are about to delete the user {{name}}. This action cannot be undone.", - "invite": "Invite" - }, - "store": { - "domain": "Store", - "manageYourStoresDetails": "Manage your store's details", - "editStore": "Edit store", - "defaultCurrency": "Default currency", - "defaultRegion": "Default region", - "swapLinkTemplate": "Swap link template", - "paymentLinkTemplate": "Payment link template", - "inviteLinkTemplate": "Invite link template", - "currencies": "Currencies", - "addCurrencies": "Add currencies", - "removeCurrencyWarning_one": "You are about to remove {{count}} currency from your store. Ensure that you have removed all prices using the currency before proceeding.", - "removeCurrencyWarning_other": "You are about to remove {{count}} currencies from your store. Ensure that you have removed all prices using the currencies before proceeding.", - "currencyAlreadyAdded": "The currency has already been added to your store.", - "edit": { - "header": "Edit Store" - }, - "toast": { - "update": "Store successfully updated", - "currenciesUpdated": "Currencies updated successfully", - "currenciesRemoved": "Removed currencies from the store successfully" - } - }, - "regions": { - "domain": "Regions", - "createRegion": "Create Region", - "createRegionHint": "Manage tax rates and providers for a set of countries.", - "addCountries": "Add countries", - "editRegion": "Edit Region", - "countriesHint": "Add the countries that should be included in this region.", - "deleteRegionWarning": "You are about to delete the region {{name}}. This action cannot be undone.", - "removeCountriesWarning_one": "You are about to remove {{count}} country from the region. This action cannot be undone.", - "removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.", - "removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.", - "taxInclusiveHint": "When enabled prices in the region will be tax inclusive.", - "providersHint": " Add which payment providers should be available in this region.", - "shippingOptions": "Shipping Options", - "deleteShippingOptionWarning": "You are about to delete the shipping option {{name}}. This action cannot be undone.", - "return": "Return", - "outbound": "Outbound", - "priceType": "Price Type", - "flatRate": "Flat Rate", - "calculated": "Calculated", - "toast": { - "delete": "Region deleted successfully", - "edit": "Region edit saved", - "create": "Region created successfully", - "countries": "Region countries updated successfully" - }, - "shippingOption": { - "createShippingOption": "Create Shipping Option", - "createShippingOptionHint": "Create a new shipping option for the region.", - "editShippingOption": "Edit Shipping Option", - "fulfillmentMethod": "Fulfillment Method", - "type": { - "outbound": "Outbound", - "outboundHint": "Use this if you are creating a shipping option for sending products to the customer.", - "return": "Return", - "returnHint": "Use this if you are creating a shipping option for the customer to return products to you." - }, - "priceType": { - "label": "Price Type", - "flatRate": "Flat rate", - "calculated": "Calculated" - }, - "availability": { - "adminOnly": "Admin only", - "adminOnlyHint": "When enabled the shipping option will only be available in the admin dashboard, and not in the storefront." - }, - "taxInclusiveHint": "When enabled, the shipping option's price will be tax inclusive.", - "requirements": { - "label": "Requirements", - "hint": "Specify the requirements for the shipping option." - } - } - }, - "taxes": { - "domain": "Tax Regions", - "domainDescription": "Manage your tax region", - "countries": { - "taxCountriesHint": "Tax settings apply to the listed countries." - }, - "settings": { - "editTaxSettings": "Edit Tax Settings", - "taxProviderLabel": "Tax provider", - "systemTaxProviderLabel": "System Tax Provider", - "calculateTaxesAutomaticallyLabel": "Calculate taxes automatically", - "calculateTaxesAutomaticallyHint": "When enabled, tax rates will be calculated automatically and applied to carts. When disabled, taxes must be manually computed at checkout. Manual taxes are recommended for usage with third-party tax providers.", - "applyTaxesOnGiftCardsLabel": "Apply taxes on gift cards", - "applyTaxesOnGiftCardsHint": "When enabled, taxes will be applied to gift cards at checkout. In some countries, tax regulations require the application of taxes to gift cards upon purchase.", - "defaultTaxRateLabel": "Default tax rate", - "defaultTaxCodeLabel": "Default tax code" - }, - "defaultRate": { - "sectionTitle": "Default Tax Rate" - }, - "taxRate": { - "sectionTitle": "Tax Rates", - "createTaxRate": "Create Tax Rate", - "createTaxRateHint": "Create a new tax rate for the region.", - "deleteRateDescription": "You are about to delete the tax rate {{name}}. This action cannot be undone.", - "editTaxRate": "Edit Tax Rate", - "editRateAction": "Edit rate", - "editOverridesAction": "Edit overrides", - "editOverridesTitle": "Edit Tax Rate Overrides", - "editOverridesHint": "Specify the overrides for the tax rate.", - "deleteTaxRateWarning": "You are about to delete the tax rate {{name}}. This action cannot be undone.", - "productOverridesLabel": "Product overrides", - "productOverridesHint": "Specify the product overrides for the tax rate.", - "addProductOverridesAction": "Add product overrides", - "productTypeOverridesLabel": "Product type overrides", - "productTypeOverridesHint": "Specify the product type overrides for the tax rate.", - "addProductTypeOverridesAction": "Add product type overrides", - "shippingOptionOverridesLabel": "Shipping option overrides", - "shippingOptionOverridesHint": "Specify the shipping option overrides for the tax rate.", - "addShippingOptionOverridesAction": "Add shipping option overrides", - "productOverridesHeader": "Products", - "productTypeOverridesHeader": "Product Types", - "shippingOptionOverridesHeader": "Shipping Options" - } - }, - "locations": { - "domain": "Locations", - "editLocation": "Edit location", - "addSalesChannels": "Add sales channels", - "noLocationsFound": "No locations found", - "selectLocations": "Select locations that stock the item.", - "deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.", - "removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.", - "removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location.", - "toast": { - "create": "Location created sucessfully", - "update": "Location updated sucessfully", - "removeChannel": "Sales channel removed sucessfully" - } - }, - "reservations": { - "domain": "Reservations", - "deleteWarning": "You are about to delete a reservation. This action cannot be undone." - }, - "salesChannels": { - "domain": "Sales Channels", - "createSalesChannel": "Create Sales Channel", - "createSalesChannelHint": "Create a new sales channel to sell your products on.", - "enabledHint": "Specify if the sales channel is enabled or disabled.", - "removeProductsWarning_one": "You are about to remove {{count}} product from {{sales_channel}}.", - "removeProductsWarning_other": "You are about to remove {{count}} products from {{sales_channel}}.", - "addProducts": "Add Products", - "editSalesChannel": "Edit sales channel", - "productAlreadyAdded": "The product has already been added to the sales channel.", - "deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone.", - "toast": { - "create": "Sales channel created successfully", - "update": "Sales channel updated successfully", - "delete": "Sales channel deleted successfully" - } - }, - "apiKeyManagement": { - "domain": { - "publishable": "Publishable API Keys", - "secret": "Secret API Keys" - }, - "status": { - "active": "Active", - "revoked": "Revoked" - }, - "type": { - "publishable": "Publishable", - "secret": "Secret" - }, - "create": { - "createPublishableHeader": "Create Publishable API Key", - "createPublishableHint": "Create a new publishable API key to limit the scope of requests to specific sales channels.", - "createSecretHeader": "Create Secret API Key", - "createSecretHint": "Create a new secret API key to access the Medusa API.", - "secretKeyCreatedHeader": "Secret Key Created", - "secretKeyCreatedHint": "Your new secret key has been generated. Copy and securely store it now. This is the only time it will be displayed.", - "copySecretTokenSuccess": "Secret key was copied to clipboard.", - "copySecretTokenFailure": "Failed to copy secret key to clipboard.", - "successToast": "API key was successfully created." - }, - "edit": { - "header": "Edit API Key", - "successToast": "API key {{title}} was successfully updated." - }, - "salesChannels": { - "successToast_one": "{{count}} sales channel was successfully added to the API key.", - "successToast_other": "{{count}} sales channels were successfully added to the API key.", - "alreadyAddedTooltip": "The sales channel has already been added to the API key." - }, - "delete": { - "warning": "You are about to delete the API key {{title}}. This action cannot be undone.", - "successToast": "API key {{title}} was successfully deleted." - }, - "revoke": { - "warning": "You are about to revoke the API key {{title}}. This action cannot be undone.", - "successToast": "API key {{title}} was successfully revoked." - }, - "removeSalesChannel": { - "warning": "You are about to remove the sales channel {{name}} from the API key. This action cannot be undone.", - "warningBatch_one": "You are about to remove {{count}} sales channel from the API key. This action cannot be undone.", - "warningBatch_other": "You are about to remove {{count}} sales channels from the API key. This action cannot be undone.", - "successToast": "Sales channel was successfully removed from the API key.", - "successToastBatch_one": "{{count}} sales channel was successfully removed from the API key.", - "successToastBatch_other": "{{count}} sales channels were successfully removed from the API key." - }, - "actions": { - "revoke": "Revoke API key", - "copy": "Copy API key", - "copySuccessToast": "API key was copied to clipboard." - }, - "table": { - "lastUsedAtHeader": "Last Used At", - "createdAtHeader": "Revoked At" - }, - "fields": { - "lastUsedAtLabel": "Last used at", - "revokedByLabel": "Revoked by", - "createdByLabel": "Created by" - } - }, - "returnReasons": { - "domain": "Return Reasons", - "calloutHint": "Manage the reasons to categorize returns.", - "deleteReasonWarning": "You are about to delete the return reason {{label}}. This action cannot be undone.", - "createReason": "Create Return Reason", - "createReasonHint": "Create a new return reason to categorize returns.", - "editReason": "Edit Return Reason", - "valueTooltip": "The value should be a unique identifier for the return reason." - }, - "login": { - "forgotPassword": "Forgot password? - <0>Reset", - "title": "Log in", - "hint": "to continue to Medusa" - }, - "invite": { - "title": "Create your account", - "hint": "to continue to Medusa", - "createAccount": "Create account", - "alreadyHaveAccount": "Already have an account? - <0>Log in", - "emailTooltip": "Your email cannot be changed. If you would like to use another email, a new invite must be sent.", - "invalidInvite": "The invite is invalid or has expired.", - "successTitle": "Your account has been created", - "successHint": "Get started with Medusa Admin right away.", - "successAction": "Sign in to start using Medusa", - "invalidTokenTitle": "Your invite token is invalid", - "invalidTokenHint": "Try requesting a new invite link.", - "passwordMismatch": "Passwords do not match", - "toast": { - "accepted": "Invite successfully accepted" - } - }, - "resetPassword": { - "title": "Reset password", - "hint": "Enter your email below, and we will send you instructions on how to reset your password.", - "email": "Email", - "sendResetInstructions": "Send reset instructions", - "backToLogin": "You can always go back - <0>Log in", - "newPasswordHint": "Choose a new password below.", - "invalidTokenTitle": "Your reset token is invalid", - "invalidTokenHint": "Try requesting a new reset link.", - "expiredTokenTitle": "Your reset token has expired", - "goToResetPassword": "Go to Reset Password", - "resetPassword": "Reset password", - "tokenExpiresIn": "Token expires in <0>{{time}} minutes", - "successfulRequest": "We have sent you an email with instructions on how to reset your password. If you don't receive an email, please check your spam folder or try again." - }, - "workflowExecutions": { - "domain": "Workflows", - "transactionIdLabel": "Transaction ID", - "workflowIdLabel": "Workflow ID", - "progressLabel": "Progress", - "stepsCompletedLabel_one": "{{completed}} of {{count}} step", - "stepsCompletedLabel_other": "{{completed}} of {{count}} steps", - "history": { - "sectionTitle": "History", - "runningState": "Running...", - "awaitingState": "Awaiting", - "failedState": "Failed", - "definitionLabel": "Definition", - "outputLabel": "Output", - "compensateInputLabel": "Compensate input", - "revertedLabel": "Reverted", - "errorLabel": "Error" - }, - "state": { - "done": "Done", - "failed": "Failed", - "reverted": "Reverted", - "invoking": "Invoking", - "compensating": "Compensating", - "notStarted": "Not started" - }, - "transaction": { - "state": { - "waitingToCompensate": "Waiting to compensate" - } - }, - "step": { - "state": { - "skipped": "Skipped", - "dormant": "Dormant", - "timeout": "Timeout" - } - } - }, - "errors": { - "serverError": "Server error - Try again later.", - "invalidCredentials": "Wrong email or password" - }, - "statuses": { - "scheduled": "Scheduled", - "expired": "Expired", - "active": "Active", - "enabled": "Enabled", - "disabled": "Disabled" - }, - "fields": { - "amount": "Amount", - "refundAmount": "Refund amount", - "name": "Name", - "default": "Default", - "lastName": "Last Name", - "firstName": "First Name", - "title": "Title", - "description": "Description", - "email": "Email", - "password": "Password", - "repeatPassword": "Repeat Password", - "confirmPassword": "Confirm Password", - "newPassword": "New Password", - "repeatNewPassword": "Repeat New Password", - "categories": "Categories", - "configurations": "Configurations", - "conditions": "Conditions", - "category": "Category", - "collection": "Collection", - "discountable": "Discountable", - "handle": "Handle", - "subtitle": "Subtitle", - "limit": "Limit", - "tags": "Tags", - "type": "Type", - "reason": "Reason", - "note": "Note", - "none": "none", - "all": "all", - "percentage": "Percentage", - "sales_channels": "Sales Channels", - "customer_groups": "Customer Groups", - "product_tags": "Product Tags", - "product_types": "Product Types", - "product_collections": "Product Collections", - "status": "Status", - "code": "Code", - "value": "Value", - "disabled": "Disabled", - "dynamic": "Dynamic", - "normal": "Normal", - "years": "Years", - "months": "Months", - "days": "Days", - "hours": "Hours", - "minutes": "Minutes", - "totalRedemptions": "Total Redemptions", - "countries": "Countries", - "paymentProviders": "Payment Providers", - "fulfillmentProviders": "Fulfillment Providers", - "fulfillmentProvider": "Fulfillment Provider", - "providers": "Providers", - "availability": "Availability", - "inventory": "Inventory", - "optional": "Optional", - "note": "Note", - "taxInclusivePricing": "Tax inclusive pricing", - "taxRate": "Tax Rate", - "taxCode": "Tax Code", - "currency": "Currency", - "address": "Address", - "address2": "Apartment, suite, etc.", - "city": "City", - "postalCode": "Postal Code", - "country": "Country", - "state": "State", - "province": "Province", - "company": "Company", - "phone": "Phone", - "metadata": "Metadata", - "selectCountry": "Select country", - "products": "Products", - "variants": "Variants", - "orders": "Orders", - "account": "Account", - "total": "Total", - "totalExclTax": "Total excl. tax", - "subtotal": "Subtotal", - "shipping": "Shipping", - "tax": "Tax", - "created": "Created", - "key": "Key", - "customer": "Customer", - "date": "Date", - "order": "Order", - "fulfillment": "Fulfillment", - "provider": "Provider", - "payment": "Payment", - "items": "Items", - "salesChannel": "Sales Channel", - "region": "Region", - "discount": "Discount", - "role": "Role", - "sent": "Sent", - "salesChannels": "Sales Channels", - "product": "Product", - "createdAt": "Created at", - "updatedAt": "Updated at", - "revokedAt": "Revoked at", - "true": "True", - "false": "False", - "giftCard": "Gift Card", - "tag": "Tag", - "dateIssued": "Date issued", - "issuedDate": "Issued date", - "expiryDate": "Expiry date", - "price": "Price", - "priceTemplate": "Price {{regionOrCountry}}", - "height": "Height", - "width": "Width", - "length": "Length", - "weight": "Weight", - "midCode": "MID code", - "hsCode": "HS code", - "ean": "EAN", - "upc": "UPC", - "inventoryQuantity": "Inventory quantity", - "barcode": "Barcode", - "countryOfOrigin": "Country of origin", - "material": "Material", - "thumbnail": "Thumbnail", - "sku": "SKU", - "managedInventory": "Managed inventory", - "allowBackorder": "Allow backorder", - "inStock": "In stock", - "location": "Location", - "quantity": "Quantity", - "variant": "Variant", - "id": "ID", - "parent": "Parent", - "minSubtotal": "Min. Subtotal", - "maxSubtotal": "Max. Subtotal", - "shippingProfile": "Shipping Profile", - "summary": "Summary", - "details": "Details", - "label": "Label", - "rate": "Rate", - "requiresShipping": "Requires shipping", - "unitPrice": "Unit price", - "startDate": "Start date", - "endDate": "End date", - "draft": "Draft", - "values": "Values" - }, - "metadata": { - "warnings": { - "ignoredKeys": "This entities metadata contains complex values that we currently don't support editing through the admin UI. Due to this, the following keys are currently not being displayed: {{keys}}. You can still edit these values using the API." - } - }, - "dateTime": { - "years_one": "Year", - "years_other": "Years", - "months_one": "Month", - "months_other": "Months", - "weeks_one": "Week", - "weeks_other": "Weeks", - "days_one": "Day", - "days_other": "Days", - "hours_one": "Hour", - "hours_other": "Hours", - "minutes_one": "Minute", - "minutes_other": "Minutes", - "seconds_one": "Second", - "seconds_other": "Seconds" - } -} diff --git a/packages/admin-next/dashboard/src/components/common/conditional-tooltip/conditional-tooltip.tsx b/packages/admin-next/dashboard/src/components/common/conditional-tooltip/conditional-tooltip.tsx index 2045800ae2..829da904d6 100644 --- a/packages/admin-next/dashboard/src/components/common/conditional-tooltip/conditional-tooltip.tsx +++ b/packages/admin-next/dashboard/src/components/common/conditional-tooltip/conditional-tooltip.tsx @@ -1,18 +1,19 @@ import { Tooltip } from "@medusajs/ui" -import { PropsWithChildren, ReactNode } from "react" +import { ComponentPropsWithoutRef, PropsWithChildren } from "react" -type ConditionalTooltipProps = PropsWithChildren<{ - content: ReactNode - showTooltip?: boolean -}> +type ConditionalTooltipProps = PropsWithChildren< + ComponentPropsWithoutRef & { + showTooltip?: boolean + } +> export const ConditionalTooltip = ({ children, - content, showTooltip = false, + ...props }: ConditionalTooltipProps) => { if (showTooltip) { - return {children} + return {children} } return children diff --git a/packages/admin-next/dashboard/src/components/forms/metadata-form/metadata-form.tsx b/packages/admin-next/dashboard/src/components/forms/metadata-form/metadata-form.tsx index 7529453ce3..f19ff2d33a 100644 --- a/packages/admin-next/dashboard/src/components/forms/metadata-form/metadata-form.tsx +++ b/packages/admin-next/dashboard/src/components/forms/metadata-form/metadata-form.tsx @@ -292,7 +292,7 @@ const GridInput = forwardRef< {...props} autoComplete="off" className={clx( - "txt-compact-small text-ui-fg-base placeholder:text-ui-fg-muted disabled:text-ui-fg-disabled disabled:bg-ui-bg-base px-2 py-1.5 outline-none", + "txt-compact-small text-ui-fg-base placeholder:text-ui-fg-muted disabled:text-ui-fg-disabled disabled:bg-ui-bg-base bg-transparent px-2 py-1.5 outline-none", className )} /> diff --git a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx index b2d2b5f519..d489633733 100644 --- a/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx @@ -1,15 +1,20 @@ import { + BuildingStorefront, Buildings, ChevronDownMini, + CogSixTooth, CurrencyDollar, + EllipsisHorizontal, + MagnifyingGlass, MinusMini, + OpenRectArrowOut, ReceiptPercent, ShoppingCart, SquaresPlus, Tag, Users, } from "@medusajs/icons" -import { Avatar, Text } from "@medusajs/ui" +import { Avatar, DropdownMenu, Text, clx } from "@medusajs/ui" import * as Collapsible from "@radix-ui/react-collapsible" import { useTranslation } from "react-i18next" @@ -20,7 +25,12 @@ import { Skeleton } from "../../common/skeleton" import { NavItem, NavItemProps } from "../../layout/nav-item" import { Shell } from "../../layout/shell" +import { Link, useLocation, useNavigate } from "react-router-dom" import routes from "virtual:medusa/routes/links" +import { useLogout } from "../../../hooks/api" +import { queryClient } from "../../../lib/query-client" +import { useSearch } from "../../../providers/search-provider" +import { UserMenu } from "../user-menu" export const MainLayout = () => { return ( @@ -40,41 +50,129 @@ const MainSidebar = () => { - - +
+
+ + +
+ +
+
+ +
) } +const Logout = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const { mutateAsync: logoutMutation } = useLogout() + + const handleLogout = async () => { + await logoutMutation(undefined, { + onSuccess: () => { + /** + * When the user logs out, we want to clear the query cache + */ + queryClient.clear() + navigate("/login") + }, + }) + } + + return ( + +
+ + {t("app.menus.actions.logout")} +
+
+ ) +} + const Header = () => { - const { store, isError, error } = useStore() + const { t } = useTranslation() + const { store, isPending, isError, error } = useStore() const name = store?.name const fallback = store?.name?.slice(0, 1).toUpperCase() + const isLoaded = !isPending && !!store && !!name && !!fallback + if (isError) { throw error } return ( -
-
-
+
+ + {fallback ? ( - + ) : ( - + )} - {name ? ( - - {store.name} - - ) : ( - - )} -
-
+
+ {name ? ( + + {store.name} + + ) : ( + + )} +
+ + + {isLoaded && ( + +
+ +
+ + {name} + + + {t("app.nav.main.store")} + +
+
+ + + + + {t("app.nav.main.storeSettings")} + + + + +
+ )} +
) } @@ -156,11 +254,40 @@ const useCoreRoutes = (): Omit[] => { ] } +const Searchbar = () => { + const { t } = useTranslation() + const { toggleSearch } = useSearch() + + return ( +
+ +
+ ) +} + const CoreRouteSection = () => { const coreRoutes = useCoreRoutes() return (
) } + +const UtilitySection = () => { + const location = useLocation() + const { t } = useTranslation() + + return ( +
+ } + /> +
+ ) +} + +const UserSection = () => { + return ( +
+
+ +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx b/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx index b0bea62dd3..f710953f6f 100644 --- a/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx +++ b/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx @@ -1,9 +1,18 @@ -import { Text, clx } from "@medusajs/ui" +import { Kbd, Text, clx } from "@medusajs/ui" import * as Collapsible from "@radix-ui/react-collapsible" -import { ReactNode, useEffect, useState } from "react" -import { Link, useLocation } from "react-router-dom" +import { + PropsWithChildren, + ReactNode, + useCallback, + useEffect, + useState, +} from "react" +import { useTranslation } from "react-i18next" +import { NavLink, useLocation } from "react-router-dom" +import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks" +import { ConditionalTooltip } from "../../common/conditional-tooltip" -type ItemType = "core" | "extension" +type ItemType = "core" | "extension" | "setting" type NestedItemProps = { label: string @@ -19,6 +28,60 @@ export type NavItemProps = { from?: string } +const BASE_NAV_LINK_CLASSES = + "text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus" +const ACTIVE_NAV_LINK_CLASSES = + "bg-ui-bg-base shadow-elevation-card-rest text-ui-fg-base hover:bg-ui-bg-base" +const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 w-full text-ui-fg-muted" +const SETTING_NAV_LINK_CLASSES = "pl-2" + +const getIsOpen = ( + to: string, + items: NestedItemProps[] | undefined, + pathname: string +) => { + return [to, ...(items?.map((i) => i.to) ?? [])].some((p) => + pathname.startsWith(p) + ) +} + +const NavItemTooltip = ({ + to, + children, +}: PropsWithChildren<{ to: string }>) => { + const { t } = useTranslation() + const globalShortcuts = useGlobalShortcuts() + const shortcut = globalShortcuts.find((s) => s.to === to) + + return ( + + {shortcut?.label} +
+ {shortcut?.keys.Mac?.map((key, index) => ( +
+ {key} + {index < (shortcut.keys.Mac?.length || 0) - 1 && ( + + {t("app.keyboardShortcuts.then")} + + )} +
+ ))} +
+ + } + side="right" + delayDuration={1500} + > +
{children}
+
+ ) +} + export const NavItem = ({ icon, label, @@ -27,111 +90,125 @@ export const NavItem = ({ type = "core", from, }: NavItemProps) => { - const location = useLocation() - - const [open, setOpen] = useState( - [to, ...(items?.map((i) => i.to) ?? [])].some((p) => - location.pathname.startsWith(p) - ) - ) + const { pathname } = useLocation() + const [open, setOpen] = useState(getIsOpen(to, items, pathname)) useEffect(() => { - setOpen( - [to, ...(items?.map((i) => i.to) ?? [])].some((p) => - location.pathname.startsWith(p) - ) - ) - }, [location.pathname, to, items]) + setOpen(getIsOpen(to, items, pathname)) + }, [pathname, to, items]) + + const navLinkClassNames = useCallback( + ({ + isActive, + isNested = false, + isSetting = false, + }: { + isActive: boolean + isNested?: boolean + isSetting?: boolean + }) => + clx(BASE_NAV_LINK_CLASSES, { + [NESTED_NAV_LINK_CLASSES]: isNested, + [ACTIVE_NAV_LINK_CLASSES]: isActive, + [SETTING_NAV_LINK_CLASSES]: isSetting, + }), + [] + ) + + const isSetting = type === "setting" return (
- 0, + + - - - {label} - - + className={(props) => + clx(navLinkClassNames({ ...props, isSetting }), { + "max-lg:hidden": !!items?.length, + }) + } + > + {type !== "setting" && ( +
+ +
+ )} + + {label} + +
+
{items && items.length > 0 && ( - +
+ +
{label}
- -
-
-
-
- - - {label} - - -
-
    - {items.map((item) => { - return ( -
  • -
    -
    -
    - +
    +
      +
    • + + + clx( + navLinkClassNames({ + ...props, + isNested: true, + isSetting, + }) + ) + } > - {item.label} + {label} - -
    • - ) - })} -
    + + +
  • + {items.map((item) => { + return ( +
  • + + + clx( + navLinkClassNames({ + ...props, + isNested: true, + isSetting, + }) + ) + } + > + + {item.label} + + + +
  • + ) + })} +
+
)} diff --git a/packages/admin-next/dashboard/src/components/layout/pages/two-column-page/two-column-page.tsx b/packages/admin-next/dashboard/src/components/layout/pages/two-column-page/two-column-page.tsx index d891150182..bb95dbba9a 100644 --- a/packages/admin-next/dashboard/src/components/layout/pages/two-column-page/two-column-page.tsx +++ b/packages/admin-next/dashboard/src/components/layout/pages/two-column-page/two-column-page.tsx @@ -128,7 +128,7 @@ const Sidebar = ({ return (
{ return ( @@ -25,10 +26,6 @@ const useSettingRoutes = (): NavItemProps[] => { return useMemo( () => [ - { - label: t("profile.domain"), - to: "/settings/profile", - }, { label: t("store.domain"), to: "/settings/store", @@ -84,6 +81,20 @@ const useDeveloperRoutes = (): NavItemProps[] => { ) } +const useMyAccountRoutes = (): NavItemProps[] => { + const { t } = useTranslation() + + return useMemo( + () => [ + { + label: t("profile.domain"), + to: "/settings/profile", + }, + ], + [t] + ) +} + const useExtensionRoutes = (): NavItemProps[] => { const links = routes.links @@ -115,12 +126,64 @@ const SettingsSidebar = () => { const routes = useSettingRoutes() const developerRoutes = useDeveloperRoutes() const extensionRoutes = useExtensionRoutes() + const myAccountRoutes = useMyAccountRoutes() const { t } = useTranslation() - const location = useLocation() + return ( + + ) +} + +const Header = () => { const [from, setFrom] = useState("/orders") + const { t } = useTranslation() + const location = useLocation() + useEffect(() => { if (location.state?.from) { setFrom(getSafeFromValue(location.state.from)) @@ -128,107 +191,70 @@ const SettingsSidebar = () => { }, [location]) return ( - + +
+ ) +} + +const CollapsibleSection = ({ + label, + items, +}: { + label: string + items: NavItemProps[] +}) => { + return ( + +
+
+ + {label} + + + + + + +
+
+ +
+ +
+
+
+ ) +} + +const UserSection = () => { + return ( +
+
+ +
+ +
) } diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx index fed94dee7d..e360f0960c 100644 --- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -1,48 +1,19 @@ import * as Dialog from "@radix-ui/react-dialog" import { - ArrowRightOnRectangle, BellAlert, - BookOpen, - Calendar, - CircleHalfSolid, - CogSixTooth, - Keyboard, - MagnifyingGlass, SidebarLeft, TriangleRightMini, - User as UserIcon, + XMark, } from "@medusajs/icons" -import { - Avatar, - Button, - DropdownMenu, - Heading, - IconButton, - Kbd, - Text, - clx, -} from "@medusajs/ui" -import { PropsWithChildren, useState } from "react" -import { - Link, - Outlet, - UIMatch, - useLocation, - useMatches, - useNavigate, -} from "react-router-dom" +import { IconButton, clx } from "@medusajs/ui" +import { PropsWithChildren } from "react" +import { Link, Outlet, UIMatch, useMatches } from "react-router-dom" import { useTranslation } from "react-i18next" -import { useLogout } from "../../../hooks/api/auth" -import { useMe } from "../../../hooks/api/users" -import { queryClient } from "../../../lib/query-client" import { KeybindProvider } from "../../../providers/keybind-provider" import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks" -import { useSearch } from "../../../providers/search-provider" import { useSidebar } from "../../../providers/sidebar-provider" -import { useTheme } from "../../../providers/theme-provider" -import { Skeleton } from "../../common/skeleton" export const Shell = ({ children }: PropsWithChildren) => { const globalShortcuts = useGlobalShortcuts() @@ -136,239 +107,6 @@ const Breadcrumbs = () => { ) } -const UserBadge = () => { - const { user, isLoading, isError, error } = useMe() - - const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ") - const displayName = name || user?.email - - const fallback = displayName ? displayName[0].toUpperCase() : null - - if (isLoading) { - return ( - - ) - } - - if (isError) { - throw error - } - - return ( - - - - ) -} - -const ThemeToggle = () => { - const { theme, setTheme } = useTheme() - - return ( - - - - Theme - - - - { - e.preventDefault() - setTheme("light") - }} - > - Light - - { - e.preventDefault() - setTheme("dark") - }} - > - Dark - - - - - ) -} - -const Logout = () => { - const navigate = useNavigate() - const { mutateAsync: logoutMutation } = useLogout() - - const handleLogout = async () => { - await logoutMutation(undefined, { - onSuccess: () => { - /** - * When the user logs out, we want to clear the query cache - */ - queryClient.clear() - navigate("/login") - }, - }) - } - - return ( - -
- - Logout -
-
- ) -} - -const Profile = () => { - const location = useLocation() - - return ( - - - - Profile - - - ) -} - -const GlobalKeybindsModal = (props: { - open: boolean - onOpenChange: (open: boolean) => void -}) => { - const { t } = useTranslation() - const globalShortcuts = useGlobalShortcuts() - - return ( - - - - -
- - Keyboard Shortcuts - -
-
- {globalShortcuts.map((shortcut, index) => { - return ( -
- {shortcut.label} -
- {shortcut.keys.Mac?.map((key, index) => { - return {key} - })} -
-
- ) - })} -
-
- - - -
-
-
-
- ) -} - -const LoggedInUser = () => { - const [openMenu, setOpenMenu] = useState(false) - const [openModal, setOpenModal] = useState(false) - - const toggleModal = () => { - setOpenMenu(false) - setOpenModal(!openModal) - } - - return ( -
- - - - - - - - - Documentation - - - - - - Changelog - - - - - Keyboard shortcuts - - - - - - - - -
- ) -} - -const SettingsLink = () => { - const location = useLocation() - - return ( - - - - - - ) -} - const ToggleNotifications = () => { return ( { ) } -const Searchbar = () => { - const { t } = useTranslation() - const { toggleSearch } = useSearch() - - return ( - - ) -} - const ToggleSidebar = () => { const { toggle } = useSidebar() @@ -410,6 +128,7 @@ const ToggleSidebar = () => { className="hidden lg:flex" variant="transparent" onClick={() => toggle("desktop")} + size="small" > @@ -417,6 +136,7 @@ const ToggleSidebar = () => { className="hidden max-lg:flex" variant="transparent" onClick={() => toggle("mobile")} + size="small" > @@ -426,20 +146,13 @@ const ToggleSidebar = () => { const Topbar = () => { return ( -
+
-
- -
-
- - -
- +
) @@ -460,13 +173,41 @@ const DesktopSidebarContainer = ({ children }: PropsWithChildren) => { } const MobileSidebarContainer = ({ children }: PropsWithChildren) => { + const { t } = useTranslation() const { mobile, toggle } = useSidebar() return ( toggle("mobile")}> - - + + +
+ + + + + + + {t("app.nav.accessibility.title")} + + + {t("app.nav.accessibility.description")} + +
{children}
diff --git a/packages/admin-next/dashboard/src/components/layout/user-menu/index.ts b/packages/admin-next/dashboard/src/components/layout/user-menu/index.ts new file mode 100644 index 0000000000..5a17251412 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/user-menu/index.ts @@ -0,0 +1 @@ +export * from "./user-menu" diff --git a/packages/admin-next/dashboard/src/components/layout/user-menu/user-menu.tsx b/packages/admin-next/dashboard/src/components/layout/user-menu/user-menu.tsx new file mode 100644 index 0000000000..d70add1554 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/user-menu/user-menu.tsx @@ -0,0 +1,342 @@ +import { + BookOpen, + CircleHalfSolid, + EllipsisHorizontal, + Keyboard, + OpenRectArrowOut, + TimelineVertical, + User as UserIcon, + XMark, +} from "@medusajs/icons" +import { + Avatar, + DropdownMenu, + Heading, + IconButton, + Input, + Kbd, + Text, + clx, +} from "@medusajs/ui" +import * as Dialog from "@radix-ui/react-dialog" +import { useTranslation } from "react-i18next" + +import { Skeleton } from "../../common/skeleton" + +import { useState } from "react" +import { Link, useLocation, useNavigate } from "react-router-dom" +import { useLogout, useMe } from "../../../hooks/api" +import { queryClient } from "../../../lib/query-client" +import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks" +import { useTheme } from "../../../providers/theme-provider" + +export const UserMenu = () => { + const { t } = useTranslation() + const location = useLocation() + + const [openMenu, setOpenMenu] = useState(false) + const [openModal, setOpenModal] = useState(false) + + const toggleModal = () => { + setOpenMenu(false) + setOpenModal(!openModal) + } + + return ( +
+ + + + + + + + + {t("app.menus.user.profileSettings")} + + + + + + + {t("app.menus.user.documentation")} + + + + + + {t("app.menus.user.changelog")} + + + + + + {t("app.menus.user.shortcuts")} + + + + + + + +
+ ) +} + +const UserBadge = () => { + const { user, isPending, isError, error } = useMe() + + const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ") + const displayName = name || user?.email + + const fallback = displayName ? displayName[0].toUpperCase() : null + + if (isPending) { + return ( + + ) + } + + if (isError) { + throw error + } + + return ( +
+ +
+ {fallback ? ( + + ) : ( + + )} +
+
+ {displayName ? ( + + {displayName} + + ) : ( + + )} +
+ +
+
+ ) +} + +const ThemeToggle = () => { + const { t } = useTranslation() + const { theme, setTheme } = useTheme() + + return ( + + + + {t("app.menus.user.theme.label")} + + + + { + e.preventDefault() + setTheme("system") + }} + > + {t("app.menus.user.theme.system")} + + { + e.preventDefault() + setTheme("light") + }} + > + {t("app.menus.user.theme.light")} + + { + e.preventDefault() + setTheme("dark") + }} + > + {t("app.menus.user.theme.dark")} + + + + + ) +} + +const Logout = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const { mutateAsync: logoutMutation } = useLogout() + + const handleLogout = async () => { + await logoutMutation(undefined, { + onSuccess: () => { + /** + * When the user logs out, we want to clear the query cache + */ + queryClient.clear() + navigate("/login") + }, + }) + } + + return ( + +
+ + {t("app.menus.actions.logout")} +
+
+ ) +} + +const GlobalKeybindsModal = (props: { + open: boolean + onOpenChange: (open: boolean) => void +}) => { + const { t } = useTranslation() + const globalShortcuts = useGlobalShortcuts() + + const [searchValue, onSearchValueChange] = useState("") + + const searchResults = searchValue + ? globalShortcuts.filter((shortcut) => { + return shortcut.label.toLowerCase().includes(searchValue?.toLowerCase()) + }) + : globalShortcuts + + return ( + + + + +
+
+
+ + {t("app.menus.user.shortcuts")} + + +
+
+ esc + + + + + +
+
+
+ onSearchValueChange(e.target.value)} + /> +
+
+
+ {searchResults.map((shortcut, index) => { + return ( +
+ {shortcut.label} +
+ {shortcut.keys.Mac?.map((key, index) => { + return ( +
+ {key} + {index < (shortcut.keys.Mac?.length || 0) - 1 && ( + + {t("app.keyboardShortcuts.then")} + + )} +
+ ) + })} +
+
+ ) + })} +
+
+
+
+ ) +} + +const UserItem = () => { + const { user, isPending, isError, error } = useMe() + + const loaded = !isPending && !!user + + if (!loaded) { + return
+ } + + const name = [user.first_name, user.last_name].filter(Boolean).join(" ") + const email = user.email + const fallback = name ? name[0].toUpperCase() : email[0].toUpperCase() + const avatar = user.avatar_url + + if (isError) { + throw error + } + + return ( +
+ +
+ + {name || email} + + {!!name && ( + + {email} + + )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/search/search.tsx b/packages/admin-next/dashboard/src/components/search/search.tsx index 04cf9c5e7f..9e137b1a61 100644 --- a/packages/admin-next/dashboard/src/components/search/search.tsx +++ b/packages/admin-next/dashboard/src/components/search/search.tsx @@ -10,7 +10,7 @@ import { } from "react" import { useTranslation } from "react-i18next" -import { useLocation } from "react-router-dom" +import { useLocation, useNavigate } from "react-router-dom" import { Shortcut, ShortcutType } from "../../providers/keybind-provider" import { useGlobalShortcuts } from "../../providers/keybind-provider/hooks" import { useSearch } from "../../providers/search-provider" @@ -20,6 +20,7 @@ export const Search = () => { const globalCommands = useGlobalShortcuts() const location = useLocation() const { t } = useTranslation() + const navigate = useNavigate() useEffect(() => { onOpenChange(false) @@ -40,9 +41,18 @@ export const Search = () => { })) }, [globalCommands]) - const handleSelect = (callback: () => void) => { - callback() + const handleSelect = (shortcut: Shortcut) => { onOpenChange(false) + + if (shortcut.to) { + navigate(shortcut.to) + return + } + + if (shortcut.callback) { + shortcut.callback() + return + } } return ( @@ -60,13 +70,25 @@ export const Search = () => { return ( handleSelect(item.callback)} + onSelect={() => handleSelect(item)} className="flex items-center justify-between" > {item.label}
{item.keys.Mac?.map((key, index) => { - return {key} + return ( +
+ {key} + {index < (item.keys.Mac?.length || 0) - 1 && ( + + {t("app.keyboardShortcuts.then")} + + )} +
+ ) })}
@@ -104,8 +126,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - - + + {children}
@@ -167,7 +189,7 @@ const CommandList = forwardRef< ({ layout = "fit", }: DataTableRootProps) => { const { t } = useTranslation() - const navigate = useNavigate() const [showStickyBorder, setShowStickyBorder] = useState(false) const scrollableRef = useRef(null) @@ -204,6 +203,13 @@ export const DataTableRoot = ({ return ( { + console.log("e.key", e.key, e.target) + + if (e.key === "x") { + row.toggleSelected() + } + }} data-selected={row.getIsSelected()} className={clx( "transition-fg group/row group relative [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap", diff --git a/packages/admin-next/dashboard/src/hooks/api/users.tsx b/packages/admin-next/dashboard/src/hooks/api/users.tsx index 1309d1915b..e6aa70ea4a 100644 --- a/packages/admin-next/dashboard/src/hooks/api/users.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/users.tsx @@ -1,3 +1,5 @@ +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" import { QueryKey, UseMutationOptions, @@ -5,11 +7,9 @@ import { useMutation, useQuery, } from "@tanstack/react-query" -import { client } from "../../lib/client" +import { sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" import { queryKeysFactory } from "../../lib/query-key-factory" -import { UpdateUserReq } from "../../types/api-payloads" -import { UserDeleteRes, UserListRes, UserRes } from "../../types/api-responses" const USERS_QUERY_KEY = "users" as const const usersQueryKeys = { @@ -18,10 +18,16 @@ const usersQueryKeys = { } export const useMe = ( - options?: UseQueryOptions + query?: HttpTypes.AdminUserParams, + options?: UseQueryOptions< + HttpTypes.AdminUserResponse, + FetchError, + HttpTypes.AdminUserResponse, + QueryKey + > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.users.me(), + queryFn: () => sdk.admin.user.me(query), queryKey: usersQueryKeys.me(), ...options, }) @@ -34,14 +40,19 @@ export const useMe = ( export const useUser = ( id: string, - query?: Record, + query?: HttpTypes.AdminUserParams, options?: Omit< - UseQueryOptions, + UseQueryOptions< + HttpTypes.AdminUserResponse, + FetchError, + HttpTypes.AdminUserResponse, + QueryKey + >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.users.retrieve(id, query), + queryFn: () => sdk.admin.user.retrieve(id, query), queryKey: usersQueryKeys.detail(id), ...options, }) @@ -50,14 +61,19 @@ export const useUser = ( } export const useUsers = ( - query?: Record, + query?: HttpTypes.AdminUserListParams, options?: Omit< - UseQueryOptions, + UseQueryOptions< + HttpTypes.AdminUserListResponse, + FetchError, + HttpTypes.AdminUserListResponse, + QueryKey + >, "queryFn" | "queryKey" > ) => { const { data, ...rest } = useQuery({ - queryFn: () => client.users.list(query), + queryFn: () => sdk.admin.user.list(query), queryKey: usersQueryKeys.list(query), ...options, }) @@ -65,12 +81,38 @@ export const useUsers = ( return { ...data, ...rest } } -export const useUpdateUser = ( - id: string, - options?: UseMutationOptions +export const useCreateUser = ( + query?: HttpTypes.AdminUserParams, + options?: UseMutationOptions< + HttpTypes.AdminUserResponse, + FetchError, + HttpTypes.AdminCreateUser, + QueryKey + > ) => { return useMutation({ - mutationFn: (payload) => client.users.update(id, payload), + mutationFn: (payload) => sdk.admin.user.create(payload, query), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateUser = ( + id: string, + query?: HttpTypes.AdminUserParams, + options?: UseMutationOptions< + HttpTypes.AdminUserResponse, + FetchError, + HttpTypes.AdminUpdateUser, + QueryKey + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.user.update(id, payload, query), onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) }) queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() }) @@ -86,10 +128,14 @@ export const useUpdateUser = ( export const useDeleteUser = ( id: string, - options?: UseMutationOptions + options?: UseMutationOptions< + HttpTypes.AdminUserDeleteResponse, + FetchError, + void + > ) => { return useMutation({ - mutationFn: () => client.users.delete(id), + mutationFn: () => sdk.admin.user.delete(id), onSuccess: (data, variables, context) => { queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) }) queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() }) diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index b0b54e6f3a..fbff88c9a4 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -90,12 +90,6 @@ "mustBeInt": "The value must be a whole number.", "mustBePositive": "The value must be a positive number." }, - "nav": { - "general": "General", - "developer": "Developer", - "extensions": "Extensions", - "settings": "Settings" - }, "actions": { "save": "Save", "saveAsDraft": "Save as draft", @@ -130,6 +124,7 @@ }, "app": { "search": { + "label": "Search", "allAreas": "All areas", "navigation": "Navigation", "openResult": "Open result", @@ -139,27 +134,74 @@ "pageShortcut": "Jump to", "settingShortcut": "Settings", "commandShortcut": "Commands", - "goToOrders": "Go to orders", - "goToProducts": "Go to products", - "goToCollections": "Go to collections", - "goToCategories": "Go to categories", - "goToCustomers": "Go to customers", - "goToCustomerGroups": "Go to customer groups", - "goToInventory": "Go to inventory", - "goToReservations": "Go to reservations", - "goToPriceLists": "Go to price lists", - "goToPromotions": "Go to promotions", - "goToCampaigns": "Go to campaigns", - "goToStore": "Go to store", - "goToUsers": "Go to users", - "goToRegions": "Go to regions", - "goToTaxRegions": "Go to tax region", - "goToSalesChannels": "Go to sales channels", - "goToProductTypes": "Go to product types", - "goToLocations": "Go to locations", - "goToPublishableApiKeys": "Go to publishable API keys", - "goToSecretApiKeys": "Go to secret API keys", - "goToWorkflows": "Go to workflows" + "then": "then", + "navigation": { + "goToOrders": "Orders", + "goToProducts": "Products", + "goToCollections": "Collections", + "goToCategories": "Categories", + "goToCustomers": "Customers", + "goToCustomerGroups": "Customer Groups", + "goToInventory": "Inventory", + "goToReservations": "Reservations", + "goToPriceLists": "Price Lists", + "goToPromotions": "Promotions", + "goToCampaigns": "Campaigns" + }, + "settings": { + "goToSettings": "Settings", + "goToStore": "Store", + "goToUsers": "Users", + "goToRegions": "Regions", + "goToTaxRegions": "Tax Regions", + "goToSalesChannels": "Sales Channels", + "goToProductTypes": "Product Types", + "goToLocations": "Locations", + "goToPublishableApiKeys": "Publishable API Keys", + "goToSecretApiKeys": "Secret API Keys", + "goToWorkflows": "Workflows", + "goToProfile": "Profile" + } + }, + "menus": { + "user": { + "documentation": "Documentation", + "changelog": "Changelog", + "shortcuts": "Shortcuts", + "profileSettings": "Profile settings", + "theme": { + "label": "Theme", + "dark": "Dark", + "light": "Light", + "system": "System" + } + }, + "store": { + "label": "Store", + "storeSettings": "Store settings" + }, + "actions": { + "logout": "Log out" + } + }, + "nav": { + "accessibility": { + "title": "Navigation", + "description": "Navigation menu for the dashboard." + }, + "common": { + "extensions": "Extensions" + }, + "main": { + "store": "Store", + "storeSettings": "Store settings" + }, + "settings": { + "header": "Settings", + "general": "General", + "developer": "Developer", + "myAccount": "My Account" + } } }, "filters": { diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx b/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx index e5984fbed3..c57c0c080e 100644 --- a/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx +++ b/packages/admin-next/dashboard/src/providers/keybind-provider/hooks.tsx @@ -29,6 +29,7 @@ export const useShortcuts = ({ debounce: number }) => { const [keys, setKeys] = useState([]) + const navigate = useNavigate() // eslint-disable-next-line react-hooks/exhaustive-deps const removeKeys = useCallback( @@ -42,6 +43,15 @@ export const useShortcuts = ({ if (shortcut && shortcut.callback) { shortcut.callback() setKeys([]) + + return + } + + if (shortcut && shortcut.to) { + navigate(shortcut.to) + setKeys([]) + + return } }, debounce / 2), [] @@ -105,170 +115,186 @@ export const useGlobalShortcuts = () => { keys: { Mac: ["G", "O"], }, - label: t("app.keyboardShortcuts.goToOrders"), + label: t("app.keyboardShortcuts.navigation.goToOrders"), type: "pageShortcut", - callback: () => navigate("/orders"), + to: "/orders", }, { keys: { Mac: ["G", "P"], }, - label: t("app.keyboardShortcuts.goToProducts"), + label: t("app.keyboardShortcuts.navigation.goToProducts"), type: "pageShortcut", - callback: () => navigate("/products"), - }, - { - keys: { - Mac: ["G", "P", "C"], - }, - label: t("app.keyboardShortcuts.goToCollections"), - type: "pageShortcut", - callback: () => navigate("/collections"), - }, - { - keys: { - Mac: ["G", "P", "A"], - }, - label: t("app.keyboardShortcuts.goToCategories"), - type: "pageShortcut", - callback: () => navigate("/categories"), + to: "/products", }, { keys: { Mac: ["G", "C"], }, - label: t("app.keyboardShortcuts.goToCustomers"), + label: t("app.keyboardShortcuts.navigation.goToCollections"), type: "pageShortcut", - callback: () => navigate("/customers"), + to: "/collections", }, { keys: { - Mac: ["G", "C", "G"], + Mac: ["G", "A"], }, - label: t("app.keyboardShortcuts.goToCustomerGroups"), + label: t("app.keyboardShortcuts.navigation.goToCategories"), type: "pageShortcut", - callback: () => navigate("/customer-groups"), + to: "/categories", + }, + { + keys: { + Mac: ["G", "U"], + }, + label: t("app.keyboardShortcuts.navigation.goToCustomers"), + type: "pageShortcut", + to: "/customers", + }, + { + keys: { + Mac: ["G", "G"], + }, + label: t("app.keyboardShortcuts.navigation.goToCustomerGroups"), + type: "pageShortcut", + to: "/customer-groups", }, { keys: { Mac: ["G", "I"], }, - label: t("app.keyboardShortcuts.goToInventory"), + label: t("app.keyboardShortcuts.navigation.goToInventory"), type: "pageShortcut", - callback: () => navigate("/inventory"), - }, - { - keys: { - Mac: ["G", "I", "R"], - }, - label: t("app.keyboardShortcuts.goToReservations"), - type: "pageShortcut", - callback: () => navigate("/reservations"), - }, - { - keys: { - Mac: ["G", "L"], - }, - label: t("app.keyboardShortcuts.goToPriceLists"), - type: "pageShortcut", - callback: () => navigate("/price-lists"), + to: "/inventory", }, { keys: { Mac: ["G", "R"], }, - label: t("app.keyboardShortcuts.goToPromotions"), + label: t("app.keyboardShortcuts.navigation.goToReservations"), type: "pageShortcut", - callback: () => navigate("/promotions"), + to: "/reservations", }, { keys: { - Mac: ["G", "R", "C"], + Mac: ["G", "L"], }, - label: t("app.keyboardShortcuts.goToCampaigns"), + label: t("app.keyboardShortcuts.navigation.goToPriceLists"), type: "pageShortcut", - callback: () => navigate("/campaigns"), - }, - // - { - keys: { - Mac: ["G", "S", "S"], - }, - label: t("app.keyboardShortcuts.goToStore"), - type: "settingShortcut", - callback: () => navigate("/settings/store"), + to: "/price-lists", }, { keys: { - Mac: ["G", "S", "U"], + Mac: ["G", "M"], }, - label: t("app.keyboardShortcuts.goToUsers"), - type: "settingShortcut", - callback: () => navigate("/settings/users"), + label: t("app.keyboardShortcuts.navigation.goToPromotions"), + type: "pageShortcut", + to: "/promotions", }, { keys: { - Mac: ["G", "S", "R"], + Mac: ["G", "K"], }, - label: t("app.keyboardShortcuts.goToRegions"), + label: t("app.keyboardShortcuts.navigation.goToCampaigns"), + type: "pageShortcut", + to: "/campaigns", + }, + // Settings + { + keys: { + Mac: ["G", ","], + }, + label: t("app.keyboardShortcuts.settings.goToSettings"), type: "settingShortcut", - callback: () => navigate("/settings/regions"), + to: "/settings", }, { keys: { - Mac: ["G", "S", "T"], + Mac: ["G", ",", "S"], }, - label: t("app.keyboardShortcuts.goToTaxRegions"), + label: t("app.keyboardShortcuts.settings.goToStore"), type: "settingShortcut", - callback: () => navigate("/settings/tax-regions"), + to: "/settings/store", }, { keys: { - Mac: ["G", "S", "A"], + Mac: ["G", ",", "U"], }, - label: t("app.keyboardShortcuts.goToSalesChannels"), + label: t("app.keyboardShortcuts.settings.goToUsers"), type: "settingShortcut", - callback: () => navigate("/settings/sales-channels"), + to: "/settings/users", }, { keys: { - Mac: ["G", "S", "P"], + Mac: ["G", ",", "R"], }, - label: t("app.keyboardShortcuts.goToProductTypes"), + label: t("app.keyboardShortcuts.settings.goToRegions"), type: "settingShortcut", - callback: () => navigate("/settings/product-types"), + to: "/settings/regions", }, { keys: { - Mac: ["G", "S", "L"], + Mac: ["G", ",", "T"], }, - label: t("app.keyboardShortcuts.goToLocations"), + label: t("app.keyboardShortcuts.settings.goToTaxRegions"), type: "settingShortcut", - callback: () => navigate("/settings/locations"), + to: "/settings/tax-regions", }, { keys: { - Mac: ["G", "S", "J"], + Mac: ["G", ",", "A"], }, - label: t("app.keyboardShortcuts.goToPublishableApiKeys"), + label: t("app.keyboardShortcuts.settings.goToSalesChannels"), type: "settingShortcut", - callback: () => navigate("/settings/publishable-api-keys"), + to: "/settings/sales-channels", }, { keys: { - Mac: ["G", "S", "K"], + Mac: ["G", ",", "P"], }, - label: t("app.keyboardShortcuts.goToSecretApiKeys"), + label: t("app.keyboardShortcuts.settings.goToProductTypes"), type: "settingShortcut", - callback: () => navigate("/settings/secret-api-keys"), + to: "/settings/product-types", }, { keys: { - Mac: ["G", "S", "W"], + Mac: ["G", ",", "L"], }, - label: t("app.keyboardShortcuts.goToWorkflows"), + label: t("app.keyboardShortcuts.settings.goToLocations"), type: "settingShortcut", - callback: () => navigate("/settings/workflows"), + to: "/settings/locations", + }, + { + keys: { + Mac: ["G", ",", "J"], + }, + label: t("app.keyboardShortcuts.settings.goToPublishableApiKeys"), + type: "settingShortcut", + to: "/settings/publishable-api-keys", + }, + { + keys: { + Mac: ["G", ",", "K"], + }, + label: t("app.keyboardShortcuts.settings.goToSecretApiKeys"), + type: "settingShortcut", + to: "/settings/secret-api-keys", + }, + { + keys: { + Mac: ["G", ",", "W"], + }, + label: t("app.keyboardShortcuts.settings.goToWorkflows"), + type: "settingShortcut", + to: "/settings/workflows", + }, + { + keys: { + Mac: ["G", ",", "M"], + }, + label: t("app.keyboardShortcuts.settings.goToProfile"), + type: "settingShortcut", + to: "/settings/profile", }, // Commands { diff --git a/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts b/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts index e4a9dde9ee..03fbd3e17c 100644 --- a/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts +++ b/packages/admin-next/dashboard/src/providers/keybind-provider/types.ts @@ -15,6 +15,14 @@ export type Shortcut = { keys: Keys type: ShortcutType label: string - callback: () => void _defaultKeys?: Keys -} +} & ( + | { + callback: () => void + to?: never + } + | { + to: string + callback?: never + } +) diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index 21a05636ef..f579d9f02b 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -624,6 +624,9 @@ export const RouteMap: RouteObject[] = [ children: [ { path: "/settings", + handle: { + crumb: () => "Settings", + }, element: , children: [ { diff --git a/packages/admin-next/dashboard/src/providers/search-provider/search-provider.tsx b/packages/admin-next/dashboard/src/providers/search-provider/search-provider.tsx index eb42c28c22..f4fab16c35 100644 --- a/packages/admin-next/dashboard/src/providers/search-provider/search-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/search-provider/search-provider.tsx @@ -1,12 +1,24 @@ import { PropsWithChildren, useEffect, useState } from "react" import { Search } from "../../components/search" +import { useSidebar } from "../sidebar-provider" import { SearchContext } from "./search-context" export const SearchProvider = ({ children }: PropsWithChildren) => { const [open, setOpen] = useState(false) + const { mobile, toggle } = useSidebar() const toggleSearch = () => { - setOpen(!open) + const update = !open + + /** + * If the mobile sidebar is open, then make sure + * to close it when opening the search + */ + if (update && mobile) { + toggle("mobile") + } + + setOpen(update) } useEffect(() => { diff --git a/packages/admin-next/dashboard/src/providers/theme-provider/index.ts b/packages/admin-next/dashboard/src/providers/theme-provider/index.ts index 17892d3c80..ecda64d0ea 100644 --- a/packages/admin-next/dashboard/src/providers/theme-provider/index.ts +++ b/packages/admin-next/dashboard/src/providers/theme-provider/index.ts @@ -1,3 +1,3 @@ -export type { Theme } from "./theme-context" +export type { ThemeOption as Theme } from "./theme-context" export * from "./theme-provider" export * from "./use-theme" diff --git a/packages/admin-next/dashboard/src/providers/theme-provider/theme-context.tsx b/packages/admin-next/dashboard/src/providers/theme-provider/theme-context.tsx index ae55b1cfda..b575d61baa 100644 --- a/packages/admin-next/dashboard/src/providers/theme-provider/theme-context.tsx +++ b/packages/admin-next/dashboard/src/providers/theme-provider/theme-context.tsx @@ -1,10 +1,11 @@ import { createContext } from "react" -export type Theme = "light" | "dark" +export type ThemeOption = "light" | "dark" | "system" +export type ThemeValue = "light" | "dark" type ThemeContextValue = { - theme: Theme - setTheme: (theme: Theme) => void + theme: ThemeOption + setTheme: (theme: ThemeOption) => void } export const ThemeContext = createContext(null) diff --git a/packages/admin-next/dashboard/src/providers/theme-provider/theme-provider.tsx b/packages/admin-next/dashboard/src/providers/theme-provider/theme-provider.tsx index 4460a1aa27..5b56ed5ec2 100644 --- a/packages/admin-next/dashboard/src/providers/theme-provider/theme-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/theme-provider/theme-provider.tsx @@ -1,16 +1,44 @@ import { PropsWithChildren, useEffect, useState } from "react" -import { Theme, ThemeContext } from "./theme-context" +import { ThemeContext, ThemeOption, ThemeValue } from "./theme-context" const THEME_KEY = "medusa_admin_theme" -export const ThemeProvider = ({ children }: PropsWithChildren) => { - const [state, setState] = useState( - (localStorage?.getItem(THEME_KEY) as Theme) || "light" - ) +function getDefaultValue(): ThemeOption { + const persisted = localStorage?.getItem(THEME_KEY) as ThemeOption - const setTheme = (theme: Theme) => { + if (persisted) { + return persisted + } + + return "system" +} + +function getThemeValue(selected: ThemeOption): ThemeValue { + if (selected === "system") { + if (window !== undefined) { + return window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + } + + // Default to light theme if we can't detect the system preference + return "light" + } + + return selected +} + +export const ThemeProvider = ({ children }: PropsWithChildren) => { + const [state, setState] = useState(getDefaultValue()) + const [value, setValue] = useState(getThemeValue(state)) + + const setTheme = (theme: ThemeOption) => { localStorage.setItem(THEME_KEY, theme) + + const themeValue = getThemeValue(theme) + setState(theme) + setValue(themeValue) } useEffect(() => { @@ -34,8 +62,8 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => { ) document.head.appendChild(css) - html.classList.remove(state === "light" ? "dark" : "light") - html.classList.add(state) + html.classList.remove(value === "light" ? "dark" : "light") + html.classList.add(value) /** * Re-enable transitions after the theme has been set, @@ -44,7 +72,7 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => { window.getComputedStyle(css).opacity document.head.removeChild(css) } - }, [state]) + }, [value]) return ( diff --git a/packages/admin-next/dashboard/src/routes/settings/settings.tsx b/packages/admin-next/dashboard/src/routes/settings/settings.tsx index 058b328f7c..8144bf050b 100644 --- a/packages/admin-next/dashboard/src/routes/settings/settings.tsx +++ b/packages/admin-next/dashboard/src/routes/settings/settings.tsx @@ -7,7 +7,7 @@ export const Settings = () => { useEffect(() => { if (location.pathname === "/settings") { - navigate("/settings/profile") + navigate("/settings/store", { replace: true }) } }, [location.pathname, navigate]) diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index 6ba91070da..8d8d28a476 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -23,6 +23,7 @@ import { Store } from "./store" import { TaxRate } from "./tax-rate" import { TaxRegion } from "./tax-region" import { Upload } from "./upload" +import { User } from "./user" export class Admin { public invite: Invite @@ -48,6 +49,7 @@ export class Admin { public taxRegion: TaxRegion public store: Store public productTag: ProductTag + public user: User public return: Return constructor(client: Client) { @@ -74,6 +76,7 @@ export class Admin { this.taxRegion = new TaxRegion(client) this.store = new Store(client) this.productTag = new ProductTag(client) + this.user = new User(client) this.return = new Return(client) } } diff --git a/packages/core/js-sdk/src/admin/user.ts b/packages/core/js-sdk/src/admin/user.ts new file mode 100644 index 0000000000..57c9b55376 --- /dev/null +++ b/packages/core/js-sdk/src/admin/user.ts @@ -0,0 +1,81 @@ +import { HttpTypes } from "@medusajs/types" +import { Client } from "../client" +import { ClientHeaders } from "../types" + +export class User { + private client: Client + constructor(client: Client) { + this.client = client + } + + async create( + body: HttpTypes.AdminCreateUser, + query?: HttpTypes.AdminUserParams, + headers?: ClientHeaders + ) { + return this.client.fetch(`/admin/users`, { + method: "POST", + headers, + body, + query, + }) + } + + async update( + id: string, + body: HttpTypes.AdminUpdateUser, + query?: HttpTypes.AdminUserParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/users/${id}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + async list( + queryParams?: HttpTypes.AdminUserListParams, + headers?: ClientHeaders + ) { + return this.client.fetch(`/admin/users`, { + headers, + query: queryParams, + }) + } + + async retrieve( + id: string, + query?: HttpTypes.AdminUserParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/users/${id}`, + { + query, + headers, + } + ) + } + + async delete(id: string, headers?: ClientHeaders) { + return this.client.fetch( + `/admin/users/${id}`, + { + method: "DELETE", + headers, + } + ) + } + + async me(query?: HttpTypes.AdminUserParams, headers?: ClientHeaders) { + return this.client.fetch(`/admin/users/me`, { + query, + headers, + }) + } +} diff --git a/packages/core/types/src/http/user/admin.ts b/packages/core/types/src/http/user/admin.ts deleted file mode 100644 index cf1c6a09ad..0000000000 --- a/packages/core/types/src/http/user/admin.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { BaseUserResponse } from "./common" - -export type AdminUserResponse = BaseUserResponse diff --git a/packages/core/types/src/http/user/common.ts b/packages/core/types/src/http/user/admin/entities.ts similarity index 61% rename from packages/core/types/src/http/user/common.ts rename to packages/core/types/src/http/user/admin/entities.ts index 6994de5784..1bdead260f 100644 --- a/packages/core/types/src/http/user/common.ts +++ b/packages/core/types/src/http/user/admin/entities.ts @@ -1,11 +1,11 @@ -export type BaseUserResponse = { +export interface AdminUser { id: string email: string first_name: string | null last_name: string | null avatar_url: string | null metadata: Record | null - created_at: Date - updated_at: Date - deleted_at: Date | null + created_at: string + updated_at: string + deleted_at: string | null } diff --git a/packages/core/types/src/http/user/admin/index.ts b/packages/core/types/src/http/user/admin/index.ts new file mode 100644 index 0000000000..1f82a2ead5 --- /dev/null +++ b/packages/core/types/src/http/user/admin/index.ts @@ -0,0 +1,4 @@ +export * from "./entities" +export * from "./payloads" +export * from "./queries" +export * from "./responses" diff --git a/packages/core/types/src/http/user/admin/payloads.ts b/packages/core/types/src/http/user/admin/payloads.ts new file mode 100644 index 0000000000..dc1d55c998 --- /dev/null +++ b/packages/core/types/src/http/user/admin/payloads.ts @@ -0,0 +1,12 @@ +export interface AdminCreateUser { + email: string + first_name?: string | null + last_name?: string | null + avatar_url?: string | null +} + +export interface AdminUpdateUser { + first_name?: string | null + last_name?: string | null + avatar_url?: string | null +} diff --git a/packages/core/types/src/http/user/admin/queries.ts b/packages/core/types/src/http/user/admin/queries.ts new file mode 100644 index 0000000000..c3de9d97e5 --- /dev/null +++ b/packages/core/types/src/http/user/admin/queries.ts @@ -0,0 +1,15 @@ +import { OperatorMap } from "../../../dal" +import { FindParams, SelectParams } from "../../common" + +export interface AdminUserListParams extends FindParams { + q?: string + id?: string | string[] + email?: string | null + first_name?: string | null + last_name?: string | null + created_at?: OperatorMap + updated_at?: OperatorMap + deleted_at?: OperatorMap +} + +export interface AdminUserParams extends SelectParams {} diff --git a/packages/core/types/src/http/user/admin/responses.ts b/packages/core/types/src/http/user/admin/responses.ts new file mode 100644 index 0000000000..9433397ec8 --- /dev/null +++ b/packages/core/types/src/http/user/admin/responses.ts @@ -0,0 +1,11 @@ +import { DeleteResponse, PaginatedResponse } from "../../common" +import { AdminUser } from "./entities" + +export interface AdminUserResponse { + user: AdminUser +} + +export interface AdminUserListResponse + extends PaginatedResponse<{ users: AdminUser[] }> {} + +export interface AdminUserDeleteResponse extends DeleteResponse<"user"> {} diff --git a/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx b/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx index 68410245b5..e0388f76d4 100644 --- a/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/design-system/ui/src/components/dropdown-menu/dropdown-menu.tsx @@ -50,13 +50,13 @@ const SubMenuTrigger = React.forwardRef< "focus-visible:bg-ui-bg-component-hover focus:bg-ui-bg-component-hover", "active:bg-ui-bg-component-pressed", "data-[disabled]:text-ui-fg-disabled data-[disabled]:pointer-events-none", - "data-[state=open]:bg-ui-bg-base-hover", + "data-[state=open]:!bg-ui-bg-component-hover", className )} {...props} > {children} - + )) SubMenuTrigger.displayName = "DropdownMenu.SubMenuTrigger"