From a2c149e7e588a9ebf080b4b43472bdb5126ed981 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:43:16 +0100 Subject: [PATCH] feat(dashboard,ui): Streamline spacing and sizing (#6061) --- .changeset/forty-shrimps-watch.md | 9 + .eslintrc.js | 4 +- packages/admin-next/dashboard/package.json | 6 +- .../public/locales/en/translation.json | 109 +- .../dashboard/scripts/generate-countries.js | 44 + packages/admin-next/dashboard/src/app.tsx | 14 +- .../require-auth/require-auth.tsx | 38 +- .../components/common/combobox/combobox.tsx | 37 + .../common/country-select/country-select.tsx | 54 + .../components/common/country-select/index.ts | 1 + .../debounced-search/debounced-search.tsx | 1 + .../empty-table-content.tsx | 59 + .../common/empty-table-content/index.ts | 1 + .../common/json-view-section/index.ts | 1 + .../json-view-section.tsx} | 18 +- .../src/components/common/json-view/index.ts | 1 - .../product-table-cells.tsx | 7 +- .../src/components/common/skeleton/index.ts | 1 + .../components/common/skeleton/skeleton.tsx | 16 + .../error/error-boundary/error-boundary.tsx | 6 +- .../filtering/filter-group/filter-group.tsx | 54 + .../filtering/filter-group/index.ts | 1 + .../components/filtering/order-by/index.ts | 1 + .../filtering/order-by/order-by.tsx | 148 ++ .../src/components/filtering/query/index.ts | 1 + .../src/components/filtering/query/query.tsx | 57 + .../layout/app-layout/app-layout.tsx | 26 - .../layout/app-layout/breadcrumbs.tsx | 52 - .../components/layout/app-layout/gutter.tsx | 9 - .../src/components/layout/app-layout/index.ts | 1 - .../components/layout/app-layout/loader.tsx | 26 - .../components/layout/app-layout/main-nav.tsx | 380 ---- .../components/layout/app-layout/nav-item.tsx | 148 -- .../layout/app-layout/search-toggle.tsx | 17 - .../layout/app-layout/settings-nav.tsx | 119 -- .../components/layout/app-layout/spacer.tsx | 7 - .../components/layout/app-layout/topbar.tsx | 19 - .../components/layout/main-layout/index.ts | 1 + .../layout/main-layout/main-layout.tsx | 197 ++ .../src/components/layout/nav-item/index.ts | 1 + .../components/layout/nav-item/nav-item.tsx | 156 ++ .../components/layout/notifications/index.ts | 1 + .../notifications.tsx | 29 +- .../layout/settings-layout/index.ts | 1 + .../settings-layout/settings-layout.tsx | 99 + .../src/components/layout/shell/index.ts | 1 + .../src/components/layout/shell/shell.tsx | 378 ++++ .../localized-date-picker/index.ts | 1 + .../localized-date-picker.tsx | 36 + .../localized-table-pagination/index.ts | 1 + .../localized-table-pagination.tsx | 26 + .../src/components/search/search.tsx | 44 +- .../dashboard/src/hooks/use-form-prompt.tsx | 20 + .../dashboard/src/hooks/use-query-params.tsx | 16 + .../src/hooks/use-route-modal-state.tsx | 63 + .../admin-next/dashboard/src/i18n/config.ts | 2 + .../admin-next/dashboard/src/i18n/types.ts | 2 + .../admin-next/dashboard/src/lib/countries.ts | 1755 +++++++++++++++++ .../admin-next/dashboard/src/lib/debounce.ts | 20 + .../dashboard/src/lib/is-axios-error.ts | 5 + .../admin-next/dashboard/src/lib/medusa.ts | 7 +- .../providers/auth-provider/auth-context.tsx | 10 - .../providers/auth-provider/auth-provider.tsx | 24 - .../src/providers/auth-provider/index.ts | 2 - .../src/providers/auth-provider/use-auth.tsx | 10 - .../router-provider/router-provider.tsx | 515 +++-- .../src/providers/sidebar-provider/index.ts | 2 + .../sidebar-provider/sidebar-context.tsx | 9 + .../sidebar-provider/sidebar-provider.tsx | 21 + .../sidebar-provider/use-sidebar.tsx | 12 + .../api-key-management-create.tsx | 15 + .../create-publishable-api-key-form.tsx | 97 + .../create-publishable-api-key-form/index.ts | 1 + .../api-key-management-create/index.ts | 1 + .../api-key-management-detail.tsx | 3 + .../api-key-management-detail/index.ts | 1 + .../api-key-management-edit.tsx | 12 + .../api-key-management-edit/index.ts | 1 + .../api-key-management-list.tsx} | 139 +- .../api-key-management-list-table.tsx | 265 +++ .../api-key-management-list-table/index.ts | 1 + .../api-key-management-list/index.ts | 1 + .../src/routes/api-key-management/index.ts | 1 - .../edit-currencies-details-drawer.tsx | 159 -- .../edit-currencies-details-drawer/index.ts | 1 - .../currencies-details/currencies-details.tsx | 488 ----- .../views/currencies-details/index.ts | 1 - .../src/routes/locations/list/index.ts | 1 - .../src/routes/locations/list/list.tsx | 11 - .../location-add-sales-channels/index.ts | 1 + .../location-add-sales-channels.tsx | 15 + .../create-location-form.tsx | 234 +++ .../components/create-location-form/index.ts | 1 + .../routes/locations/location-create/index.ts | 1 + .../location-create/location-create.tsx | 15 + .../location-general-section/index.ts | 1 + .../location-general-section.tsx | 101 + .../location-sales-channel-section/index.ts | 1 + .../location-sales-channel-section.tsx} | 70 +- .../routes/locations/location-detail/index.ts | 1 + .../location-detail/location-detail.tsx | 38 + .../edit-location-form/edit-location-form.tsx | 218 ++ .../routes/locations/location-edit/index.ts | 1 + .../locations/location-edit/location-edit.tsx | 47 + .../components/locations-list-table/index.ts | 1 + .../locations-list-table.tsx | 278 +++ .../routes/locations/location-list/index.ts | 1 + .../locations/location-list/location-list.tsx | 11 + .../dashboard/src/routes/login/login.tsx | 73 +- .../product-attribute-section/index.ts | 0 .../product-attribute-section.tsx | 0 .../product-general-section/index.ts | 0 .../product-general-section.tsx | 0 .../components/product-media-section/index.ts | 0 .../product-media-section.tsx | 0 .../product-option-section/index.ts | 0 .../product-option-section.tsx | 0 .../product-sales-channel-section/index.ts | 0 .../product-sales-channel-section.tsx | 0 .../product-thumbnail-section/index.ts | 0 .../product-thumbnail-section.tsx | 0 .../product-variant-section/index.ts | 0 .../product-variant-section.tsx | 0 .../routes/products/product-detail/index.ts | 2 + .../loader.ts | 2 +- .../product-detail.tsx} | 66 +- .../components/product-list-table/index.ts | 1 + .../product-list-table/product-list-table.tsx | 284 +++ .../src/routes/products/product-list/index.ts | 2 + .../{views => }/product-list/loader.ts | 20 +- .../products/product-list/product-list.tsx | 18 + .../products/views/product-details/index.ts | 2 - .../products/views/product-list/index.ts | 2 - .../products/views/product-list/list.tsx | 277 --- .../edit-profile-details-drawer.tsx | 236 --- .../profile-general-section/index.ts | 1 + .../profile-general-section.tsx | 63 + .../routes/profile/profile-detail/index.ts | 1 + .../profile/profile-detail/profile-detail.tsx | 26 + .../edit-profile-form/edit-profile-form.tsx | 208 ++ .../src/routes/profile/profile-edit/index.ts | 1 + .../profile/profile-edit/profile-edit.tsx | 52 + .../profile/views/profile-details/index.ts | 1 - .../profile/views/profile-details/profile.tsx | 70 - .../edit-region-details-drawer.tsx | 12 - .../src/routes/regions/region-create/index.ts | 1 + .../regions/region-create/region-create.tsx | 201 ++ .../region-general-section/index.ts | 1 + .../region-general-section.tsx | 230 +++ .../region-shipping-option-section.tsx | 156 ++ .../src/routes/regions/region-detail/index.ts | 2 + .../routes/regions/region-detail/loader.ts | 23 + .../regions/region-detail/region-detail.tsx | 42 + .../edit-region-form/edit-region-form.tsx | 105 + .../components/edit-region-form/index.ts | 1 + .../src/routes/regions/region-edit/index.ts | 1 + .../regions/region-edit/region-edit.tsx | 49 + .../components/region-list-table/index.ts | 1 + .../region-list-table/region-list-table.tsx | 302 +++ .../regions/{views => }/region-list/index.ts | 0 .../regions/region-list/region-list.tsx | 13 + .../regions/views/region-details/index.ts | 1 - .../views/region-details/region-details.tsx | 142 -- .../regions/views/region-list/region-list.tsx | 26 - .../sales-channel-details-section/index.ts | 1 - .../sales-channel-details-section.tsx | 200 -- .../sales-channel-products-section/index.ts | 1 - .../sales-channel-products-section.tsx | 621 ------ .../add-products-to-sales-channel-form.tsx | 362 ++++ .../components/index.ts | 1 + .../sales-channel-add-products/index.ts | 1 + .../sales-channel-add-products.tsx | 36 + .../create-sales-channel-form.tsx | 160 ++ .../create-sales-channel-form/index.ts | 1 + .../sales-channel-create/index.ts | 1 + .../sales-channel-create.tsx | 15 + .../sales-channel-general-section/index.ts | 1 + .../sales-channel-general-section.tsx | 88 + .../sales-channel-product-section/index.ts | 1 + .../sales-channel-product-section.tsx | 348 ++++ .../sales-channel-detail/index.ts | 1 + .../sales-channel-detail.tsx | 26 + .../edit-sales-channel-form.tsx | 140 ++ .../edit-sales-channel-form/index.ts | 1 + .../sales-channel-edit/index.ts | 1 + .../sales-channel-edit/sales-channel-edit.tsx | 41 + .../sales-channel-list-table/index.ts | 1 + .../sales-channel-list-table.tsx | 232 +++ .../{views => }/sales-channel-list/index.ts | 0 .../sales-channel-list/sales-channel-list.tsx | 11 + .../views/sales-channel-details/index.ts | 1 - .../sales-channel-details.tsx | 23 - .../src/routes/settings/settings.tsx | 17 +- .../edit-store-details-drawer.tsx | 174 -- .../edit-store-details-drawer/index.ts | 1 - .../add-currencies-form.tsx | 325 +++ .../store/store-add-currencies/index.ts | 1 + .../store-add-currencies.tsx | 42 + .../store-currencies-section.tsx/index.ts | 1 + .../store-currency-section.tsx | 300 +++ .../components/store-general-section/index.ts | 1 + .../store-general-section.tsx | 99 + .../src/routes/store/store-detail/index.ts | 2 + .../store-details => store-detail}/loader.ts | 30 +- .../store/store-detail/store-detail.tsx | 39 + .../edit-store-form/edit-store-form.tsx | 142 ++ .../src/routes/store/store-edit/index.ts | 1 + .../routes/store/store-edit/store-edit.tsx | 49 + .../routes/store/views/store-details/index.ts | 2 - .../views/store-details/store-details.tsx | 101 - .../dashboard/src/routes/users/index.ts | 1 - .../components/user-general-section/index.ts | 1 + .../user-general-section.tsx | 45 + .../src/routes/users/user-detail/index.ts | 1 + .../routes/users/user-detail/user-detail.tsx | 29 + .../src/routes/users/user-edit/index.ts | 1 + .../src/routes/users/user-edit/user-edit.tsx | 12 + .../src/routes/users/user-invite/index.ts | 1 + .../routes/users/user-invite/user-invite.tsx | 12 + .../components/user-list-table/index.ts | 1 + .../user-list-table/user-list-table.tsx | 156 ++ .../src/routes/users/user-list/index.ts | 1 + .../src/routes/users/user-list/user-list.tsx | 9 + .../dashboard/src/routes/users/users.tsx | 155 -- .../__tests__/lock-closed-solid-mini.spec.tsx | 17 + .../__tests__/triangle-left-mini.spec.tsx | 17 + .../__tests__/triangle-up-mini.spec.tsx | 17 + .../__tests__/triangles-mini.spec.tsx | 17 + .../icons/src/components/index.ts | 4 + .../src/components/lock-closed-solid-mini.tsx | 27 + .../src/components/lock-closed-solid.tsx | 4 +- .../icons/src/components/lock-open-solid.tsx | 2 +- .../src/components/triangle-left-mini.tsx | 23 + .../icons/src/components/triangle-up-mini.tsx | 23 + .../icons/src/components/triangles-mini.tsx | 23 + .../ui-preset/src/theme/extension/theme.ts | 48 +- .../ui-preset/src/theme/tokens/colors.ts | 44 +- .../ui-preset/src/theme/tokens/components.ts | 16 +- .../ui-preset/src/theme/tokens/typography.ts | 12 +- .../ui/src/components/avatar/avatar.tsx | 84 +- .../ui/src/components/button/button.tsx | 9 +- .../ui/src/components/calendar/calendar.tsx | 27 +- .../ui/src/components/checkbox/checkbox.tsx | 2 +- .../components/command-bar/command-bar.tsx | 16 +- .../currency-input/currency-input.tsx | 33 +- .../components/date-picker/date-picker.tsx | 152 +- .../ui/src/components/drawer/drawer.tsx | 22 +- .../dropdown-menu/dropdown-menu.tsx | 17 +- .../components/focus-modal/focus-modal.tsx | 21 +- .../ui/src/components/hint/hint.tsx | 30 +- .../components/icon-button/icon-button.tsx | 5 +- .../ui/src/components/input/input.tsx | 151 +- .../progress-accordion/progress-accordion.tsx | 36 +- .../progress-tabs/progress-tabs.tsx | 54 +- .../ui/src/components/prompt/prompt.tsx | 60 +- .../components/radio-group/radio-group.tsx | 4 +- .../ui/src/components/select/select.tsx | 18 +- .../components/status-badge/status-badge.tsx | 61 +- .../ui/src/components/switch/switch.tsx | 39 +- .../ui/src/components/table/table.tsx | 41 +- .../ui/src/components/tabs/tabs.tsx | 4 +- .../ui/src/components/textarea/textarea.tsx | 2 +- .../src/components/time-input/time-input.tsx | 23 +- .../ui/src/components/tooltip/tooltip.tsx | 4 +- .../ui/src/hooks/use-prompt/render-prompt.tsx | 27 +- yarn.lock | 168 +- 266 files changed, 10738 insertions(+), 4646 deletions(-) create mode 100644 .changeset/forty-shrimps-watch.md create mode 100644 packages/admin-next/dashboard/scripts/generate-countries.js create mode 100644 packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/country-select/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx create mode 100644 packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/json-view-section/index.ts rename packages/admin-next/dashboard/src/components/common/{json-view/json-view.tsx => json-view-section/json-view-section.tsx} (88%) delete mode 100644 packages/admin-next/dashboard/src/components/common/json-view/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/skeleton/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx create mode 100644 packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx create mode 100644 packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts create mode 100644 packages/admin-next/dashboard/src/components/filtering/order-by/index.ts create mode 100644 packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx create mode 100644 packages/admin-next/dashboard/src/components/filtering/query/index.ts create mode 100644 packages/admin-next/dashboard/src/components/filtering/query/query.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/index.ts delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx delete mode 100644 packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx create mode 100644 packages/admin-next/dashboard/src/components/layout/main-layout/index.ts create mode 100644 packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx create mode 100644 packages/admin-next/dashboard/src/components/layout/nav-item/index.ts create mode 100644 packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx create mode 100644 packages/admin-next/dashboard/src/components/layout/notifications/index.ts rename packages/admin-next/dashboard/src/components/layout/{app-layout => notifications}/notifications.tsx (53%) create mode 100644 packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts create mode 100644 packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx create mode 100644 packages/admin-next/dashboard/src/components/layout/shell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/layout/shell/shell.tsx create mode 100644 packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts create mode 100644 packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx create mode 100644 packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts create mode 100644 packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/use-query-params.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx create mode 100644 packages/admin-next/dashboard/src/lib/countries.ts create mode 100644 packages/admin-next/dashboard/src/lib/debounce.ts create mode 100644 packages/admin-next/dashboard/src/lib/is-axios-error.ts delete mode 100644 packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx delete mode 100644 packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx delete mode 100644 packages/admin-next/dashboard/src/providers/auth-provider/index.ts delete mode 100644 packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx create mode 100644 packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts create mode 100644 packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx create mode 100644 packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx create mode 100644 packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts rename packages/admin-next/dashboard/src/routes/api-key-management/{api-key-management.tsx => api-key-management-list/api-key-management-list.tsx} (62%) create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/api-key-management/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/locations/list/list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts rename packages/admin-next/dashboard/src/routes/{sales-channels/views/sales-channel-list/sales-channel-list.tsx => locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx} (80%) create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-attribute-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-attribute-section/product-attribute-section.tsx (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-general-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-general-section/product-general-section.tsx (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-media-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-media-section/product-media-section.tsx (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-option-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-option-section/product-option-section.tsx (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-sales-channel-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-sales-channel-section/product-sales-channel-section.tsx (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-thumbnail-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-thumbnail-section/product-thumbnail-section.tsx (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-variant-section/index.ts (100%) rename packages/admin-next/dashboard/src/routes/products/{ => product-detail}/components/product-variant-section/product-variant-section.tsx (100%) create mode 100644 packages/admin-next/dashboard/src/routes/products/product-detail/index.ts rename packages/admin-next/dashboard/src/routes/products/{views/product-details => product-detail}/loader.ts (91%) rename packages/admin-next/dashboard/src/routes/products/{views/product-details/details.tsx => product-detail/product-detail.tsx} (66%) create mode 100644 packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/products/product-list/index.ts rename packages/admin-next/dashboard/src/routes/products/{views => }/product-list/loader.ts (55%) create mode 100644 packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx rename packages/admin-next/dashboard/src/routes/regions/{views => }/region-list/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-products-section/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-products-section/sales-channel-products-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-add-products/components/add-products-to-sales-channel-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-add-products/components/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-add-products/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-add-products/sales-channel-add-products.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-create/components/create-sales-channel-form/create-sales-channel-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-create/components/create-sales-channel-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-create/sales-channel-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-detail/components/sales-channel-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-detail/components/sales-channel-general-section/sales-channel-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-detail/components/sales-channel-product-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-detail/components/sales-channel-product-section/sales-channel-product-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-detail/sales-channel-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-edit/components/edit-sales-channel-form/edit-sales-channel-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-edit/components/edit-sales-channel-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-edit/sales-channel-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-list/components/sales-channel-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-list/components/sales-channel-list-table/sales-channel-list-table.tsx rename packages/admin-next/dashboard/src/routes/sales-channels/{views => }/sales-channel-list/index.ts (100%) create mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/sales-channel-list/sales-channel-list.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-details/sales-channel-details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/store/components/edit-store-details-drawer/edit-store-details-drawer.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/store/components/edit-store-details-drawer/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/store/store-add-currencies/components/add-currencies-form/add-currencies-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/store/store-add-currencies/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/store/store-add-currencies/store-add-currencies.tsx create mode 100644 packages/admin-next/dashboard/src/routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/store/store-detail/components/store-currency-section/store-currencies-section.tsx/store-currency-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/store/store-detail/components/store-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/store/store-detail/components/store-general-section/store-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/store/store-detail/index.ts rename packages/admin-next/dashboard/src/routes/store/{views/store-details => store-detail}/loader.ts (54%) create mode 100644 packages/admin-next/dashboard/src/routes/store/store-detail/store-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/store/store-edit/components/edit-store-form/edit-store-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/store/store-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/store/store-edit/store-edit.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/store/views/store-details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/store/views/store-details/store-details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/users/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-detail/components/user-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-detail/components/user-general-section/user-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/users/user-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-detail/user-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/users/user-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-edit/user-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/users/user-invite/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-invite/user-invite.tsx create mode 100644 packages/admin-next/dashboard/src/routes/users/user-list/components/user-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-list/components/user-list-table/user-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/users/user-list/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-list/user-list.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/users/users.tsx create mode 100644 packages/design-system/icons/src/components/__tests__/lock-closed-solid-mini.spec.tsx create mode 100644 packages/design-system/icons/src/components/__tests__/triangle-left-mini.spec.tsx create mode 100644 packages/design-system/icons/src/components/__tests__/triangle-up-mini.spec.tsx create mode 100644 packages/design-system/icons/src/components/__tests__/triangles-mini.spec.tsx create mode 100644 packages/design-system/icons/src/components/lock-closed-solid-mini.tsx create mode 100644 packages/design-system/icons/src/components/triangle-left-mini.tsx create mode 100644 packages/design-system/icons/src/components/triangle-up-mini.tsx create mode 100644 packages/design-system/icons/src/components/triangles-mini.tsx diff --git a/.changeset/forty-shrimps-watch.md b/.changeset/forty-shrimps-watch.md new file mode 100644 index 0000000000..d60f718b33 --- /dev/null +++ b/.changeset/forty-shrimps-watch.md @@ -0,0 +1,9 @@ +--- +"@medusajs/ui": minor +"@medusajs/ui-preset": minor +"@medusajs/icons": minor +--- + +feat(ui): Updates spacing and sizing of components. Introduces new `size` variants for some components, such as `Button`, `IconButton`, and `Avatar`. Change most `:focus` styles to `:focus-visible` styles, to prevenent focus styles from being visible when not needed, such as on button clicks. +feat(ui-preset): Publishes latest updates to our design system styles, as well as adding new colors. Noticable changes include changing `ui-code-text-*` styles to `ui-code-fg-*` for better consistency. +feat(icons): Updates the `LockClosedSolid` and `LockOpenSolid` icons, and introduces four new icons: `LockClosedSolidMini`, `TriangleLeftMini`, `TriangleRightMini`, and `TriangleMini`. diff --git a/.eslintrc.js b/.eslintrc.js index be653df480..22a44eb628 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -227,7 +227,7 @@ module.exports = { }, }, { - files: ["packages/admin-next/dashboard/**/*"], + files: ["packages/admin-next/dashboard/src/**/*.{ts,tsx}"], env: { browser: true, es2020: true, node: true }, extends: [ "eslint:recommended", @@ -236,7 +236,7 @@ module.exports = { ], parser: "@typescript-eslint/parser", parserOptions: { - project: "./packages/admin-next/dashboard/tsconfig.json", + project: "tsconfig.json", }, plugins: ["react-refresh"], rules: { diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json index 39e57d182a..68f7d41826 100644 --- a/packages/admin-next/dashboard/package.json +++ b/packages/admin-next/dashboard/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "scripts": { + "generate:countries": "node ./scripts/generate-countries.js && prettier --write ./src/lib/countries.ts", "dev": "vite", "build": "vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" @@ -23,6 +24,7 @@ "@tanstack/react-table": "8.10.7", "@uiw/react-json-view": "2.0.0-alpha.10", "cmdk": "^0.2.0", + "date-fns": "^3.2.0", "i18next": "23.7.11", "i18next-browser-languagedetector": "7.2.0", "i18next-http-backend": "2.4.2", @@ -36,15 +38,15 @@ }, "devDependencies": { "@medusajs/medusa": "workspace:^", + "@medusajs/types": "workspace:^", "@medusajs/ui-preset": "workspace:^", "@medusajs/vite-plugin-extension": "workspace:^", "@types/react": "18.2.43", "@types/react-dom": "18.2.17", - "@typescript-eslint/eslint-plugin": "6.14.0", - "@typescript-eslint/parser": "6.14.0", "@vitejs/plugin-react": "4.2.1", "autoprefixer": "10.4.16", "postcss": "8.4.32", + "prettier": "^3.1.1", "tailwindcss": "3.3.6", "typescript": "5.2.2", "vite": "5.0.10" diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json index 6a3b624d8b..85ab3191e9 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en/translation.json @@ -1,12 +1,25 @@ { "$schema": "../$schema.json", "general": { + "ascending": "Ascending", + "descending": "Descending", "cancel": "Cancel", "save": "Save", "create": "Create", "delete": "Delete", "edit": "Edit", + "add": "Add", + "continue": "Continue", + "start": "Start", + "end": "End", + "apply": "Apply", + "range": "Range", "search": "Search", + "of": "of", + "results": "results", + "pages": "pages", + "next": "Next", + "prev": "Prev", "extensions": "Extensions", "settings": "Settings", "general": "General", @@ -14,17 +27,25 @@ "enabled": "Enabled", "disabled": "Disabled", "remove": "Remove", + "admin": "Admin", + "store": "Store", "countSelected": "{{count}} selected", "plusCountMore": "+ {{count}} more", "areYouSure": "Are you sure?", - "noRecordsFound": "No records found" + "noRecordsFound": "No records found", + "typeToConfirm": "Please type {val} to confirm:", + "noResultsMessage": "Try changing the filters or search 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." }, "products": { "domain": "Products", "variants": "Variants", "availableInSalesChannels": "Available in <0>{{x}} of <1>{{y}} sales channels", - "inStockVariants_one": "{{inventory}} in stock for {{count}} variant", - "inStockVariants_other": "{{inventory}} in stock for {{count}} variants", + "variantCount_one": "{{count}} variant", + "variantCount_other": "{{count}} variants", "productStatus": { "draft": "Draft", "published": "Published", @@ -65,9 +86,11 @@ "profile": { "domain": "Profile", "manageYourProfileDetails": "Manage your profile details", - "editProfileDetails": "Edit Profile Details", + "editProfile": "Edit profile", "languageHint": "The language you want to use in the admin dashboard. This will not change the language of your store.", - "userInsightsHint": "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." + "userInsightsHint": "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.", + "language": "Language", + "usageInsights": "Usage insights" }, "users": { "domain": "Users", @@ -81,38 +104,54 @@ "store": { "domain": "Store", "manageYourStoresDetails": "Manage your store's details", - "editStoreDetails": "Edit Store Details", - "storeName": "Store name", + "editStore": "Edit store", + "defaultCurrency": "Default currency", "swapLinkTemplate": "Swap link template", "paymentLinkTemplate": "Payment link template", - "inviteLinkTemplate": "Invite 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." }, "regions": { - "domain": "Regions" + "domain": "Regions", + "createRegion": "Create Region", + "editRegion": "Edit Region", + "deleteRegionWarning": "You are about to delete the region {{name}}. This action cannot be undone.", + "taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.", + "providersHint": "The providers that are available in the region." + }, + "locations": { + "domain": "Locations", + "createLocation": "Create location", + "editLocation": "Edit location", + "addSalesChannels": "Add sales channels", + "detailsHint": "Specify the details of the location.", + "noLocationsFound": "No locations found", + "deleteLocationWarning": "You are about to delete the location {{name}}. 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", - "isEnabledHint": "Specify if the sales channel is enabled or disabled.", - "productAlreadyAdded": "The product has already been added to the sales channel." - }, - "currencies": { - "domain": "Currencies", - "manageTheCurrencies": "Manage the currencies you want to use in your store", - "editCurrencyDetails": "Edit Currency Details", - "defaultCurrency": "Default Currency", - "defaultCurrencyHint": "The default currency of your store.", - "removeCurrenciesWarning_one": "You are about to remove {{count}} currency from your store. Ensure that you have removed all prices using the currency before proceeding.", - "removeCurrenciesWarning_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." + "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." }, "apiKeyManagement": { "domain": "API Key Management", - "createAPublishableApiKey": "Create a publishable API key", - "createKey": "Create Key" + "createKey": "Create key", + "createPublishableApiKey": "Create Publishable API Key", + "revoke": "Revoke", + "publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.", + "deleteKeyWarning": "You are about to delete the API key {{title}}. This action cannot be undone.", + "revokeKeyWarning": "You are about to revoke the API key {{title}}." }, "fields": { "name": "Name", @@ -133,8 +172,28 @@ "sales_channels": "Sales Channels", "status": "Status", "code": "Code", + "countries": "Countries", + "paymentProviders": "Payment Providers", + "fulfillmentProviders": "Fulfillment Providers", + "providers": "Providers", "availability": "Availability", "inventory": "Inventory", - "optional": "Optional" + "optional": "Optional", + "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", + "variants": "Variants" } } diff --git a/packages/admin-next/dashboard/scripts/generate-countries.js b/packages/admin-next/dashboard/scripts/generate-countries.js new file mode 100644 index 0000000000..491310e677 --- /dev/null +++ b/packages/admin-next/dashboard/scripts/generate-countries.js @@ -0,0 +1,44 @@ +async function generateCountries() { + const { countries } = await import("@medusajs/medusa/dist/utils/countries.js") + const fs = await import("fs") + const path = await import("path") + + const arr = countries.map((c) => { + const iso_2 = c.alpha2.toLowerCase() + const iso_3 = c.alpha3.toLowerCase() + const num_code = parseInt(c.numeric, 10) + const name = c.name.toUpperCase() + const display_name = c.name + + return { + iso_2, + iso_3, + num_code, + name, + display_name, + } + }) + + const json = JSON.stringify(arr, null, 2) + + const dest = path.join(__dirname, "../src/lib/countries.ts") + const destDir = path.dirname(dest) + + const fileContent = `/** This file is auto-generated. Do not modify it manually. */\nimport type { Country } from "@medusajs/medusa"\n\nexport const countries: Omit[] = ${json}` + + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }) + } + + fs.writeFileSync(dest, fileContent) +} + +;(async () => { + console.log("Generating countries") + try { + await generateCountries() + console.log("Countries generated") + } catch (e) { + console.error(e) + } +})() diff --git a/packages/admin-next/dashboard/src/app.tsx b/packages/admin-next/dashboard/src/app.tsx index 9170f0ff08..602ac18fba 100644 --- a/packages/admin-next/dashboard/src/app.tsx +++ b/packages/admin-next/dashboard/src/app.tsx @@ -1,28 +1,22 @@ import { Toaster } from "@medusajs/ui" import { MedusaProvider } from "medusa-react" -import { AuthProvider } from "./providers/auth-provider" import { RouterProvider } from "./providers/router-provider" import { ThemeProvider } from "./providers/theme-provider" -import { queryClient } from "./lib/medusa" - -const BASE_URL = - import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000" +import { MEDUSA_BACKEND_URL, queryClient } from "./lib/medusa" function App() { return ( - - - - + + ) diff --git a/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx b/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx index d64ad20b87..e3a29ae7ea 100644 --- a/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx +++ b/packages/admin-next/dashboard/src/components/authentication/require-auth/require-auth.tsx @@ -1,25 +1,31 @@ -import { Spinner } from "@medusajs/icons"; -import { PropsWithChildren } from "react"; -import { Navigate, useLocation } from "react-router-dom"; +import { Spinner } from "@medusajs/icons" +import { Navigate, Outlet, useLocation } from "react-router-dom" -import { useAuth } from "../../../providers/auth-provider"; +import { useAdminGetSession } from "medusa-react" +import { SearchProvider } from "../../../providers/search-provider" +import { SidebarProvider } from "../../../providers/sidebar-provider" -export const RequireAuth = ({ children }: PropsWithChildren) => { - const auth = useAuth(); - const location = useLocation(); +export const ProtectedRoute = () => { + const { user, isLoading } = useAdminGetSession() + const location = useLocation() - if (auth.isLoading) { + if (isLoading) { return ( -
- +
+
- ); + ) } - if (!auth.user) { - console.log("redirecting"); - return ; + if (!user) { + return } - return children; -}; + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx b/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx new file mode 100644 index 0000000000..b499a8e943 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/combobox/combobox.tsx @@ -0,0 +1,37 @@ +import { clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" + +type ComboboxOption = { + value: string + label: string +} + +type ComboboxProps = { + size?: "base" | "small" + options: ComboboxOption[] + value: string +} + +export const Combobox = ({ size = "base" }: ComboboxProps) => { + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx b/packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx new file mode 100644 index 0000000000..5a2dd75ef9 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/country-select/country-select.tsx @@ -0,0 +1,54 @@ +import { forwardRef } from "react" + +import { TrianglesMini } from "@medusajs/icons" +import { clx } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { countries } from "../../../lib/countries" + +export const CountrySelect = forwardRef< + HTMLSelectElement, + React.ComponentPropsWithoutRef<"select"> & { placeholder?: string } +>(({ className, disabled, placeholder, ...props }, ref) => { + const { t } = useTranslation() + + return ( +
+ + +
+ ) +}) +CountrySelect.displayName = "CountrySelect" diff --git a/packages/admin-next/dashboard/src/components/common/country-select/index.ts b/packages/admin-next/dashboard/src/components/common/country-select/index.ts new file mode 100644 index 0000000000..4e19e8a7eb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/country-select/index.ts @@ -0,0 +1 @@ +export * from "./country-select" diff --git a/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx b/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx index 49fe276389..d763a2ec3f 100644 --- a/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx +++ b/packages/admin-next/dashboard/src/components/common/debounced-search/debounced-search.tsx @@ -15,6 +15,7 @@ export const DebouncedSearch = ({ value: initialValue, onChange, debounce = 500, + size = "small", placeholder, ...props }: DebouncedSearchProps) => { diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx new file mode 100644 index 0000000000..062c47913c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx @@ -0,0 +1,59 @@ +import { ExclamationCircle, MagnifyingGlass } from "@medusajs/icons" +import { Button, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" + +type NoResultsProps = { + title: string + message?: string +} + +export const NoResults = ({ title, message }: NoResultsProps) => { + const { t } = useTranslation() + + return ( +
+
+ + + {title} + + + {message ?? t("general.noResultsMessage")} + +
+
+ ) +} + +type NoRecordsProps = { + title?: string + message?: string + action?: { + to: string + label: string + } +} + +export const NoRecords = ({ title, message, action }: NoRecordsProps) => { + const { t } = useTranslation() + + return ( +
+
+ + + {title ?? t("general.noRecordsTitle")} + + + {message ?? t("general.noRecordsMessage")} + +
+ {action && ( + + + + )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts b/packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts new file mode 100644 index 0000000000..405e5ca379 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/index.ts @@ -0,0 +1 @@ +export * from "./empty-table-content" diff --git a/packages/admin-next/dashboard/src/components/common/json-view-section/index.ts b/packages/admin-next/dashboard/src/components/common/json-view-section/index.ts new file mode 100644 index 0000000000..be52949afd --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/json-view-section/index.ts @@ -0,0 +1 @@ +export * from "./json-view-section" diff --git a/packages/admin-next/dashboard/src/components/common/json-view/json-view.tsx b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx similarity index 88% rename from packages/admin-next/dashboard/src/components/common/json-view/json-view.tsx rename to packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx index ee8055b46d..7727b9f663 100644 --- a/packages/admin-next/dashboard/src/components/common/json-view/json-view.tsx +++ b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx @@ -21,23 +21,27 @@ type JsonViewProps = { } // TODO: Fix the positioning of the copy btn -export const JsonView = ({ data, root }: JsonViewProps) => { +export const JsonViewSection = ({ data, root }: JsonViewProps) => { const numberOfKeys = Object.keys(data).length return ( - +
JSON {numberOfKeys} keys
- + -
+
JSON {numberOfKeys} keys @@ -45,7 +49,11 @@ export const JsonView = ({ data, root }: JsonViewProps) => {
esc - + diff --git a/packages/admin-next/dashboard/src/components/common/json-view/index.ts b/packages/admin-next/dashboard/src/components/common/json-view/index.ts deleted file mode 100644 index eed209d97d..0000000000 --- a/packages/admin-next/dashboard/src/components/common/json-view/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./json-view"; diff --git a/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx b/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx index 63ca516c8e..cd290cf662 100644 --- a/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx +++ b/packages/admin-next/dashboard/src/components/common/product-table-cells/product-table-cells.tsx @@ -8,7 +8,7 @@ import { StatusBadge, Text } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { Thumbnail } from "../thumbnail" -export const ProductInventoryCell = ({ +export const ProductVariantCell = ({ variants, }: { variants: ProductVariant[] | null @@ -23,13 +23,10 @@ export const ProductInventoryCell = ({ ) } - const inventory = variants.reduce((acc, v) => acc + v.inventory_quantity, 0) - return ( - {t("products.inStockVariants", { + {t("products.variantCount", { count: variants.length, - inventory: inventory, })} ) diff --git a/packages/admin-next/dashboard/src/components/common/skeleton/index.ts b/packages/admin-next/dashboard/src/components/common/skeleton/index.ts new file mode 100644 index 0000000000..d889ad708e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/skeleton/index.ts @@ -0,0 +1 @@ +export * from "./skeleton" diff --git a/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx b/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx new file mode 100644 index 0000000000..0ad49b6f96 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/skeleton/skeleton.tsx @@ -0,0 +1,16 @@ +import { clx } from "@medusajs/ui" + +type SkeletonProps = { + className?: string +} + +export const Skeleton = ({ className }: SkeletonProps) => { + return ( +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx b/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx index 6bc6d1719c..baf03c2873 100644 --- a/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx +++ b/packages/admin-next/dashboard/src/components/error/error-boundary/error-boundary.tsx @@ -1,5 +1,5 @@ -import type { AxiosError } from "axios" import { Navigate, useLocation, useRouteError } from "react-router-dom" +import { isAxiosError } from "../../../lib/is-axios-error" export const ErrorBoundary = () => { const error = useRouteError() @@ -20,7 +20,3 @@ export const ErrorBoundary = () => { // TODO: Actual catch-all error page return
Dang!
} - -const isAxiosError = (error: any): error is AxiosError => { - return error.isAxiosError -} diff --git a/packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx b/packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx new file mode 100644 index 0000000000..a9c161ba17 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/filter-group/filter-group.tsx @@ -0,0 +1,54 @@ +import { Button, DropdownMenu } from "@medusajs/ui" +import { ReactNode } from "react" +import { useSearchParams } from "react-router-dom" + +type FilterGroupProps = { + filters: { + [key: string]: ReactNode + } +} + +export const FilterGroup = ({ filters }: FilterGroupProps) => { + const [searchParams] = useSearchParams() + const filterKeys = Object.keys(filters) + + if (filterKeys.length === 0) { + return null + } + + const isClearable = filterKeys.some((key) => searchParams.get(key)) + const hasMore = !filterKeys.every((key) => searchParams.get(key)) + const availableKeys = filterKeys.filter((key) => !searchParams.get(key)) + + return ( +
+ {hasMore && } + {isClearable && ( + + )} +
+ ) +} + +type AddFilterMenuProps = { + availableKeys: string[] +} + +const AddFilterMenu = ({ availableKeys }: AddFilterMenuProps) => { + return ( + + + + + + {availableKeys.map((key) => ( + {key} + ))} + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts b/packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts new file mode 100644 index 0000000000..03b216813f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/filter-group/index.ts @@ -0,0 +1 @@ +export * from "./filter-group" diff --git a/packages/admin-next/dashboard/src/components/filtering/order-by/index.ts b/packages/admin-next/dashboard/src/components/filtering/order-by/index.ts new file mode 100644 index 0000000000..d8200bb491 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/order-by/index.ts @@ -0,0 +1 @@ +export * from "./order-by" diff --git a/packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx b/packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx new file mode 100644 index 0000000000..e2dcb2793c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/order-by/order-by.tsx @@ -0,0 +1,148 @@ +import { ArrowUpDown } from "@medusajs/icons" +import { DropdownMenu, IconButton } from "@medusajs/ui" +import { useState } from "react" +import { useTranslation } from "react-i18next" +import { useSearchParams } from "react-router-dom" + +type OrderByProps = { + keys: string[] +} + +enum SortDirection { + ASC = "asc", + DESC = "desc", +} + +type SortState = { + key?: string + dir: SortDirection +} + +const initState = (params: URLSearchParams): SortState => { + const sortParam = params.get("order") + + if (!sortParam) { + return { + dir: SortDirection.ASC, + } + } + + const dir = sortParam.startsWith("-") ? SortDirection.DESC : SortDirection.ASC + const key = sortParam.replace("-", "") + + return { + key, + dir, + } +} + +const formatKey = (key: string) => { + const words = key.split("_") + const formattedWords = words.map((word, index) => { + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1) + } else { + return word + } + }) + return formattedWords.join(" ") +} + +export const OrderBy = ({ keys }: OrderByProps) => { + const [searchParams, setSearchParams] = useSearchParams() + const [state, setState] = useState<{ + key?: string + dir: SortDirection + }>(initState(searchParams)) + + const { t } = useTranslation() + + const handleDirChange = (dir: string) => { + setState((prev) => ({ + ...prev, + dir: dir as SortDirection, + })) + updateOrderParam({ + key: state.key, + dir: dir as SortDirection, + }) + } + + const handleKeyChange = (value: string) => { + setState((prev) => ({ + ...prev, + key: value, + })) + + updateOrderParam({ + key: value, + dir: state.dir, + }) + } + + const updateOrderParam = (state: SortState) => { + if (!state.key) { + setSearchParams((prev) => { + prev.delete("order") + return prev + }) + + return + } + + const orderParam = + state.dir === SortDirection.ASC ? state.key : `-${state.key}` + setSearchParams((prev) => { + prev.set("order", orderParam) + return prev + }) + } + + return ( + + + + + + + + + {keys.map((key) => ( + event.preventDefault()} + > + {formatKey(key)} + + ))} + + + + event.preventDefault()} + > + {t("general.ascending")} + 1 - 30 + + event.preventDefault()} + > + {t("general.descending")} + 30 - 1 + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/filtering/query/index.ts b/packages/admin-next/dashboard/src/components/filtering/query/index.ts new file mode 100644 index 0000000000..4ddfa841d3 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/query/index.ts @@ -0,0 +1 @@ +export * from "./query" diff --git a/packages/admin-next/dashboard/src/components/filtering/query/query.tsx b/packages/admin-next/dashboard/src/components/filtering/query/query.tsx new file mode 100644 index 0000000000..eb5bef4907 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/filtering/query/query.tsx @@ -0,0 +1,57 @@ +import { Input } from "@medusajs/ui" +import { debounce } from "lodash" +import { ChangeEvent, useCallback, useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useSearchParams } from "react-router-dom" + +type QueryProps = { + placeholder?: string +} + +export const Query = ({ placeholder }: QueryProps) => { + const { t } = useTranslation() + const placeholderText = placeholder || t("general.search") + + const [searchParams, setSearchParams] = useSearchParams() + const [inputValue, setInputValue] = useState(searchParams.get("q") || "") + + const updateSearchParams = (newValue: string) => { + if (!newValue) { + setSearchParams((prev) => { + prev.delete("q") + return prev + }) + + return + } + + setSearchParams((prev) => ({ ...prev, q: newValue || "" })) + } + + const debouncedUpdate = useCallback( + debounce((newValue: string) => updateSearchParams(newValue), 500), + [] + ) + + useEffect(() => { + debouncedUpdate(inputValue) + + return () => { + debouncedUpdate.cancel() + } + }, [inputValue, debouncedUpdate]) + + const handleInputChange = (event: ChangeEvent) => { + setInputValue(event.target.value) + } + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx deleted file mode 100644 index 0946a6a9c3..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/app-layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Outlet, useLocation } from "react-router-dom" -import { Gutter } from "./gutter" -import { MainNav } from "./main-nav" -import { SettingsNav } from "./settings-nav" -import { Topbar } from "./topbar" - -export const AppLayout = () => { - const location = useLocation() - - const isSettings = location.pathname.startsWith("/settings") - - return ( -
- -
- {isSettings && } -
- - - - -
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx deleted file mode 100644 index 3ccb7ce18e..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/breadcrumbs.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { TriangleRightMini } from "@medusajs/icons"; -import { clx } from "@medusajs/ui"; -import { Link, UIMatch, useMatches } from "react-router-dom"; - -type BreadcrumbProps = React.ComponentPropsWithoutRef<"ol">; - -export const Breadcrumbs = ({ className, ...props }: BreadcrumbProps) => { - const matches = useMatches() as unknown as UIMatch< - unknown, - { crumb?: (data?: unknown) => string } - >[]; - - const crumbs = matches - .filter((match) => Boolean(match.handle?.crumb)) - .map((match) => { - const handle = match.handle; - - return { - label: handle.crumb!(match.data), - path: match.pathname, - }; - }); - - if (crumbs.length < 2) { - return null; - } - - return ( -
    - {crumbs.map((crumb, index) => { - const isLast = index === crumbs.length - 1; - - return ( -
  1. - {!isLast ? ( - {crumb.label} - ) : ( - {crumb.label} - )} - {!isLast && } -
  2. - ); - })} -
- ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx deleted file mode 100644 index 00b75f447b..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/gutter.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PropsWithChildren } from "react"; - -export const Gutter = ({ children }: PropsWithChildren) => { - return ( -
- {children} -
- ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/index.ts b/packages/admin-next/dashboard/src/components/layout/app-layout/index.ts deleted file mode 100644 index 07c56e200e..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./app-layout"; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx deleted file mode 100644 index 917b5b80aa..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/loader.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { QueryClient } from "@tanstack/react-query"; -import { adminProductKeys } from "medusa-react"; -import { LoaderFunctionArgs } from "react-router-dom"; -import { medusa, queryClient } from "../../../lib/medusa"; - -const appLoaderQuery = (id: string) => ({ - queryKey: adminProductKeys.detail(id), - queryFn: async () => medusa.admin.products.retrieve(id), -}); - -export const productLoader = (client: QueryClient) => { - return async ({ params }: LoaderFunctionArgs) => { - const id = params?.id; - - if (!id) { - throw new Error("No id provided"); - } - - const query = appLoaderQuery(id); - - return ( - queryClient.getQueryData(query.queryKey) ?? - (await client.fetchQuery(query)) - ); - }; -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx deleted file mode 100644 index 9332308ea7..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/main-nav.tsx +++ /dev/null @@ -1,380 +0,0 @@ -import { - ArrowRightOnRectangle, - BookOpen, - BuildingStorefront, - Calendar, - ChevronDownMini, - CircleHalfSolid, - CogSixTooth, - CurrencyDollar, - EllipsisHorizontal, - MinusMini, - ReceiptPercent, - ShoppingCart, - Sidebar, - SquaresPlus, - Tag, - Users, -} from "@medusajs/icons" -import { Avatar, DropdownMenu, IconButton, Text } from "@medusajs/ui" -import * as Collapsible from "@radix-ui/react-collapsible" -import * as Dialog from "@radix-ui/react-dialog" -import { useAdminDeleteSession, useAdminStore } from "medusa-react" -import { Link, useLocation, useNavigate } from "react-router-dom" - -import { useAuth } from "../../../providers/auth-provider" -import { useTheme } from "../../../providers/theme-provider" - -import { Fragment, useEffect, useState } from "react" -import { Breadcrumbs } from "./breadcrumbs" -import { NavItem, NavItemProps } from "./nav-item" -import { Notifications } from "./notifications" -import { SearchToggle } from "./search-toggle" -import { Spacer } from "./spacer" - -import extensions from "medusa-admin:routes/links" -import { useTranslation } from "react-i18next" - -export const MainNav = () => { - return ( - - - - - ) -} - -const MobileNav = () => { - const [open, setOpen] = useState(false) - const location = useLocation() - - // If the user navigates to a new route, we want to close the menu - useEffect(() => { - setOpen(false) - }, [location.pathname]) - - return ( -
- -
- - - - - - -
- - - -
-
-
- -
- - -
-
- - - -
-
-
-
-
- - -
-
- ) -} - -const DesktopNav = () => { - return ( - - ) -} - -const Header = () => { - const { store } = useAdminStore() - const { setTheme, theme } = useTheme() - const { mutateAsync: logoutMutation } = useAdminDeleteSession() - const navigate = useNavigate() - - const logout = async () => { - await logoutMutation(undefined, { - onSuccess: () => { - navigate("/login") - }, - }) - } - - if (!store) { - return null - } - - return ( -
- - -
-
-
-
- {store.name[0].toUpperCase()} -
-
- - {store.name} - -
-
- -
-
-
- - - - Store Settings - - - - - - Documentation - - - - - - Changelog - - - - - - - Theme - - - - { - e.preventDefault() - setTheme("light") - }} - > - Light - - { - e.preventDefault() - setTheme("dark") - }} - > - Dark - - - - - - - - Logout - ⌥⇧Q - - -
-
- ) -} - -const useCoreRoutes = (): Omit[] => { - const { t } = useTranslation() - - return [ - { - icon: , - label: t("orders.domain"), - to: "/orders", - items: [ - { - label: t("draftOrders.domain"), - to: "/draft-orders", - }, - ], - }, - { - icon: , - label: t("products.domain"), - to: "/products", - items: [ - { - label: t("collections.domain"), - to: "/collections", - }, - { - label: t("categories.domain"), - to: "/categories", - }, - { - label: t("giftCards.domain"), - to: "/gift-cards", - }, - { - label: t("inventory.domain"), - to: "/inventory", - }, - ], - }, - { - icon: , - label: t("customers.domain"), - to: "/customers", - items: [ - { - label: t("customerGroups.domain"), - to: "/customer-groups", - }, - ], - }, - { - icon: , - label: t("discounts.domain"), - to: "/discounts", - }, - { - icon: , - label: t("pricing.domain"), - to: "/pricing", - }, - ] -} - -const CoreRouteSection = () => { - const coreRoutes = useCoreRoutes() - - return ( - - ) -} - -const ExtensionRouteSection = () => { - if (!extensions.links || extensions.links.length === 0) { - return null - } - - return ( -
- -
- -
- - - -
- -
- {extensions.links.map((link) => { - return ( - : } - type="extension" - /> - ) - })} -
-
-
-
-
- ) -} - -const SettingsSection = () => { - return ( -
- } label="Settings" to="/settings" /> -
- ) -} - -const UserSection = () => { - const { user } = useAuth() - - if (!user) { - return null - } - - const fallback = - user.first_name && user.last_name - ? `${user.first_name[0]}${user.last_name[0]}` - : user.first_name - ? user.first_name[0] - : user.email[0] - - return ( -
- - -
- {(user.first_name || user.last_name) && ( - {`${user.first_name && `${user.first_name} `}${ - user.last_name - }`} - )} - - {user.email} - -
- -
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx deleted file mode 100644 index 23d6f9946e..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/nav-item.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { Text, clx } from "@medusajs/ui"; -import * as Collapsible from "@radix-ui/react-collapsible"; -import { useEffect, useState } from "react"; -import { Link, useLocation } from "react-router-dom"; - -type ItemType = "core" | "extension"; - -type NestedItemProps = { - label: string; - to: string; -}; - -export type NavItemProps = { - icon?: React.ReactNode; - label: string; - to: string; - items?: NestedItemProps[]; - type?: ItemType; -}; - -export const NavItem = ({ - icon, - label, - to, - items, - type = "core", -}: NavItemProps) => { - const location = useLocation(); - - const [open, setOpen] = useState( - [to, ...(items?.map((i) => i.to) ?? [])].some((p) => - location.pathname.startsWith(p) - ) - ); - - useEffect(() => { - setOpen( - [to, ...(items?.map((i) => i.to) ?? [])].some((p) => - location.pathname.startsWith(p) - ) - ); - }, [location.pathname, to, items]); - - return ( -
- 0, - } - )} - > - - - {label} - - - {items && items.length > 0 && ( - - - - - {label} - - - - -
-
-
- - {label} - - - {items.map((item) => { - return ( - -
-
-
- - {item.label} - - - ); - })} - - - )} -
- ); -}; - -const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => { - if (!icon) { - return null; - } - - return type === "extension" ? ( -
-
{icon}
-
- ) : ( - icon - ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx deleted file mode 100644 index 949cca5ce1..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/search-toggle.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { MagnifyingGlass } from "@medusajs/icons" -import { IconButton } from "@medusajs/ui" -import { useSearch } from "../../../providers/search-provider" - -export const SearchToggle = () => { - const { toggleSearch } = useSearch() - - return ( - - - - ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx deleted file mode 100644 index acb25a8aa9..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/settings-nav.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { ChevronDownMini, CogSixTooth, MinusMini } from "@medusajs/icons" -import { Text } from "@medusajs/ui" -import * as Collapsible from "@radix-ui/react-collapsible" - -import { useMemo } from "react" -import { useTranslation } from "react-i18next" -import { NavItem, NavItemProps } from "./nav-item" -import { Spacer } from "./spacer" - -const useSettingRoutes = (): NavItemProps[] => { - const { t } = useTranslation() - - return useMemo( - () => [ - { - label: t("profile.domain"), - to: "/settings/profile", - }, - { - label: t("store.domain"), - to: "/settings/store", - }, - { - label: t("users.domain"), - to: "/settings/users", - }, - { - label: t("regions.domain"), - to: "/settings/regions", - }, - { - label: t("currencies.domain"), - to: "/settings/currencies", - }, - { - label: "Taxes", - to: "/settings/taxes", - }, - { - label: "Locations", - to: "/settings/locations", - }, - { - label: t("salesChannels.domain"), - to: "/settings/sales-channels", - }, - { - label: t("apiKeyManagement.domain"), - to: "/settings/api-key-management", - }, - ], - [t] - ) -} - -export const SettingsNav = () => { - const routes = useSettingRoutes() - const { t } = useTranslation() - - return ( -
-
-
- - - {t("general.settings")} - -
-
- -
- -
- - - -
- - - -
- -
- - - -
- - - -
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx deleted file mode 100644 index 4e00210d2f..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/spacer.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const Spacer = () => { - return ( -
-
-
- ); -}; diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx b/packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx deleted file mode 100644 index cd2bdc40db..0000000000 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/topbar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Sidebar } from "@medusajs/icons" -import { Breadcrumbs } from "./breadcrumbs" -import { Notifications } from "./notifications" -import { SearchToggle } from "./search-toggle" - -export const Topbar = () => { - return ( -
-
- - -
-
- - -
-
- ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/main-layout/index.ts b/packages/admin-next/dashboard/src/components/layout/main-layout/index.ts new file mode 100644 index 0000000000..035d09e6d6 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/index.ts @@ -0,0 +1 @@ +export * from "./main-layout" 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 new file mode 100644 index 0000000000..0d76bd7dd4 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx @@ -0,0 +1,197 @@ +import { + ChevronDownMini, + CurrencyDollar, + MinusMini, + ReceiptPercent, + ShoppingCart, + SquaresPlus, + Tag, + Users, +} from "@medusajs/icons" +import { Avatar, Text } from "@medusajs/ui" +import * as Collapsible from "@radix-ui/react-collapsible" +import { useAdminStore } from "medusa-react" +import { useTranslation } from "react-i18next" + +import { Skeleton } from "../../common/skeleton" +import { NavItem, NavItemProps } from "../nav-item" +import { Shell } from "../shell" + +import extensions from "medusa-admin:routes/links" + +export const MainLayout = () => { + return ( + + + + ) +} + +const MainSidebar = () => { + return ( + + ) +} + +const Header = () => { + const { store, isError, error } = useAdminStore() + + const name = store?.name + const fallback = store?.name?.slice(0, 1).toUpperCase() + + if (isError) { + throw error + } + + return ( +
+
+
+ {fallback ? ( + + ) : ( + + )} + {name ? ( + + {store.name} + + ) : ( + + )} +
+
+
+ ) +} + +const useCoreRoutes = (): Omit[] => { + const { t } = useTranslation() + + return [ + { + icon: , + label: t("orders.domain"), + to: "/orders", + items: [ + { + label: t("draftOrders.domain"), + to: "/draft-orders", + }, + ], + }, + { + icon: , + label: t("products.domain"), + to: "/products", + items: [ + { + label: t("collections.domain"), + to: "/collections", + }, + { + label: t("categories.domain"), + to: "/categories", + }, + { + label: t("giftCards.domain"), + to: "/gift-cards", + }, + { + label: t("inventory.domain"), + to: "/inventory", + }, + ], + }, + { + icon: , + label: t("customers.domain"), + to: "/customers", + items: [ + { + label: t("customerGroups.domain"), + to: "/customer-groups", + }, + ], + }, + { + icon: , + label: t("discounts.domain"), + to: "/discounts", + }, + { + icon: , + label: t("pricing.domain"), + to: "/pricing", + }, + ] +} + +const CoreRouteSection = () => { + const coreRoutes = useCoreRoutes() + + return ( + + ) +} + +const ExtensionRouteSection = () => { + if (!extensions.links || extensions.links.length === 0) { + return null + } + + return ( +
+
+
+
+
+ +
+ + + +
+ +
+ {extensions.links.map((link) => { + return ( + : } + type="extension" + /> + ) + })} +
+
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/nav-item/index.ts b/packages/admin-next/dashboard/src/components/layout/nav-item/index.ts new file mode 100644 index 0000000000..c0155cc9bb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/nav-item/index.ts @@ -0,0 +1 @@ +export * from "./nav-item" 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 new file mode 100644 index 0000000000..55f7cc7204 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/nav-item/nav-item.tsx @@ -0,0 +1,156 @@ +import { Text, clx } from "@medusajs/ui" +import * as Collapsible from "@radix-ui/react-collapsible" +import { useEffect, useState } from "react" +import { Link, useLocation } from "react-router-dom" + +type ItemType = "core" | "extension" + +type NestedItemProps = { + label: string + to: string +} + +export type NavItemProps = { + icon?: React.ReactNode + label: string + to: string + items?: NestedItemProps[] + type?: ItemType + from?: string +} + +export const NavItem = ({ + icon, + label, + to, + items, + type = "core", + from, +}: NavItemProps) => { + const location = useLocation() + + const [open, setOpen] = useState( + [to, ...(items?.map((i) => i.to) ?? [])].some((p) => + location.pathname.startsWith(p) + ) + ) + + useEffect(() => { + setOpen( + [to, ...(items?.map((i) => i.to) ?? [])].some((p) => + location.pathname.startsWith(p) + ) + ) + }, [location.pathname, to, items]) + + return ( +
+ 0, + } + )} + > + + + {label} + + + {items && items.length > 0 && ( + + + + + {label} + + + +
+
+
+
+ + + {label} + + +
+
    + {items.map((item) => { + return ( +
  • +
    +
    +
    + + + {item.label} + + +
  • + ) + })} +
+ + + )} +
+ ) +} + +const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => { + if (!icon) { + return null + } + + return type === "extension" ? ( +
+
{icon}
+
+ ) : ( + icon + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/notifications/index.ts b/packages/admin-next/dashboard/src/components/layout/notifications/index.ts new file mode 100644 index 0000000000..3509846360 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/notifications/index.ts @@ -0,0 +1 @@ +export * from "./notifications" diff --git a/packages/admin-next/dashboard/src/components/layout/app-layout/notifications.tsx b/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx similarity index 53% rename from packages/admin-next/dashboard/src/components/layout/app-layout/notifications.tsx rename to packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx index 6af0a698a3..921147ec64 100644 --- a/packages/admin-next/dashboard/src/components/layout/app-layout/notifications.tsx +++ b/packages/admin-next/dashboard/src/components/layout/notifications/notifications.tsx @@ -1,28 +1,31 @@ -import { BellAlert } from "@medusajs/icons"; -import { Drawer, Heading, IconButton } from "@medusajs/ui"; -import { useEffect, useState } from "react"; +import { BellAlert } from "@medusajs/icons" +import { Drawer, Heading, IconButton } from "@medusajs/ui" +import { useEffect, useState } from "react" export const Notifications = () => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false) useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "n" && (e.metaKey || e.ctrlKey)) { - setOpen((prev) => !prev); + setOpen((prev) => !prev) } - }; + } - document.addEventListener("keydown", onKeyDown); + document.addEventListener("keydown", onKeyDown) return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, []); + document.removeEventListener("keydown", onKeyDown) + } + }, []) return ( - + @@ -33,5 +36,5 @@ export const Notifications = () => { Notifications will go here - ); -}; + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts b/packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts new file mode 100644 index 0000000000..6c0bebb323 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/index.ts @@ -0,0 +1 @@ +export * from "./settings-layout" diff --git a/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx new file mode 100644 index 0000000000..25f181756c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -0,0 +1,99 @@ +import { ArrowUturnLeft } from "@medusajs/icons" +import { IconButton, Text } from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useLocation } from "react-router-dom" + +import { NavItem, NavItemProps } from "../nav-item" +import { Shell } from "../shell" + +export const SettingsLayout = () => { + return ( + + + + ) +} + +const useSettingRoutes = (): NavItemProps[] => { + const { t } = useTranslation() + + return useMemo( + () => [ + { + label: t("profile.domain"), + to: "/settings/profile", + }, + { + label: t("store.domain"), + to: "/settings/store", + }, + { + label: t("users.domain"), + to: "/settings/users", + }, + { + label: t("regions.domain"), + to: "/settings/regions", + }, + { + label: "Taxes", + to: "/settings/taxes", + }, + { + label: "Locations", + to: "/settings/locations", + }, + { + label: t("salesChannels.domain"), + to: "/settings/sales-channels", + }, + { + label: t("apiKeyManagement.domain"), + to: "/settings/api-key-management", + }, + ], + [t] + ) +} + +const SettingsSidebar = () => { + const routes = useSettingRoutes() + const { t } = useTranslation() + + const location = useLocation() + const [from, setFrom] = useState("/orders") + + useEffect(() => { + if (location.state?.from) { + setFrom(location.state.from) + } + }, [location]) + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/layout/shell/index.ts b/packages/admin-next/dashboard/src/components/layout/shell/index.ts new file mode 100644 index 0000000000..d1e0c03c89 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/shell/index.ts @@ -0,0 +1 @@ +export * from "./shell" diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx new file mode 100644 index 0000000000..31044999a7 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -0,0 +1,378 @@ +import { + ArrowRightOnRectangle, + BellAlert, + BookOpen, + Calendar, + CircleHalfSolid, + CogSixTooth, + MagnifyingGlass, + Sidebar, + User as UserIcon, +} from "@medusajs/icons" +import { Avatar, DropdownMenu, IconButton, Kbd, Text, clx } from "@medusajs/ui" +import * as Dialog from "@radix-ui/react-dialog" +import { useAdminDeleteSession, useAdminGetSession } from "medusa-react" +import { PropsWithChildren } from "react" +import { + Link, + Outlet, + UIMatch, + useLocation, + useMatches, + useNavigate, +} from "react-router-dom" + +import { Skeleton } from "../../common/skeleton" + +import { useSearch } from "../../../providers/search-provider" +import { useSidebar } from "../../../providers/sidebar-provider" +import { useTheme } from "../../../providers/theme-provider" + +export const Shell = ({ children }: PropsWithChildren) => { + return ( +
+
+ {children} + {children} +
+
+ +
+ + + +
+
+
+ ) +} + +const Gutter = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ) +} + +const Breadcrumbs = () => { + const matches = useMatches() as unknown as UIMatch< + unknown, + { crumb?: (data?: unknown) => string } + >[] + + const crumbs = matches + .filter((match) => Boolean(match.handle?.crumb)) + .map((match) => { + const handle = match.handle + + return { + label: handle.crumb!(match.data), + path: match.pathname, + } + }) + + return ( +
    + {crumbs.map((crumb, index) => { + const isLast = index === crumbs.length - 1 + + return ( +
  1. + {!isLast ? ( + + {crumb.label} + + ) : ( +
    + ... + + {crumb.label} + +
    + )} + {/* {!isLast && } */} + {!isLast && ›} +
  2. + ) + })} +
+ ) +} + +const UserBadge = () => { + const { user, isError, error } = useAdminGetSession() + + const displayName = user + ? user.first_name && user.last_name + ? `${user.first_name} ${user.last_name}` + : user.first_name + ? user.first_name + : user.email + : null + + const fallback = displayName ? displayName[0].toUpperCase() : null + + 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 } = useAdminDeleteSession() + + const handleLayout = async () => { + await logoutMutation(undefined, { + onSuccess: () => { + navigate("/login") + }, + }) + } + + return ( + +
+ + Logout +
+
+ ) +} + +const Profile = () => { + const location = useLocation() + + return ( + + + + Profile + + + ) +} + +const LoggedInUser = () => { + return ( + + + + + + + + + Documentation + + + + + + Changelog + + + + + + + + + ) +} + +const SettingsLink = () => { + const location = useLocation() + + return ( + + + + + + ) +} + +const ToggleNotifications = () => { + return ( + + + + ) +} + +const Searchbar = () => { + const { toggleSearch } = useSearch() + + return ( + + ) +} + +const ToggleSidebar = () => { + const { toggle } = useSidebar() + + return ( +
+ toggle("desktop")} + > + + + toggle("mobile")} + > + + +
+ ) +} + +const Topbar = () => { + return ( +
+
+ + +
+
+ +
+
+
+ + +
+ +
+
+ ) +} + +const DesktopSidebarContainer = ({ children }: PropsWithChildren) => { + const { desktop } = useSidebar() + + return ( +
+ {children} +
+ ) +} + +const MobileSidebarContainer = ({ children }: PropsWithChildren) => { + const { mobile, toggle } = useSidebar() + + return ( + toggle("mobile")}> + + + + {children} + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts new file mode 100644 index 0000000000..3b64d8f60f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/index.ts @@ -0,0 +1 @@ +export * from "./localized-date-picker" diff --git a/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx new file mode 100644 index 0000000000..8517f15817 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx @@ -0,0 +1,36 @@ +import { DatePicker } from "@medusajs/ui" +import { ComponentPropsWithoutRef } from "react" +import { useTranslation } from "react-i18next" +import { languages } from "../../../i18n/config" + +type LocalizedDatePickerProps = Omit< + ComponentPropsWithoutRef, + "translations" | "locale" +> + +export const LocalizedDatePicker = ({ + mode = "single", + ...props +}: LocalizedDatePickerProps) => { + const { i18n, t } = useTranslation() + + const locale = languages.find((lang) => lang.code === i18n.language) + ?.date_locale + + const translations = { + cancel: t("general.cancel"), + apply: t("general.apply"), + end: t("general.end"), + start: t("general.start"), + range: t("general.range"), + } + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts new file mode 100644 index 0000000000..40d1ccdfac --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/index.ts @@ -0,0 +1 @@ +export * from "./localized-table-pagination" diff --git a/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx new file mode 100644 index 0000000000..8ec8a46955 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/localization/localized-table-pagination/localized-table-pagination.tsx @@ -0,0 +1,26 @@ +import { Table } from "@medusajs/ui" +import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react" +import { useTranslation } from "react-i18next" + +type LocalizedTablePaginationProps = Omit< + ComponentPropsWithoutRef, + "translations" +> + +export const LocalizedTablePagination = forwardRef< + ElementRef, + LocalizedTablePaginationProps +>((props, ref) => { + const { t } = useTranslation() + + const translations = { + of: t("general.of"), + results: t("general.results"), + pages: t("general.pages"), + prev: t("general.prev"), + next: t("general.next"), + } + + return +}) +LocalizedTablePagination.displayName = "LocalizedTablePagination" diff --git a/packages/admin-next/dashboard/src/components/search/search.tsx b/packages/admin-next/dashboard/src/components/search/search.tsx index 5bee68d10a..d7fcfad992 100644 --- a/packages/admin-next/dashboard/src/components/search/search.tsx +++ b/packages/admin-next/dashboard/src/components/search/search.tsx @@ -1,5 +1,5 @@ import { MagnifyingGlass } from "@medusajs/icons" -import { clx } from "@medusajs/ui" +import { Kbd, Text, clx } from "@medusajs/ui" import * as Dialog from "@radix-ui/react-dialog" import { Command } from "cmdk" import { @@ -10,6 +10,7 @@ import { useMemo, } from "react" import { useTranslation } from "react-i18next" + import { useSearch } from "../../providers/search-provider" export const Search = () => { @@ -100,6 +101,18 @@ const useLinks = (): CommandGroupProps[] => { { label: t("users.domain"), }, + { + label: t("regions.domain"), + }, + { + label: t("locations.domain"), + }, + { + label: t("salesChannels.domain"), + }, + { + label: t("apiKeyManagement.domain"), + }, ], }, ], @@ -129,11 +142,30 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - + {children} -
+
+
+
+
+ + Navigation + +
+ ↓ + ↑ +
+
+
+ + Open Result + + ↵ +
+
+
@@ -149,7 +181,7 @@ const CommandInput = forwardRef< { + const { t } = useTranslation() + const fn = usePrompt() + + const promptValues = { + title: t("general.unsavedChangesTitle"), + description: t("general.unsavedChangesDescription"), + cancelText: t("general.cancel"), + confirmText: t("general.continue"), + } + + const prompt = async () => { + return await fn(promptValues) + } + + return prompt +} diff --git a/packages/admin-next/dashboard/src/hooks/use-query-params.tsx b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx new file mode 100644 index 0000000000..afb282362e --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx @@ -0,0 +1,16 @@ +import { useSearchParams } from "react-router-dom" + +export function useQueryParams( + keys: T[] +): Record { + const [params] = useSearchParams() + + // Use a type assertion to initialize the result + const result = {} as Record + + keys.forEach((key) => { + result[key] = params.get(key) || undefined + }) + + return result +} diff --git a/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx b/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx new file mode 100644 index 0000000000..ce20816951 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx @@ -0,0 +1,63 @@ +import { usePrompt } from "@medusajs/ui" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" + +type Prompt = { + title: string + description: string + cancelText: string + confirmText: string +} + +/** + * Hook for managing the state of route modals. + */ +export const useRouteModalState = (): [ + open: boolean, + onOpenChange: (open: boolean, ignore?: boolean) => void, + /** + * Subscribe to the dirty state of the form. + * If the form is dirty, the modal will prompt + * the user before closing. + */ + subscribe: (value: boolean) => void, +] => { + const [open, setOpen] = useState(false) + const [shouldPrompt, subscribe] = useState(false) + + const navigate = useNavigate() + const prompt = usePrompt() + const { t } = useTranslation() + + let promptValues: Prompt = { + title: t("general.unsavedChangesTitle"), + description: t("general.unsavedChangesDescription"), + cancelText: t("general.cancel"), + confirmText: t("general.continue"), + } + + useEffect(() => { + setOpen(true) + }, []) + + const onOpenChange = async (open: boolean, ignore = false) => { + if (!open) { + if (shouldPrompt && !ignore) { + const confirmed = await prompt(promptValues) + + if (!confirmed) { + return + } + } + + setTimeout(() => { + navigate("..", { replace: true }) + }, 200) + } + + setOpen(open) + } + + return [open, onOpenChange, subscribe] +} diff --git a/packages/admin-next/dashboard/src/i18n/config.ts b/packages/admin-next/dashboard/src/i18n/config.ts index c346a4b8ac..ff128d9e5e 100644 --- a/packages/admin-next/dashboard/src/i18n/config.ts +++ b/packages/admin-next/dashboard/src/i18n/config.ts @@ -1,3 +1,4 @@ +import { enUS } from "date-fns/locale" import i18n from "i18next" import LanguageDetector from "i18next-browser-languagedetector" import Backend, { type HttpBackendOptions } from "i18next-http-backend" @@ -22,6 +23,7 @@ export const languages: Language[] = [ code: "en", display_name: "English", ltr: true, + date_locale: enUS, }, ] diff --git a/packages/admin-next/dashboard/src/i18n/types.ts b/packages/admin-next/dashboard/src/i18n/types.ts index 6dd753ee66..14e59028fd 100644 --- a/packages/admin-next/dashboard/src/i18n/types.ts +++ b/packages/admin-next/dashboard/src/i18n/types.ts @@ -1,3 +1,4 @@ +import type { Locale } from "date-fns" import en from "../../public/locales/en/translation.json" const resources = { @@ -10,4 +11,5 @@ export type Language = { code: string display_name: string ltr: boolean + date_locale: Locale } diff --git a/packages/admin-next/dashboard/src/lib/countries.ts b/packages/admin-next/dashboard/src/lib/countries.ts new file mode 100644 index 0000000000..8e4efca026 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/countries.ts @@ -0,0 +1,1755 @@ +/** This file is auto-generated. Do not modify it manually. */ +import type { Country } from "@medusajs/medusa" + +export const countries: Omit[] = [ + { + iso_2: "af", + iso_3: "afg", + num_code: 4, + name: "AFGHANISTAN", + display_name: "Afghanistan", + }, + { + iso_2: "al", + iso_3: "alb", + num_code: 8, + name: "ALBANIA", + display_name: "Albania", + }, + { + iso_2: "dz", + iso_3: "dza", + num_code: 12, + name: "ALGERIA", + display_name: "Algeria", + }, + { + iso_2: "as", + iso_3: "asm", + num_code: 16, + name: "AMERICAN SAMOA", + display_name: "American Samoa", + }, + { + iso_2: "ad", + iso_3: "and", + num_code: 20, + name: "ANDORRA", + display_name: "Andorra", + }, + { + iso_2: "ao", + iso_3: "ago", + num_code: 24, + name: "ANGOLA", + display_name: "Angola", + }, + { + iso_2: "ai", + iso_3: "aia", + num_code: 660, + name: "ANGUILLA", + display_name: "Anguilla", + }, + { + iso_2: "aq", + iso_3: "ata", + num_code: 10, + name: "ANTARCTICA", + display_name: "Antarctica", + }, + { + iso_2: "ag", + iso_3: "atg", + num_code: 28, + name: "ANTIGUA AND BARBUDA", + display_name: "Antigua and Barbuda", + }, + { + iso_2: "ar", + iso_3: "arg", + num_code: 32, + name: "ARGENTINA", + display_name: "Argentina", + }, + { + iso_2: "am", + iso_3: "arm", + num_code: 51, + name: "ARMENIA", + display_name: "Armenia", + }, + { + iso_2: "aw", + iso_3: "abw", + num_code: 533, + name: "ARUBA", + display_name: "Aruba", + }, + { + iso_2: "au", + iso_3: "aus", + num_code: 36, + name: "AUSTRALIA", + display_name: "Australia", + }, + { + iso_2: "at", + iso_3: "aut", + num_code: 40, + name: "AUSTRIA", + display_name: "Austria", + }, + { + iso_2: "az", + iso_3: "aze", + num_code: 31, + name: "AZERBAIJAN", + display_name: "Azerbaijan", + }, + { + iso_2: "bs", + iso_3: "bhs", + num_code: 44, + name: "BAHAMAS", + display_name: "Bahamas", + }, + { + iso_2: "bh", + iso_3: "bhr", + num_code: 48, + name: "BAHRAIN", + display_name: "Bahrain", + }, + { + iso_2: "bd", + iso_3: "bgd", + num_code: 50, + name: "BANGLADESH", + display_name: "Bangladesh", + }, + { + iso_2: "bb", + iso_3: "brb", + num_code: 52, + name: "BARBADOS", + display_name: "Barbados", + }, + { + iso_2: "by", + iso_3: "blr", + num_code: 112, + name: "BELARUS", + display_name: "Belarus", + }, + { + iso_2: "be", + iso_3: "bel", + num_code: 56, + name: "BELGIUM", + display_name: "Belgium", + }, + { + iso_2: "bz", + iso_3: "blz", + num_code: 84, + name: "BELIZE", + display_name: "Belize", + }, + { + iso_2: "bj", + iso_3: "ben", + num_code: 204, + name: "BENIN", + display_name: "Benin", + }, + { + iso_2: "bm", + iso_3: "bmu", + num_code: 60, + name: "BERMUDA", + display_name: "Bermuda", + }, + { + iso_2: "bt", + iso_3: "btn", + num_code: 64, + name: "BHUTAN", + display_name: "Bhutan", + }, + { + iso_2: "bo", + iso_3: "bol", + num_code: 68, + name: "BOLIVIA", + display_name: "Bolivia", + }, + { + iso_2: "bq", + iso_3: "bes", + num_code: 535, + name: "BONAIRE, SINT EUSTATIUS AND SABA", + display_name: "Bonaire, Sint Eustatius and Saba", + }, + { + iso_2: "ba", + iso_3: "bih", + num_code: 70, + name: "BOSNIA AND HERZEGOVINA", + display_name: "Bosnia and Herzegovina", + }, + { + iso_2: "bw", + iso_3: "bwa", + num_code: 72, + name: "BOTSWANA", + display_name: "Botswana", + }, + { + iso_2: "bv", + iso_3: "bvd", + num_code: 74, + name: "BOUVET ISLAND", + display_name: "Bouvet Island", + }, + { + iso_2: "br", + iso_3: "bra", + num_code: 76, + name: "BRAZIL", + display_name: "Brazil", + }, + { + iso_2: "io", + iso_3: "iot", + num_code: 86, + name: "BRITISH INDIAN OCEAN TERRITORY", + display_name: "British Indian Ocean Territory", + }, + { + iso_2: "bn", + iso_3: "brn", + num_code: 96, + name: "BRUNEI DARUSSALAM", + display_name: "Brunei Darussalam", + }, + { + iso_2: "bg", + iso_3: "bgr", + num_code: 100, + name: "BULGARIA", + display_name: "Bulgaria", + }, + { + iso_2: "bf", + iso_3: "bfa", + num_code: 854, + name: "BURKINA FASO", + display_name: "Burkina Faso", + }, + { + iso_2: "bi", + iso_3: "bdi", + num_code: 108, + name: "BURUNDI", + display_name: "Burundi", + }, + { + iso_2: "kh", + iso_3: "khm", + num_code: 116, + name: "CAMBODIA", + display_name: "Cambodia", + }, + { + iso_2: "cm", + iso_3: "cmr", + num_code: 120, + name: "CAMEROON", + display_name: "Cameroon", + }, + { + iso_2: "ca", + iso_3: "can", + num_code: 124, + name: "CANADA", + display_name: "Canada", + }, + { + iso_2: "cv", + iso_3: "cpv", + num_code: 132, + name: "CAPE VERDE", + display_name: "Cape Verde", + }, + { + iso_2: "ky", + iso_3: "cym", + num_code: 136, + name: "CAYMAN ISLANDS", + display_name: "Cayman Islands", + }, + { + iso_2: "cf", + iso_3: "caf", + num_code: 140, + name: "CENTRAL AFRICAN REPUBLIC", + display_name: "Central African Republic", + }, + { + iso_2: "td", + iso_3: "tcd", + num_code: 148, + name: "CHAD", + display_name: "Chad", + }, + { + iso_2: "cl", + iso_3: "chl", + num_code: 152, + name: "CHILE", + display_name: "Chile", + }, + { + iso_2: "cn", + iso_3: "chn", + num_code: 156, + name: "CHINA", + display_name: "China", + }, + { + iso_2: "cx", + iso_3: "cxr", + num_code: 162, + name: "CHRISTMAS ISLAND", + display_name: "Christmas Island", + }, + { + iso_2: "cc", + iso_3: "cck", + num_code: 166, + name: "COCOS (KEELING) ISLANDS", + display_name: "Cocos (Keeling) Islands", + }, + { + iso_2: "co", + iso_3: "col", + num_code: 170, + name: "COLOMBIA", + display_name: "Colombia", + }, + { + iso_2: "km", + iso_3: "com", + num_code: 174, + name: "COMOROS", + display_name: "Comoros", + }, + { + iso_2: "cg", + iso_3: "cog", + num_code: 178, + name: "CONGO", + display_name: "Congo", + }, + { + iso_2: "cd", + iso_3: "cod", + num_code: 180, + name: "CONGO, THE DEMOCRATIC REPUBLIC OF THE", + display_name: "Congo, the Democratic Republic of the", + }, + { + iso_2: "ck", + iso_3: "cok", + num_code: 184, + name: "COOK ISLANDS", + display_name: "Cook Islands", + }, + { + iso_2: "cr", + iso_3: "cri", + num_code: 188, + name: "COSTA RICA", + display_name: "Costa Rica", + }, + { + iso_2: "ci", + iso_3: "civ", + num_code: 384, + name: "COTE D'IVOIRE", + display_name: "Cote D'Ivoire", + }, + { + iso_2: "hr", + iso_3: "hrv", + num_code: 191, + name: "CROATIA", + display_name: "Croatia", + }, + { + iso_2: "cu", + iso_3: "cub", + num_code: 192, + name: "CUBA", + display_name: "Cuba", + }, + { + iso_2: "cw", + iso_3: "cuw", + num_code: 531, + name: "CURAÇAO", + display_name: "Curaçao", + }, + { + iso_2: "cy", + iso_3: "cyp", + num_code: 196, + name: "CYPRUS", + display_name: "Cyprus", + }, + { + iso_2: "cz", + iso_3: "cze", + num_code: 203, + name: "CZECH REPUBLIC", + display_name: "Czech Republic", + }, + { + iso_2: "dk", + iso_3: "dnk", + num_code: 208, + name: "DENMARK", + display_name: "Denmark", + }, + { + iso_2: "dj", + iso_3: "dji", + num_code: 262, + name: "DJIBOUTI", + display_name: "Djibouti", + }, + { + iso_2: "dm", + iso_3: "dma", + num_code: 212, + name: "DOMINICA", + display_name: "Dominica", + }, + { + iso_2: "do", + iso_3: "dom", + num_code: 214, + name: "DOMINICAN REPUBLIC", + display_name: "Dominican Republic", + }, + { + iso_2: "ec", + iso_3: "ecu", + num_code: 218, + name: "ECUADOR", + display_name: "Ecuador", + }, + { + iso_2: "eg", + iso_3: "egy", + num_code: 818, + name: "EGYPT", + display_name: "Egypt", + }, + { + iso_2: "sv", + iso_3: "slv", + num_code: 222, + name: "EL SALVADOR", + display_name: "El Salvador", + }, + { + iso_2: "gq", + iso_3: "gnq", + num_code: 226, + name: "EQUATORIAL GUINEA", + display_name: "Equatorial Guinea", + }, + { + iso_2: "er", + iso_3: "eri", + num_code: 232, + name: "ERITREA", + display_name: "Eritrea", + }, + { + iso_2: "ee", + iso_3: "est", + num_code: 233, + name: "ESTONIA", + display_name: "Estonia", + }, + { + iso_2: "et", + iso_3: "eth", + num_code: 231, + name: "ETHIOPIA", + display_name: "Ethiopia", + }, + { + iso_2: "fk", + iso_3: "flk", + num_code: 238, + name: "FALKLAND ISLANDS (MALVINAS)", + display_name: "Falkland Islands (Malvinas)", + }, + { + iso_2: "fo", + iso_3: "fro", + num_code: 234, + name: "FAROE ISLANDS", + display_name: "Faroe Islands", + }, + { + iso_2: "fj", + iso_3: "fji", + num_code: 242, + name: "FIJI", + display_name: "Fiji", + }, + { + iso_2: "fi", + iso_3: "fin", + num_code: 246, + name: "FINLAND", + display_name: "Finland", + }, + { + iso_2: "fr", + iso_3: "fra", + num_code: 250, + name: "FRANCE", + display_name: "France", + }, + { + iso_2: "gf", + iso_3: "guf", + num_code: 254, + name: "FRENCH GUIANA", + display_name: "French Guiana", + }, + { + iso_2: "pf", + iso_3: "pyf", + num_code: 258, + name: "FRENCH POLYNESIA", + display_name: "French Polynesia", + }, + { + iso_2: "tf", + iso_3: "atf", + num_code: 260, + name: "FRENCH SOUTHERN TERRITORIES", + display_name: "French Southern Territories", + }, + { + iso_2: "ga", + iso_3: "gab", + num_code: 266, + name: "GABON", + display_name: "Gabon", + }, + { + iso_2: "gm", + iso_3: "gmb", + num_code: 270, + name: "GAMBIA", + display_name: "Gambia", + }, + { + iso_2: "ge", + iso_3: "geo", + num_code: 268, + name: "GEORGIA", + display_name: "Georgia", + }, + { + iso_2: "de", + iso_3: "deu", + num_code: 276, + name: "GERMANY", + display_name: "Germany", + }, + { + iso_2: "gh", + iso_3: "gha", + num_code: 288, + name: "GHANA", + display_name: "Ghana", + }, + { + iso_2: "gi", + iso_3: "gib", + num_code: 292, + name: "GIBRALTAR", + display_name: "Gibraltar", + }, + { + iso_2: "gr", + iso_3: "grc", + num_code: 300, + name: "GREECE", + display_name: "Greece", + }, + { + iso_2: "gl", + iso_3: "grl", + num_code: 304, + name: "GREENLAND", + display_name: "Greenland", + }, + { + iso_2: "gd", + iso_3: "grd", + num_code: 308, + name: "GRENADA", + display_name: "Grenada", + }, + { + iso_2: "gp", + iso_3: "glp", + num_code: 312, + name: "GUADELOUPE", + display_name: "Guadeloupe", + }, + { + iso_2: "gu", + iso_3: "gum", + num_code: 316, + name: "GUAM", + display_name: "Guam", + }, + { + iso_2: "gt", + iso_3: "gtm", + num_code: 320, + name: "GUATEMALA", + display_name: "Guatemala", + }, + { + iso_2: "gg", + iso_3: "ggy", + num_code: 831, + name: "GUERNSEY", + display_name: "Guernsey", + }, + { + iso_2: "gn", + iso_3: "gin", + num_code: 324, + name: "GUINEA", + display_name: "Guinea", + }, + { + iso_2: "gw", + iso_3: "gnb", + num_code: 624, + name: "GUINEA-BISSAU", + display_name: "Guinea-Bissau", + }, + { + iso_2: "gy", + iso_3: "guy", + num_code: 328, + name: "GUYANA", + display_name: "Guyana", + }, + { + iso_2: "ht", + iso_3: "hti", + num_code: 332, + name: "HAITI", + display_name: "Haiti", + }, + { + iso_2: "hm", + iso_3: "hmd", + num_code: 334, + name: "HEARD ISLAND AND MCDONALD ISLANDS", + display_name: "Heard Island And Mcdonald Islands", + }, + { + iso_2: "va", + iso_3: "vat", + num_code: 336, + name: "HOLY SEE (VATICAN CITY STATE)", + display_name: "Holy See (Vatican City State)", + }, + { + iso_2: "hn", + iso_3: "hnd", + num_code: 340, + name: "HONDURAS", + display_name: "Honduras", + }, + { + iso_2: "hk", + iso_3: "hkg", + num_code: 344, + name: "HONG KONG", + display_name: "Hong Kong", + }, + { + iso_2: "hu", + iso_3: "hun", + num_code: 348, + name: "HUNGARY", + display_name: "Hungary", + }, + { + iso_2: "is", + iso_3: "isl", + num_code: 352, + name: "ICELAND", + display_name: "Iceland", + }, + { + iso_2: "in", + iso_3: "ind", + num_code: 356, + name: "INDIA", + display_name: "India", + }, + { + iso_2: "id", + iso_3: "idn", + num_code: 360, + name: "INDONESIA", + display_name: "Indonesia", + }, + { + iso_2: "ir", + iso_3: "irn", + num_code: 364, + name: "IRAN, ISLAMIC REPUBLIC OF", + display_name: "Iran, Islamic Republic of", + }, + { + iso_2: "iq", + iso_3: "irq", + num_code: 368, + name: "IRAQ", + display_name: "Iraq", + }, + { + iso_2: "ie", + iso_3: "irl", + num_code: 372, + name: "IRELAND", + display_name: "Ireland", + }, + { + iso_2: "im", + iso_3: "imn", + num_code: 833, + name: "ISLE OF MAN", + display_name: "Isle Of Man", + }, + { + iso_2: "il", + iso_3: "isr", + num_code: 376, + name: "ISRAEL", + display_name: "Israel", + }, + { + iso_2: "it", + iso_3: "ita", + num_code: 380, + name: "ITALY", + display_name: "Italy", + }, + { + iso_2: "jm", + iso_3: "jam", + num_code: 388, + name: "JAMAICA", + display_name: "Jamaica", + }, + { + iso_2: "jp", + iso_3: "jpn", + num_code: 392, + name: "JAPAN", + display_name: "Japan", + }, + { + iso_2: "je", + iso_3: "jey", + num_code: 832, + name: "JERSEY", + display_name: "Jersey", + }, + { + iso_2: "jo", + iso_3: "jor", + num_code: 400, + name: "JORDAN", + display_name: "Jordan", + }, + { + iso_2: "kz", + iso_3: "kaz", + num_code: 398, + name: "KAZAKHSTAN", + display_name: "Kazakhstan", + }, + { + iso_2: "ke", + iso_3: "ken", + num_code: 404, + name: "KENYA", + display_name: "Kenya", + }, + { + iso_2: "ki", + iso_3: "kir", + num_code: 296, + name: "KIRIBATI", + display_name: "Kiribati", + }, + { + iso_2: "kp", + iso_3: "prk", + num_code: 408, + name: "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF", + display_name: "Korea, Democratic People's Republic of", + }, + { + iso_2: "kr", + iso_3: "kor", + num_code: 410, + name: "KOREA, REPUBLIC OF", + display_name: "Korea, Republic of", + }, + { + iso_2: "xk", + iso_3: "xkx", + num_code: 900, + name: "KOSOVO", + display_name: "Kosovo", + }, + { + iso_2: "kw", + iso_3: "kwt", + num_code: 414, + name: "KUWAIT", + display_name: "Kuwait", + }, + { + iso_2: "kg", + iso_3: "kgz", + num_code: 417, + name: "KYRGYZSTAN", + display_name: "Kyrgyzstan", + }, + { + iso_2: "la", + iso_3: "lao", + num_code: 418, + name: "LAO PEOPLE'S DEMOCRATIC REPUBLIC", + display_name: "Lao People's Democratic Republic", + }, + { + iso_2: "lv", + iso_3: "lva", + num_code: 428, + name: "LATVIA", + display_name: "Latvia", + }, + { + iso_2: "lb", + iso_3: "lbn", + num_code: 422, + name: "LEBANON", + display_name: "Lebanon", + }, + { + iso_2: "ls", + iso_3: "lso", + num_code: 426, + name: "LESOTHO", + display_name: "Lesotho", + }, + { + iso_2: "lr", + iso_3: "lbr", + num_code: 430, + name: "LIBERIA", + display_name: "Liberia", + }, + { + iso_2: "ly", + iso_3: "lby", + num_code: 434, + name: "LIBYAN ARAB JAMAHIRIYA", + display_name: "Libyan Arab Jamahiriya", + }, + { + iso_2: "li", + iso_3: "lie", + num_code: 438, + name: "LIECHTENSTEIN", + display_name: "Liechtenstein", + }, + { + iso_2: "lt", + iso_3: "ltu", + num_code: 440, + name: "LITHUANIA", + display_name: "Lithuania", + }, + { + iso_2: "lu", + iso_3: "lux", + num_code: 442, + name: "LUXEMBOURG", + display_name: "Luxembourg", + }, + { + iso_2: "mo", + iso_3: "mac", + num_code: 446, + name: "MACAO", + display_name: "Macao", + }, + { + iso_2: "mk", + iso_3: "mkd", + num_code: 807, + name: "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF", + display_name: "Macedonia, the Former Yugoslav Republic of", + }, + { + iso_2: "mg", + iso_3: "mdg", + num_code: 450, + name: "MADAGASCAR", + display_name: "Madagascar", + }, + { + iso_2: "mw", + iso_3: "mwi", + num_code: 454, + name: "MALAWI", + display_name: "Malawi", + }, + { + iso_2: "my", + iso_3: "mys", + num_code: 458, + name: "MALAYSIA", + display_name: "Malaysia", + }, + { + iso_2: "mv", + iso_3: "mdv", + num_code: 462, + name: "MALDIVES", + display_name: "Maldives", + }, + { + iso_2: "ml", + iso_3: "mli", + num_code: 466, + name: "MALI", + display_name: "Mali", + }, + { + iso_2: "mt", + iso_3: "mlt", + num_code: 470, + name: "MALTA", + display_name: "Malta", + }, + { + iso_2: "mh", + iso_3: "mhl", + num_code: 584, + name: "MARSHALL ISLANDS", + display_name: "Marshall Islands", + }, + { + iso_2: "mq", + iso_3: "mtq", + num_code: 474, + name: "MARTINIQUE", + display_name: "Martinique", + }, + { + iso_2: "mr", + iso_3: "mrt", + num_code: 478, + name: "MAURITANIA", + display_name: "Mauritania", + }, + { + iso_2: "mu", + iso_3: "mus", + num_code: 480, + name: "MAURITIUS", + display_name: "Mauritius", + }, + { + iso_2: "yt", + iso_3: "myt", + num_code: 175, + name: "MAYOTTE", + display_name: "Mayotte", + }, + { + iso_2: "mx", + iso_3: "mex", + num_code: 484, + name: "MEXICO", + display_name: "Mexico", + }, + { + iso_2: "fm", + iso_3: "fsm", + num_code: 583, + name: "MICRONESIA, FEDERATED STATES OF", + display_name: "Micronesia, Federated States of", + }, + { + iso_2: "md", + iso_3: "mda", + num_code: 498, + name: "MOLDOVA, REPUBLIC OF", + display_name: "Moldova, Republic of", + }, + { + iso_2: "mc", + iso_3: "mco", + num_code: 492, + name: "MONACO", + display_name: "Monaco", + }, + { + iso_2: "mn", + iso_3: "mng", + num_code: 496, + name: "MONGOLIA", + display_name: "Mongolia", + }, + { + iso_2: "me", + iso_3: "mne", + num_code: 499, + name: "MONTENEGRO", + display_name: "Montenegro", + }, + { + iso_2: "ms", + iso_3: "msr", + num_code: 500, + name: "MONTSERRAT", + display_name: "Montserrat", + }, + { + iso_2: "ma", + iso_3: "mar", + num_code: 504, + name: "MOROCCO", + display_name: "Morocco", + }, + { + iso_2: "mz", + iso_3: "moz", + num_code: 508, + name: "MOZAMBIQUE", + display_name: "Mozambique", + }, + { + iso_2: "mm", + iso_3: "mmr", + num_code: 104, + name: "MYANMAR", + display_name: "Myanmar", + }, + { + iso_2: "na", + iso_3: "nam", + num_code: 516, + name: "NAMIBIA", + display_name: "Namibia", + }, + { + iso_2: "nr", + iso_3: "nru", + num_code: 520, + name: "NAURU", + display_name: "Nauru", + }, + { + iso_2: "np", + iso_3: "npl", + num_code: 524, + name: "NEPAL", + display_name: "Nepal", + }, + { + iso_2: "nl", + iso_3: "nld", + num_code: 528, + name: "NETHERLANDS", + display_name: "Netherlands", + }, + { + iso_2: "nc", + iso_3: "ncl", + num_code: 540, + name: "NEW CALEDONIA", + display_name: "New Caledonia", + }, + { + iso_2: "nz", + iso_3: "nzl", + num_code: 554, + name: "NEW ZEALAND", + display_name: "New Zealand", + }, + { + iso_2: "ni", + iso_3: "nic", + num_code: 558, + name: "NICARAGUA", + display_name: "Nicaragua", + }, + { + iso_2: "ne", + iso_3: "ner", + num_code: 562, + name: "NIGER", + display_name: "Niger", + }, + { + iso_2: "ng", + iso_3: "nga", + num_code: 566, + name: "NIGERIA", + display_name: "Nigeria", + }, + { + iso_2: "nu", + iso_3: "niu", + num_code: 570, + name: "NIUE", + display_name: "Niue", + }, + { + iso_2: "nf", + iso_3: "nfk", + num_code: 574, + name: "NORFOLK ISLAND", + display_name: "Norfolk Island", + }, + { + iso_2: "mp", + iso_3: "mnp", + num_code: 580, + name: "NORTHERN MARIANA ISLANDS", + display_name: "Northern Mariana Islands", + }, + { + iso_2: "no", + iso_3: "nor", + num_code: 578, + name: "NORWAY", + display_name: "Norway", + }, + { + iso_2: "om", + iso_3: "omn", + num_code: 512, + name: "OMAN", + display_name: "Oman", + }, + { + iso_2: "pk", + iso_3: "pak", + num_code: 586, + name: "PAKISTAN", + display_name: "Pakistan", + }, + { + iso_2: "pw", + iso_3: "plw", + num_code: 585, + name: "PALAU", + display_name: "Palau", + }, + { + iso_2: "ps", + iso_3: "pse", + num_code: 275, + name: "PALESTINIAN TERRITORY, OCCUPIED", + display_name: "Palestinian Territory, Occupied", + }, + { + iso_2: "pa", + iso_3: "pan", + num_code: 591, + name: "PANAMA", + display_name: "Panama", + }, + { + iso_2: "pg", + iso_3: "png", + num_code: 598, + name: "PAPUA NEW GUINEA", + display_name: "Papua New Guinea", + }, + { + iso_2: "py", + iso_3: "pry", + num_code: 600, + name: "PARAGUAY", + display_name: "Paraguay", + }, + { + iso_2: "pe", + iso_3: "per", + num_code: 604, + name: "PERU", + display_name: "Peru", + }, + { + iso_2: "ph", + iso_3: "phl", + num_code: 608, + name: "PHILIPPINES", + display_name: "Philippines", + }, + { + iso_2: "pn", + iso_3: "pcn", + num_code: 612, + name: "PITCAIRN", + display_name: "Pitcairn", + }, + { + iso_2: "pl", + iso_3: "pol", + num_code: 616, + name: "POLAND", + display_name: "Poland", + }, + { + iso_2: "pt", + iso_3: "prt", + num_code: 620, + name: "PORTUGAL", + display_name: "Portugal", + }, + { + iso_2: "pr", + iso_3: "pri", + num_code: 630, + name: "PUERTO RICO", + display_name: "Puerto Rico", + }, + { + iso_2: "qa", + iso_3: "qat", + num_code: 634, + name: "QATAR", + display_name: "Qatar", + }, + { + iso_2: "re", + iso_3: "reu", + num_code: 638, + name: "REUNION", + display_name: "Reunion", + }, + { + iso_2: "ro", + iso_3: "rom", + num_code: 642, + name: "ROMANIA", + display_name: "Romania", + }, + { + iso_2: "ru", + iso_3: "rus", + num_code: 643, + name: "RUSSIAN FEDERATION", + display_name: "Russian Federation", + }, + { + iso_2: "rw", + iso_3: "rwa", + num_code: 646, + name: "RWANDA", + display_name: "Rwanda", + }, + { + iso_2: "bl", + iso_3: "blm", + num_code: 652, + name: "SAINT BARTHÉLEMY", + display_name: "Saint Barthélemy", + }, + { + iso_2: "sh", + iso_3: "shn", + num_code: 654, + name: "SAINT HELENA", + display_name: "Saint Helena", + }, + { + iso_2: "kn", + iso_3: "kna", + num_code: 659, + name: "SAINT KITTS AND NEVIS", + display_name: "Saint Kitts and Nevis", + }, + { + iso_2: "lc", + iso_3: "lca", + num_code: 662, + name: "SAINT LUCIA", + display_name: "Saint Lucia", + }, + { + iso_2: "mf", + iso_3: "maf", + num_code: 663, + name: "SAINT MARTIN (FRENCH PART)", + display_name: "Saint Martin (French part)", + }, + { + iso_2: "pm", + iso_3: "spm", + num_code: 666, + name: "SAINT PIERRE AND MIQUELON", + display_name: "Saint Pierre and Miquelon", + }, + { + iso_2: "vc", + iso_3: "vct", + num_code: 670, + name: "SAINT VINCENT AND THE GRENADINES", + display_name: "Saint Vincent and the Grenadines", + }, + { + iso_2: "ws", + iso_3: "wsm", + num_code: 882, + name: "SAMOA", + display_name: "Samoa", + }, + { + iso_2: "sm", + iso_3: "smr", + num_code: 674, + name: "SAN MARINO", + display_name: "San Marino", + }, + { + iso_2: "st", + iso_3: "stp", + num_code: 678, + name: "SAO TOME AND PRINCIPE", + display_name: "Sao Tome and Principe", + }, + { + iso_2: "sa", + iso_3: "sau", + num_code: 682, + name: "SAUDI ARABIA", + display_name: "Saudi Arabia", + }, + { + iso_2: "sn", + iso_3: "sen", + num_code: 686, + name: "SENEGAL", + display_name: "Senegal", + }, + { + iso_2: "rs", + iso_3: "srb", + num_code: 688, + name: "SERBIA", + display_name: "Serbia", + }, + { + iso_2: "sc", + iso_3: "syc", + num_code: 690, + name: "SEYCHELLES", + display_name: "Seychelles", + }, + { + iso_2: "sl", + iso_3: "sle", + num_code: 694, + name: "SIERRA LEONE", + display_name: "Sierra Leone", + }, + { + iso_2: "sg", + iso_3: "sgp", + num_code: 702, + name: "SINGAPORE", + display_name: "Singapore", + }, + { + iso_2: "sx", + iso_3: "sxm", + num_code: 534, + name: "SINT MAARTEN", + display_name: "Sint Maarten", + }, + { + iso_2: "sk", + iso_3: "svk", + num_code: 703, + name: "SLOVAKIA", + display_name: "Slovakia", + }, + { + iso_2: "si", + iso_3: "svn", + num_code: 705, + name: "SLOVENIA", + display_name: "Slovenia", + }, + { + iso_2: "sb", + iso_3: "slb", + num_code: 90, + name: "SOLOMON ISLANDS", + display_name: "Solomon Islands", + }, + { + iso_2: "so", + iso_3: "som", + num_code: 706, + name: "SOMALIA", + display_name: "Somalia", + }, + { + iso_2: "za", + iso_3: "zaf", + num_code: 710, + name: "SOUTH AFRICA", + display_name: "South Africa", + }, + { + iso_2: "gs", + iso_3: "sgs", + num_code: 239, + name: "SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS", + display_name: "South Georgia and the South Sandwich Islands", + }, + { + iso_2: "ss", + iso_3: "ssd", + num_code: 728, + name: "SOUTH SUDAN", + display_name: "South Sudan", + }, + { + iso_2: "es", + iso_3: "esp", + num_code: 724, + name: "SPAIN", + display_name: "Spain", + }, + { + iso_2: "lk", + iso_3: "lka", + num_code: 144, + name: "SRI LANKA", + display_name: "Sri Lanka", + }, + { + iso_2: "sd", + iso_3: "sdn", + num_code: 729, + name: "SUDAN", + display_name: "Sudan", + }, + { + iso_2: "sr", + iso_3: "sur", + num_code: 740, + name: "SURINAME", + display_name: "Suriname", + }, + { + iso_2: "sj", + iso_3: "sjm", + num_code: 744, + name: "SVALBARD AND JAN MAYEN", + display_name: "Svalbard and Jan Mayen", + }, + { + iso_2: "sz", + iso_3: "swz", + num_code: 748, + name: "SWAZILAND", + display_name: "Swaziland", + }, + { + iso_2: "se", + iso_3: "swe", + num_code: 752, + name: "SWEDEN", + display_name: "Sweden", + }, + { + iso_2: "ch", + iso_3: "che", + num_code: 756, + name: "SWITZERLAND", + display_name: "Switzerland", + }, + { + iso_2: "sy", + iso_3: "syr", + num_code: 760, + name: "SYRIAN ARAB REPUBLIC", + display_name: "Syrian Arab Republic", + }, + { + iso_2: "tw", + iso_3: "twn", + num_code: 158, + name: "TAIWAN, PROVINCE OF CHINA", + display_name: "Taiwan, Province of China", + }, + { + iso_2: "tj", + iso_3: "tjk", + num_code: 762, + name: "TAJIKISTAN", + display_name: "Tajikistan", + }, + { + iso_2: "tz", + iso_3: "tza", + num_code: 834, + name: "TANZANIA, UNITED REPUBLIC OF", + display_name: "Tanzania, United Republic of", + }, + { + iso_2: "th", + iso_3: "tha", + num_code: 764, + name: "THAILAND", + display_name: "Thailand", + }, + { + iso_2: "tl", + iso_3: "tls", + num_code: 626, + name: "TIMOR LESTE", + display_name: "Timor Leste", + }, + { + iso_2: "tg", + iso_3: "tgo", + num_code: 768, + name: "TOGO", + display_name: "Togo", + }, + { + iso_2: "tk", + iso_3: "tkl", + num_code: 772, + name: "TOKELAU", + display_name: "Tokelau", + }, + { + iso_2: "to", + iso_3: "ton", + num_code: 776, + name: "TONGA", + display_name: "Tonga", + }, + { + iso_2: "tt", + iso_3: "tto", + num_code: 780, + name: "TRINIDAD AND TOBAGO", + display_name: "Trinidad and Tobago", + }, + { + iso_2: "tn", + iso_3: "tun", + num_code: 788, + name: "TUNISIA", + display_name: "Tunisia", + }, + { + iso_2: "tr", + iso_3: "tur", + num_code: 792, + name: "TURKEY", + display_name: "Turkey", + }, + { + iso_2: "tm", + iso_3: "tkm", + num_code: 795, + name: "TURKMENISTAN", + display_name: "Turkmenistan", + }, + { + iso_2: "tc", + iso_3: "tca", + num_code: 796, + name: "TURKS AND CAICOS ISLANDS", + display_name: "Turks and Caicos Islands", + }, + { + iso_2: "tv", + iso_3: "tuv", + num_code: 798, + name: "TUVALU", + display_name: "Tuvalu", + }, + { + iso_2: "ug", + iso_3: "uga", + num_code: 800, + name: "UGANDA", + display_name: "Uganda", + }, + { + iso_2: "ua", + iso_3: "ukr", + num_code: 804, + name: "UKRAINE", + display_name: "Ukraine", + }, + { + iso_2: "ae", + iso_3: "are", + num_code: 784, + name: "UNITED ARAB EMIRATES", + display_name: "United Arab Emirates", + }, + { + iso_2: "gb", + iso_3: "gbr", + num_code: 826, + name: "UNITED KINGDOM", + display_name: "United Kingdom", + }, + { + iso_2: "us", + iso_3: "usa", + num_code: 840, + name: "UNITED STATES", + display_name: "United States", + }, + { + iso_2: "um", + iso_3: "umi", + num_code: 581, + name: "UNITED STATES MINOR OUTLYING ISLANDS", + display_name: "United States Minor Outlying Islands", + }, + { + iso_2: "uy", + iso_3: "ury", + num_code: 858, + name: "URUGUAY", + display_name: "Uruguay", + }, + { + iso_2: "uz", + iso_3: "uzb", + num_code: 860, + name: "UZBEKISTAN", + display_name: "Uzbekistan", + }, + { + iso_2: "vu", + iso_3: "vut", + num_code: 548, + name: "VANUATU", + display_name: "Vanuatu", + }, + { + iso_2: "ve", + iso_3: "ven", + num_code: 862, + name: "VENEZUELA", + display_name: "Venezuela", + }, + { + iso_2: "vn", + iso_3: "vnm", + num_code: 704, + name: "VIET NAM", + display_name: "Viet Nam", + }, + { + iso_2: "vg", + iso_3: "vgb", + num_code: 92, + name: "VIRGIN ISLANDS, BRITISH", + display_name: "Virgin Islands, British", + }, + { + iso_2: "vi", + iso_3: "vir", + num_code: 850, + name: "VIRGIN ISLANDS, U.S.", + display_name: "Virgin Islands, U.S.", + }, + { + iso_2: "wf", + iso_3: "wlf", + num_code: 876, + name: "WALLIS AND FUTUNA", + display_name: "Wallis and Futuna", + }, + { + iso_2: "eh", + iso_3: "esh", + num_code: 732, + name: "WESTERN SAHARA", + display_name: "Western Sahara", + }, + { + iso_2: "ye", + iso_3: "yem", + num_code: 887, + name: "YEMEN", + display_name: "Yemen", + }, + { + iso_2: "zm", + iso_3: "zmb", + num_code: 894, + name: "ZAMBIA", + display_name: "Zambia", + }, + { + iso_2: "zw", + iso_3: "zwe", + num_code: 716, + name: "ZIMBABWE", + display_name: "Zimbabwe", + }, + { + iso_2: "ax", + iso_3: "ala", + num_code: 248, + name: "ÅLAND ISLANDS", + display_name: "Åland Islands", + }, +] diff --git a/packages/admin-next/dashboard/src/lib/debounce.ts b/packages/admin-next/dashboard/src/lib/debounce.ts new file mode 100644 index 0000000000..3ac6187085 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/debounce.ts @@ -0,0 +1,20 @@ +type Options = { + leading?: boolean +} + +export const debounce = ( + func: (...args: any[]) => void, + delay: number, + { leading }: Options = {} +): ((...args: any[]) => void) => { + let timerId: NodeJS.Timeout | undefined + + return (...args) => { + if (!timerId && leading) { + func(...args) + } + clearTimeout(timerId) + + timerId = setTimeout(() => func(...args), delay) + } +} diff --git a/packages/admin-next/dashboard/src/lib/is-axios-error.ts b/packages/admin-next/dashboard/src/lib/is-axios-error.ts new file mode 100644 index 0000000000..1d8cba11f8 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/is-axios-error.ts @@ -0,0 +1,5 @@ +import type { AxiosError } from "axios" + +export const isAxiosError = (error: any): error is AxiosError => { + return error.isAxiosError +} diff --git a/packages/admin-next/dashboard/src/lib/medusa.ts b/packages/admin-next/dashboard/src/lib/medusa.ts index 5b3a4f4abe..1edaff40b1 100644 --- a/packages/admin-next/dashboard/src/lib/medusa.ts +++ b/packages/admin-next/dashboard/src/lib/medusa.ts @@ -1,6 +1,9 @@ import Medusa from "@medusajs/medusa-js" import { QueryClient } from "@tanstack/react-query" +export const MEDUSA_BACKEND_URL = + import.meta.env.VITE_MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000" + export const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -12,6 +15,6 @@ export const queryClient = new QueryClient({ }) export const medusa = new Medusa({ - baseUrl: "http://localhost:9000", - maxRetries: 3, + baseUrl: MEDUSA_BACKEND_URL, + maxRetries: 1, }) diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx b/packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx deleted file mode 100644 index 4068ea01d8..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/auth-context.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { AdminAuthRes, User } from "@medusajs/medusa" -import { createContext } from "react" - -type AuthContextValue = { - login: (email: string, password: string) => Promise - user: Omit | null - isLoading: boolean -} - -export const AuthContext = createContext(null) diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx b/packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx deleted file mode 100644 index ddb2589cd4..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/auth-provider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useAdminGetSession, useAdminLogin } from "medusa-react" -import { PropsWithChildren } from "react" -import { AuthContext } from "./auth-context" - -export const AuthProvider = ({ children }: PropsWithChildren) => { - const { mutateAsync: loginMutation } = useAdminLogin() - const { user, isLoading } = useAdminGetSession() - - const login = async (email: string, password: string) => { - return await loginMutation({ email, password }) - } - - return ( - - {children} - - ) -} diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/index.ts b/packages/admin-next/dashboard/src/providers/auth-provider/index.ts deleted file mode 100644 index c2b66aa1c0..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./auth-provider"; -export * from "./use-auth"; diff --git a/packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx b/packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx deleted file mode 100644 index 8bef3683ca..0000000000 --- a/packages/admin-next/dashboard/src/providers/auth-provider/use-auth.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -import { AuthContext } from "./auth-context"; - -export const useAuth = () => { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within an AuthProvider"); - } - return context; -}; diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index ae5b05bfb8..34918b0f9c 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -1,38 +1,35 @@ +import type { AdminProductsRes, AdminRegionsRes } from "@medusajs/medusa" import { + Outlet, RouterProvider as Provider, - RouteObject, createBrowserRouter, } from "react-router-dom" -import { RequireAuth } from "../../components/authentication/require-auth" -import { AppLayout } from "../../components/layout/app-layout" -import { PublicLayout } from "../../components/layout/public-layout" - -import { AdminProductsRes } from "@medusajs/medusa" -import routes from "medusa-admin:routes/pages" -import settings from "medusa-admin:settings/pages" +import { ProtectedRoute } from "../../components/authentication/require-auth" import { ErrorBoundary } from "../../components/error/error-boundary" -import { SearchProvider } from "../search-provider" +import { MainLayout } from "../../components/layout/main-layout" +import { PublicLayout } from "../../components/layout/public-layout" +import { SettingsLayout } from "../../components/layout/settings-layout" -const routeExtensions: RouteObject[] = routes.pages.map((ext) => { - return { - path: ext.path, - async lazy() { - const { default: Component } = await import(/* @vite-ignore */ ext.file) - return { Component } - }, - } -}) +// const routeExtensions: RouteObject[] = routes.pages.map((ext) => { +// return { +// path: ext.path, +// async lazy() { +// const { default: Component } = await import(/* @vite-ignore */ ext.file) +// return { Component } +// }, +// } +// }) -const settingsExtensions: RouteObject[] = settings.pages.map((ext) => { - return { - path: `/settings${ext.path}`, - async lazy() { - const { default: Component } = await import(/* @vite-ignore */ ext.file) - return { Component } - }, - } -}) +// const settingsExtensions: RouteObject[] = settings.pages.map((ext) => { +// return { +// path: `/settings${ext.path}`, +// async lazy() { +// const { default: Component } = await import(/* @vite-ignore */ ext.file) +// return { Component } +// }, +// } +// }) const router = createBrowserRouter([ { @@ -45,161 +42,192 @@ const router = createBrowserRouter([ ], }, { - element: ( - - - - - - ), + element: , errorElement: , children: [ { path: "/", - lazy: () => import("../../routes/home"), - }, - { - path: "/orders", + element: , children: [ { index: true, - lazy: () => import("../../routes/orders/list"), + lazy: () => import("../../routes/home"), }, { - path: ":id", - lazy: () => import("../../routes/orders/details"), - }, - ], - }, - { - path: "/draft-orders", - children: [ - { - index: true, - lazy: () => import("../../routes/draft-orders/list"), - }, - { - path: ":id", - lazy: () => import("../../routes/draft-orders/details"), - }, - ], - }, - { - path: "/products", - handle: { - crumb: () => "Products", - }, - children: [ - { - index: true, - lazy: () => import("../../routes/products/views/product-list"), - }, - { - path: ":id", - lazy: () => import("../../routes/products/views/product-details"), + path: "/orders", handle: { - crumb: (data: AdminProductsRes) => data.product.title, + crumb: () => "Orders", }, - }, - ], - }, - { - path: "/categories", - children: [ - { - index: true, - lazy: () => import("../../routes/categories/list"), + children: [ + { + index: true, + lazy: () => import("../../routes/orders/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/orders/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/categories/details"), - }, - ], - }, - { - path: "/collections", - children: [ - { - index: true, - lazy: () => import("../../routes/collections/list"), + path: "/draft-orders", + handle: { + crumb: () => "Draft Orders", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/draft-orders/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/draft-orders/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/collections/details"), - }, - ], - }, - { - path: "/customers", - children: [ - { - index: true, - lazy: () => import("../../routes/customers/list"), + path: "/products", + handle: { + crumb: () => "Products", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/products/product-list"), + }, + { + path: ":id", + lazy: () => import("../../routes/products/product-detail"), + handle: { + crumb: (data: AdminProductsRes) => data.product.title, + }, + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/customers/details"), - }, - ], - }, - { - path: "/customer-groups", - children: [ - { - index: true, - lazy: () => import("../../routes/customer-groups/list"), + path: "/categories", + handle: { + crumb: () => "Categories", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/categories/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/categories/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/customer-groups/details"), - }, - ], - }, - { - path: "/gift-cards", - children: [ - { - index: true, - lazy: () => import("../../routes/gift-cards/list"), + path: "/collections", + handle: { + crumb: () => "Collections", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/collections/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/collections/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/gift-cards/details"), - }, - ], - }, - { - path: "/inventory", - lazy: () => import("../../routes/inventory/list"), - }, - { - path: "/discounts", - children: [ - { - index: true, - lazy: () => import("../../routes/discounts/list"), + path: "/customers", + handle: { + crumb: () => "Customers", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/customers/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/customers/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/discounts/details"), - }, - ], - }, - { - path: "/pricing", - children: [ - { - index: true, - lazy: () => import("../../routes/pricing/list"), + path: "/customer-groups", + handle: { + crumb: () => "Customer Groups", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/customer-groups/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/customer-groups/details"), + }, + ], }, { - path: ":id", - lazy: () => import("../../routes/pricing/details"), + path: "/gift-cards", + handle: { + crumb: () => "Gift Cards", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/gift-cards/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/gift-cards/details"), + }, + ], + }, + { + path: "/inventory", + handle: { + crumb: () => "Inventory", + }, + lazy: () => import("../../routes/inventory/list"), + }, + { + path: "/discounts", + handle: { + crumb: () => "Discounts", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/discounts/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/discounts/details"), + }, + ], + }, + { + path: "/pricing", + handle: { + crumb: () => "Pricing", + }, + children: [ + { + index: true, + lazy: () => import("../../routes/pricing/list"), + }, + { + path: ":id", + lazy: () => import("../../routes/pricing/details"), + }, + ], }, ], }, { path: "/settings", + element: , handle: { crumb: () => "Settings", }, @@ -210,52 +238,131 @@ const router = createBrowserRouter([ }, { path: "profile", - lazy: () => import("../../routes/profile/views/profile-details"), + lazy: () => import("../../routes/profile/profile-detail"), handle: { crumb: () => "Profile", }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/profile/profile-edit"), + }, + ], }, { path: "store", - lazy: () => import("../../routes/store/views/store-details"), + lazy: () => import("../../routes/store/store-detail"), handle: { crumb: () => "Store", }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/store/store-edit"), + }, + { + path: "add-currencies", + lazy: () => import("../../routes/store/store-add-currencies"), + }, + ], }, { path: "locations", - lazy: () => import("../../routes/locations/list"), + element: , + handle: { + crumb: () => "Locations", + }, + children: [ + { + path: "", + lazy: () => import("../../routes/locations/location-list"), + children: [ + { + path: "create", + lazy: () => + import("../../routes/locations/location-create"), + }, + ], + }, + { + path: ":id", + lazy: () => import("../../routes/locations/location-detail"), + children: [ + { + path: "edit", + lazy: () => import("../../routes/locations/location-edit"), + }, + { + path: "add-sales-channels", + lazy: () => + import( + "../../routes/locations/location-add-sales-channels" + ), + }, + ], + }, + ], }, { path: "regions", + element: , handle: { crumb: () => "Regions", }, children: [ { - index: true, - lazy: () => import("../../routes/regions/views/region-list"), + path: "", + lazy: () => import("../../routes/regions/region-list"), + children: [ + { + path: "create", + lazy: () => import("../../routes/regions/region-create"), + }, + ], }, { path: ":id", - lazy: () => import("../../routes/regions/views/region-details"), + lazy: () => import("../../routes/regions/region-detail"), + handle: { + crumb: (data: AdminRegionsRes) => data.region.name, + }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/regions/region-edit"), + }, + ], }, ], }, { path: "users", - lazy: () => import("../../routes/users"), + element: , handle: { crumb: () => "Users", }, - }, - { - path: "currencies", - lazy: () => - import("../../routes/currencies/views/currencies-details"), - handle: { - crumb: () => "Currencies", - }, + children: [ + { + path: "", + lazy: () => import("../../routes/users/user-list"), + children: [ + { + path: "invite", + lazy: () => import("../../routes/users/user-invite"), + }, + ], + }, + { + path: ":id", + lazy: () => import("../../routes/users/user-detail"), + children: [ + { + path: "edit", + lazy: () => import("../../routes/users/user-edit"), + }, + ], + }, + ], }, { path: "taxes", @@ -275,37 +382,91 @@ const router = createBrowserRouter([ }, { path: "sales-channels", + element: , handle: { crumb: () => "Sales Channels", }, children: [ { - index: true, + path: "", lazy: () => - import( - "../../routes/sales-channels/views/sales-channel-list" - ), + import("../../routes/sales-channels/sales-channel-list"), + children: [ + { + path: "create", + lazy: () => + import( + "../../routes/sales-channels/sales-channel-create" + ), + }, + ], }, { path: ":id", lazy: () => - import( - "../../routes/sales-channels/views/sales-channel-details" - ), + import("../../routes/sales-channels/sales-channel-detail"), + children: [ + { + path: "edit", + lazy: () => + import("../../routes/sales-channels/sales-channel-edit"), + }, + { + path: "add-products", + lazy: () => + import( + "../../routes/sales-channels/sales-channel-add-products" + ), + }, + ], }, ], }, { path: "api-key-management", - lazy: () => import("../../routes/api-key-management"), + element: , handle: { crumb: () => "API Key Management", }, + children: [ + { + path: "", + lazy: () => + import( + "../../routes/api-key-management/api-key-management-list" + ), + children: [ + { + path: "create", + lazy: () => + import( + "../../routes/api-key-management/api-key-management-create" + ), + }, + ], + }, + { + path: ":id", + lazy: () => + import( + "../../routes/api-key-management/api-key-management-detail" + ), + children: [ + { + path: "edit", + lazy: () => + import( + "../../routes/api-key-management/api-key-management-edit" + ), + }, + ], + }, + ], }, - ...settingsExtensions, + // ...settingsExtensions, ], }, - ...routeExtensions, + // ...routeExtensions, ], }, { diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts b/packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts new file mode 100644 index 0000000000..abb41fa819 --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/index.ts @@ -0,0 +1,2 @@ +export * from "./sidebar-provider" +export * from "./use-sidebar" diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx new file mode 100644 index 0000000000..23d0e7298f --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-context.tsx @@ -0,0 +1,9 @@ +import { createContext } from "react" + +type SidebarContextValue = { + desktop: boolean + mobile: boolean + toggle: (view: "desktop" | "mobile") => void +} + +export const SidebarContext = createContext(null) diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx new file mode 100644 index 0000000000..e9013d2bdf --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/sidebar-provider.tsx @@ -0,0 +1,21 @@ +import { PropsWithChildren, useState } from "react" +import { SidebarContext } from "./sidebar-context" + +export const SidebarProvider = ({ children }: PropsWithChildren) => { + const [desktop, setDesktop] = useState(true) + const [mobile, setMobile] = useState(false) + + const toggle = (view: "desktop" | "mobile") => { + if (view === "desktop") { + setDesktop(!desktop) + } else { + setMobile(!mobile) + } + } + + return ( + + {children} + + ) +} diff --git a/packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx b/packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx new file mode 100644 index 0000000000..21cb708e15 --- /dev/null +++ b/packages/admin-next/dashboard/src/providers/sidebar-provider/use-sidebar.tsx @@ -0,0 +1,12 @@ +import { useContext } from "react" +import { SidebarContext } from "./sidebar-context" + +export const useSidebar = () => { + const context = useContext(SidebarContext) + + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider") + } + + return context +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx new file mode 100644 index 0000000000..ef7c98898e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/api-key-management-create.tsx @@ -0,0 +1,15 @@ +import { FocusModal } from "@medusajs/ui" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { CreatePublishableApiKeyForm } from "./components/create-publishable-api-key-form" + +export const ApiKeyManagementCreate = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx new file mode 100644 index 0000000000..654550fec5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx @@ -0,0 +1,97 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" +import { useAdminCreatePublishableApiKey } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { useEffect } from "react" +import { Form } from "../../../../../components/common/form" + +type CreatePublishableApiKeyFormProps = { + subscribe: (state: boolean) => void +} + +const CreatePublishableApiKeySchema = zod.object({ + title: zod.string().min(1), +}) + +export const CreatePublishableApiKeyForm = ({ + subscribe, +}: CreatePublishableApiKeyFormProps) => { + const { mutateAsync, isLoading } = useAdminCreatePublishableApiKey() + + const form = useForm>({ + defaultValues: { + title: "", + }, + resolver: zodResolver(CreatePublishableApiKeySchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync(values) + }) + + return ( +
+ + +
+ + + + +
+
+ +
+
+
+ + {t("apiKeyManagement.createPublishableApiKey")} + + + {t("apiKeyManagement.publishableApiKeyHint")} + +
+
+ { + return ( + + {t("fields.title")} + + + + + + ) + }} + /> +
+
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts new file mode 100644 index 0000000000..e5aa57664b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/index.ts @@ -0,0 +1 @@ +export * from "./create-publishable-api-key-form" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts new file mode 100644 index 0000000000..e85125cac7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementCreate as Component } from "./api-key-management-create" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx new file mode 100644 index 0000000000..169888b96e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/api-key-management-detail.tsx @@ -0,0 +1,3 @@ +export const ApiKeyManagementDetail = () => { + return
+} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts new file mode 100644 index 0000000000..2c193a6bd8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementDetail as Component } from "./api-key-management-detail" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx new file mode 100644 index 0000000000..5c4125b5dd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/api-key-management-edit.tsx @@ -0,0 +1,12 @@ +import { Drawer } from "@medusajs/ui" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" + +export const ApiKeyManagementEdit = () => { + const [open, onOpenChange] = useRouteModalState() + + return ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts new file mode 100644 index 0000000000..dcda77a479 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementEdit as Component } from "./api-key-management-edit" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx similarity index 62% rename from packages/admin-next/dashboard/src/routes/api-key-management/api-key-management.tsx rename to packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx index d896d2578a..9f65eeb782 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx @@ -1,4 +1,3 @@ -import { InformationCircle } from "@medusajs/icons" import { PublishableApiKey, SalesChannel } from "@medusajs/medusa" import { Button, @@ -21,136 +20,22 @@ import { } from "@tanstack/react-table" import { useAdminCreatePublishableApiKey, - useAdminPublishableApiKeys, useAdminSalesChannels, } from "medusa-react" -import { useMemo, useState } from "react" +import { useMemo } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import * as zod from "zod" -import { Form } from "../../components/common/form" - -export const ApiKeyManagement = () => { - const [showCreateModal, setShowCreateModal] = useState(false) - - const { publishable_api_keys, isLoading, isError, error } = - useAdminPublishableApiKeys() - - const columns = useColumns() - - const table = useReactTable({ - data: publishable_api_keys || [], - columns, - getCoreRowModel: getCoreRowModel(), - getRowId: (row) => row.id, - }) - - const { t } = useTranslation() - - // TODO: Move to loading.tsx and set as Suspense fallback for the route - if (isLoading) { - return
Loading
- } - - // TODO: Move to error.tsx and set as ErrorBoundary for the route - if (isError || !publishable_api_keys) { - const err = error ? JSON.parse(JSON.stringify(error)) : null - return ( -
- {(err as Error & { status: number })?.status === 404 ? ( -
Not found
- ) : ( -
Something went wrong!
- )} -
- ) - } - - const hasData = publishable_api_keys.length !== 0 +import { Outlet } from "react-router-dom" +import { Form } from "../../../components/common/form" +import { ApiKeyManagementListTable } from "./components/api-key-management-list-table" +export const ApiKeyManagementList = () => { return (
- -
- {t("apiKeyManagement.domain")} -
-
- {hasData ? ( - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
- ) : ( -
-
-
- - - {t("general.noRecordsFound")} - - - {t("apiKeyManagement.createAPublishableApiKey")} - -
- -
-
- )} -
-
-
- + +
) } @@ -274,8 +159,14 @@ const CreatePublishableApiKey = (props: CreatePublishableApiKeyProps) => {
- - + + + +
diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx new file mode 100644 index 0000000000..cdbf3d8adb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/api-key-management-list-table.tsx @@ -0,0 +1,265 @@ +import { EllipsisHorizontal, Trash, XCircle } from "@medusajs/icons" +import { PublishableApiKey } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, + usePrompt, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + useAdminDeletePublishableApiKey, + useAdminPublishableApiKeys, + useAdminRevokePublishableApiKey, +} from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { NoRecords } from "../../../../../components/common/empty-table-content" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +const PAGE_SIZE = 50 + +export const ApiKeyManagementListTable = () => { + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { publishable_api_keys, count, isLoading, isError, error } = + useAdminPublishableApiKeys({}) + + const columns = useColumns() + + const table = useReactTable({ + data: publishable_api_keys || [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + getRowId: (row) => row.id, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + const { t } = useTranslation() + const navigate = useNavigate() + + if (isLoading) { + return
Loading...
+ } + + if (isError) { + throw error + } + + return ( + +
+ {t("apiKeyManagement.domain")} + + + +
+
+ {(publishable_api_keys?.length ?? 0) > 0 ? ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + navigate( + `/settings/api-key-management/${row.original.id}` + ) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ ) : ( + + )} +
+
+ ) +} + +const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => { + const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey( + apiKey.id + ) + const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey( + apiKey.id + ) + + const { t } = useTranslation() + const prompt = usePrompt() + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("apiKeyManagement.deleteKeyWarning", { + title: apiKey.title, + }), + confirmText: t("general.delete"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await deleteAsync() + } + + const handleRevoke = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("apiKeyManagement.revokeKeyWarning", { + title: apiKey.title, + }), + confirmText: t("apiKeyManagement.revoke"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await revokeAsync() + } + + return ( + + + + + + + + +
+ + {t("apiKeyManagement.revoke")} +
+
+ +
+ + {t("general.delete")} +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + const columns = useMemo( + () => [ + columnHelper.accessor("title", { + header: t("fields.title"), + cell: ({ getValue }) => getValue(), + }), + columnHelper.accessor("id", { + header: "Key", + cell: ({ getValue }) => getValue(), + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return + }, + }), + ], + [t] + ) + + return columns +} diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts new file mode 100644 index 0000000000..ff8ba6f75d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/components/api-key-management-list-table/index.ts @@ -0,0 +1 @@ +export * from "./api-key-management-list-table" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts new file mode 100644 index 0000000000..7fd84a7245 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/index.ts @@ -0,0 +1 @@ +export { ApiKeyManagementList as Component } from "./api-key-management-list" diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/index.ts b/packages/admin-next/dashboard/src/routes/api-key-management/index.ts deleted file mode 100644 index 9ddecac337..0000000000 --- a/packages/admin-next/dashboard/src/routes/api-key-management/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ApiKeyManagement as Component } from "./api-key-management" diff --git a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx b/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx deleted file mode 100644 index 3a2fc31169..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/edit-currencies-details-drawer.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Store } from "@medusajs/medusa" -import { Button, Drawer, Heading, Select } from "@medusajs/ui" -import { useAdminUpdateStore } from "medusa-react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import * as zod from "zod" -import { Form } from "../../../../components/common/form" - -const EditCurrenciesDetailsSchema = zod.object({ - default_currency_code: zod.string(), -}) - -type EditCurrenciesDetailsDrawerProps = { - store: Store -} - -export const EditCurrenciesDetailsDrawer = ({ - store, -}: EditCurrenciesDetailsDrawerProps) => { - const [open, setOpen] = useState(false) - const [selectOpen, setSelectOpen] = useState(false) - - const { t } = useTranslation() - - const { mutateAsync } = useAdminUpdateStore() - - const form = useForm>({ - defaultValues: { - default_currency_code: store.default_currency_code, - }, - }) - - const sortedCurrencies = store.currencies.sort((a, b) => { - if (a.code === store.default_currency_code) { - return -1 - } - - if (b.code === store.default_currency_code) { - return 1 - } - - return a.code.localeCompare(b.code) - }) - - const onOpenChange = (open: boolean) => { - if (!open) { - form.reset() - - /** - * We need to close the select when the drawer closes. - * Otherwise it may lead to `pointer-events: none` being applied to the body. - */ - setSelectOpen(false) - } - setOpen(open) - } - - const onSubmit = form.handleSubmit(async (values) => { - await mutateAsync( - { - default_currency_code: values.default_currency_code, - }, - { - onSuccess: ({ store }) => { - form.reset({ - default_currency_code: store.default_currency_code, - }) - - onOpenChange(false) - }, - onError: (err) => { - console.log(err) - }, - } - ) - }) - - return ( - - - - - - - {t("currencies.editCurrencyDetails")} - - -
- - { - return ( - - {t("currencies.defaultCurrency")} -
- - - - -
- - {t("currencies.defaultCurrencyHint")} - -
- ) - }} - /> - - -
- - - - - - -
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts b/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts deleted file mode 100644 index 8a247de79e..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/components/edit-currencies-details-drawer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./edit-currencies-details-drawer" diff --git a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx b/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx deleted file mode 100644 index c2550a459a..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/currencies-details.tsx +++ /dev/null @@ -1,488 +0,0 @@ -import { - BuildingTax, - CurrencyDollar, - EllipsisHorizontal, -} from "@medusajs/icons" -import { Currency, Store } from "@medusajs/medusa" -import { - Badge, - Button, - Checkbox, - CommandBar, - Container, - DropdownMenu, - FocusModal, - Heading, - IconButton, - Table, - Text, - Tooltip, - clx, - usePrompt, -} from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - getPaginationRowModel, - useReactTable, -} from "@tanstack/react-table" -import { - useAdminCurrencies, - useAdminStore, - useAdminUpdateStore, -} from "medusa-react" -import { useMemo, useState } from "react" -import { useTranslation } from "react-i18next" -import { EditCurrenciesDetailsDrawer } from "../../components/edit-currencies-details-drawer" - -export const CurrenciesDetails = () => { - const { t } = useTranslation() - - const { store, isLoading } = useAdminStore() - - if (isLoading || !store) { - return
Loading...
- } - - return ( -
- -
-
- {t("currencies.domain")} - - {t("currencies.manageTheCurrencies")} - -
- -
-
- - {t("currencies.defaultCurrency")} - -
- - {store.default_currency_code} - - - {store.default_currency.name} - -
-
-
- -
- ) -} - -type StoreCurrenciesSectionProps = { - store: Store -} - -const PAGE_SIZE = 20 - -const StoreCurrencySection = ({ store }: StoreCurrenciesSectionProps) => { - const [addModalOpen, setAddModalOpen] = useState(false) - const [rowSelection, setRowSelection] = useState({}) - const { mutateAsync } = useAdminUpdateStore() - const prompt = usePrompt() - const { t } = useTranslation() - const pageCount = Math.ceil(store.currencies.length / PAGE_SIZE) - const columns = useStoreCurrencyColumns() - - const table = useReactTable({ - data: store.currencies, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onRowSelectionChange: setRowSelection, - pageCount: pageCount, - state: { - rowSelection, - }, - }) - - const onDeleteCurrencies = async () => { - const ids = Object.keys(rowSelection) - - const result = await prompt({ - title: t("general.areYouSure"), - description: t("currencies.removeCurrenciesWarning", { - count: ids.length, - }), - confirmText: t("general.remove"), - cancelText: t("general.cancel"), - }) - - if (!result) { - return - } - - await mutateAsync({ - currencies: store.currencies - .filter((c) => !ids.includes(c.code)) - .map((c) => c.code), - }) - } - - return ( - -
- Store Currencies - - - - - - - - setAddModalOpen(!addModalOpen)} - > - - Add Currencies - - - - Tax Preferences - - - -
-
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} - -
- - - - - {t("general.countSelected", { - count: Object.keys(rowSelection).length, - })} - - - - - -
- -
- ) -} - -const storeCurrencyColumnHelper = createColumnHelper() - -const useStoreCurrencyColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - storeCurrencyColumnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row }) => { - return ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - }, - }), - storeCurrencyColumnHelper.accessor("code", { - header: t("fields.code"), - cell: ({ getValue }) => getValue().toUpperCase(), - }), - storeCurrencyColumnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => getValue(), - }), - storeCurrencyColumnHelper.accessor("includes_tax", { - header: "Tax Inclusive Prices", - cell: ({ getValue }) => { - return getValue() ? t("general.enabled") : t("general.disabled") - }, - }), - ], - [t] - ) -} - -const CURRENCIES_PAGE_SIZE = 50 - -const AddCurrenciesModal = ({ - store, - open, - onOpenChange, -}: { - store: Store - open: boolean - onOpenChange: (open: boolean) => void -}) => { - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: CURRENCIES_PAGE_SIZE, - }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - const { currencies, count, isLoading } = useAdminCurrencies({ - limit: CURRENCIES_PAGE_SIZE, - offset: pageIndex * CURRENCIES_PAGE_SIZE, - }) - - const columns = useCurrencyColumns() - - const table = useReactTable({ - data: currencies ?? [], - columns, - pageCount: Math.ceil((count ?? 0) / CURRENCIES_PAGE_SIZE), - state: { - pagination, - rowSelection, - }, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - meta: { - currencyCodes: store.currencies?.map((c) => c.code) ?? [], - }, - }) - - const { t } = useTranslation() - - return ( - - - -
- - -
-
- -
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - c.code) - ?.includes(row.original.code), - }, - { - "bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover": - row.getIsSelected(), - } - )} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
-
-
- -
-
-
-
- ) -} - -const currencyColumnHelper = createColumnHelper() - -const useCurrencyColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - currencyColumnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row, table }) => { - const { currencyCodes } = table.options.meta as { - currencyCodes: string[] - } - - const isAdded = currencyCodes.includes(row.original.code) - - const isSelected = row.getIsSelected() || isAdded - - const Component = ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - - if (isAdded) { - return ( - - {Component} - - ) - } - - return Component - }, - }), - currencyColumnHelper.accessor("code", { - header: t("fields.code"), - cell: ({ getValue }) => getValue().toUpperCase(), - }), - currencyColumnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => getValue(), - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts b/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts deleted file mode 100644 index 5069f29b0c..0000000000 --- a/packages/admin-next/dashboard/src/routes/currencies/views/currencies-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CurrenciesDetails as Component } from "./currencies-details" diff --git a/packages/admin-next/dashboard/src/routes/locations/list/index.ts b/packages/admin-next/dashboard/src/routes/locations/list/index.ts deleted file mode 100644 index 4ede307bd7..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LocationsList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/locations/list/list.tsx b/packages/admin-next/dashboard/src/routes/locations/list/list.tsx deleted file mode 100644 index 25a6d42694..0000000000 --- a/packages/admin-next/dashboard/src/routes/locations/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const LocationsList = () => { - return ( -
- - Locations - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts new file mode 100644 index 0000000000..eb7ea74f11 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/index.ts @@ -0,0 +1 @@ +export { LocationAddSalesChannels as Component } from "./location-add-sales-channels" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx new file mode 100644 index 0000000000..6f7d42bfeb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-add-sales-channels/location-add-sales-channels.tsx @@ -0,0 +1,15 @@ +import { FocusModal } from "@medusajs/ui" +import { useAdminAddLocationToSalesChannel } from "medusa-react" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" + +export const LocationAddSalesChannels = () => { + const [open, onOpenChange] = useRouteModalState() + + const { mutateAsync } = useAdminAddLocationToSalesChannel() // TODO: We need a batch mutation instead of this to avoid multiple requests + + return ( + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx new file mode 100644 index 0000000000..5c8c6fff37 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/create-location-form.tsx @@ -0,0 +1,234 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" +import { useAdminCreateStockLocation } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { CountrySelect } from "../../../../../components/common/country-select" +import { Form } from "../../../../../components/common/form" + +const CreateLocationSchema = zod.object({ + name: zod.string().min(1), + address: zod.object({ + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), // TODO: Add validation + }), +}) + +export const CreateLocationForm = () => { + const { mutateAsync, isLoading } = useAdminCreateStockLocation() + + const form = useForm>({ + defaultValues: { + name: "", + address: { + address_1: "", + address_2: "", + city: "", + company: "", + country_code: "", + phone: "", + postal_code: "", + province: "", + }, + }, + resolver: zodResolver(CreateLocationSchema), + }) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + mutateAsync( + { + name: values.name, + address: values.address, + }, + { + onSuccess: () => {}, + } + ) + }) + + return ( +
+ + +
+ + + + +
+
+ +
+
+
+ + {t("locations.createLocation")} + + + {t("locations.detailsHint")} + +
+
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + + {t("fields.postalCode")} + + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts new file mode 100644 index 0000000000..0803aab3f5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/components/create-location-form/index.ts @@ -0,0 +1 @@ +export * from "./create-location-form" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-create/index.ts new file mode 100644 index 0000000000..9e7d96209e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/index.ts @@ -0,0 +1 @@ +export { LocationCreate as Component } from "./location-create" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx b/packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx new file mode 100644 index 0000000000..0369b258d6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-create/location-create.tsx @@ -0,0 +1,15 @@ +import { FocusModal } from "@medusajs/ui" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { CreateLocationForm } from "./components/create-location-form" + +export const LocationCreate = () => { + const [open, onOpenChange] = useRouteModalState() + + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts new file mode 100644 index 0000000000..301849accd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/index.ts @@ -0,0 +1 @@ +export * from "./location-general-section" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx new file mode 100644 index 0000000000..b12aea39ab --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx @@ -0,0 +1,101 @@ +import type { + StockLocationAddressDTO, + StockLocationExpandedDTO, +} from "@medusajs/types" +import { Button, Container, Heading, Text, clx } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" + +type LocationGeneralSectionProps = { + location: StockLocationExpandedDTO +} + +export const LocationGeneralSection = ({ + location, +}: LocationGeneralSectionProps) => { + const { t } = useTranslation() + + return ( + +
+ {location.name} + + + +
+
+ + {t("fields.address")} + + +
+
+ + {t("fields.company")} + + + {location.address?.company || "-"} + +
+
+ + {t("fields.phone")} + + + {location.address?.phone || "-"} + +
+
+ ) +} + +const AddressDisplay = ({ + address, +}: { + address: StockLocationAddressDTO | undefined +}) => { + if (!address) { + return ( + + - + + ) + } + + const { address_1, address_2, city, province, postal_code, country_code } = + address + + const addressParts = [ + address_1, + address_2, + `${city ? city + " " : ""}${province ? province + " " : ""}${postal_code}`, + country_code.toUpperCase(), + ] + + const addressString = addressParts + .filter((part) => part !== null && part !== undefined && part.trim() !== "") + .join(", ") + + return {addressString} +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts new file mode 100644 index 0000000000..6c16c11a32 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/index.ts @@ -0,0 +1 @@ +export * from "./location-sales-channel-section" diff --git a/packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-list/sales-channel-list.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx similarity index 80% rename from packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-list/sales-channel-list.tsx rename to packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx index 24c577883e..83c4f5f18a 100644 --- a/packages/admin-next/dashboard/src/routes/sales-channels/views/sales-channel-list/sales-channel-list.tsx +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/components/location-sales-channel-section/location-sales-channel-section.tsx @@ -1,6 +1,8 @@ import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" import { SalesChannel } from "@medusajs/medusa" +import { StockLocationExpandedDTO } from "@medusajs/types" import { + Button, Container, DropdownMenu, Heading, @@ -17,16 +19,22 @@ import { getCoreRowModel, useReactTable, } from "@tanstack/react-table" -import { useAdminDeleteSalesChannel, useAdminSalesChannels } from "medusa-react" import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" +import { Link, useNavigate } from "react-router-dom" +import { NoRecords } from "../../../../../components/common/empty-table-content/empty-table-content" -const PAGE_SIZE = 50 +type LocationSalesChannelSectionProps = { + location: StockLocationExpandedDTO +} -export const SalesChannelList = () => { - const navigate = useNavigate() +const PAGE_SIZE = 20 + +export const LocationSalesChannelSection = ({ + location, +}: LocationSalesChannelSectionProps) => { const { t } = useTranslation() + const navigate = useNavigate() const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, @@ -43,15 +51,12 @@ export const SalesChannelList = () => { const [rowSelection, setRowSelection] = useState({}) - const { sales_channels, count, isLoading } = useAdminSalesChannels({ - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - }) - + const salesChannels = location.sales_channels + const count = location.sales_channels?.length || 0 const columns = useColumns() const table = useReactTable({ - data: sales_channels ?? [], + data: salesChannels ?? [], columns, pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), state: { @@ -65,12 +70,17 @@ export const SalesChannelList = () => { }) return ( -
- -
- {t("salesChannels.domain")} -
-
+ +
+ Sales Channels + + + +
+
+ {count ? ( {table.getHeaderGroups().map((headerGroup) => { @@ -120,24 +130,20 @@ export const SalesChannelList = () => { ))}
- -
-
-
+ )} +
+ ) } const SalesChannelActions = ({ id }: { id: string }) => { - const { mutateAsync } = useAdminDeleteSalesChannel(id) const { t } = useTranslation() return ( @@ -178,11 +184,11 @@ const useColumns = () => { cell: ({ getValue }) => getValue(), }), columnHelper.accessor("is_disabled", { - header: () =>
{t("fields.status")}
, + header: t("fields.status"), cell: ({ getValue }) => { const value = getValue() return ( -
+
{value ? t("general.disabled") : t("general.enabled")} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts new file mode 100644 index 0000000000..086ccf707a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/index.ts @@ -0,0 +1 @@ +export { LocationDetail as Component } from "./location-detail" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx b/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx new file mode 100644 index 0000000000..45985636c6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-detail/location-detail.tsx @@ -0,0 +1,38 @@ +import { useAdminStockLocations } from "medusa-react" +import { Outlet, json, useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { LocationGeneralSection } from "./components/location-general-section" +import { LocationSalesChannelSection } from "./components/location-sales-channel-section" + +export const LocationDetail = () => { + const { id } = useParams() + const { stock_locations, isLoading, isError, error } = useAdminStockLocations( + { + id, + expand: "address,sales_channels", + } + ) + + if (isLoading) { + return
Loading...
+ } + + if (isError) { + throw error + } + + const stock_location = stock_locations?.[0] + + if (!stock_location) { + throw json({ message: "Not found" }, 404) + } + + return ( +
+ + + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx b/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx new file mode 100644 index 0000000000..3591059a73 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/components/edit-location-form/edit-location-form.tsx @@ -0,0 +1,218 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { StockLocationExpandedDTO } from "@medusajs/types" +import { Button, Drawer, Input } from "@medusajs/ui" +import { useAdminUpdateStockLocation } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { CountrySelect } from "../../../../../components/common/country-select" +import { Form } from "../../../../../components/common/form" + +type EditLocationFormProps = { + location: StockLocationExpandedDTO +} + +const EditLocationSchema = zod.object({ + name: zod.string().min(1), + address: zod.object({ + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), // TODO: Add validation + }), +}) + +export const EditLocationForm = ({ location }: EditLocationFormProps) => { + const form = useForm>({ + defaultValues: { + name: location.name, + address: { + address_1: location.address?.address_1 || "", + address_2: location.address?.address_2 || "", + city: location.address?.city || "", + company: location.address?.company || "", + country_code: location.address?.country_code || "", + phone: location.address?.phone || "", + postal_code: location.address?.postal_code || "", + province: location.address?.province || "", + }, + }, + resolver: zodResolver(EditLocationSchema), + }) + + const { mutateAsync, isLoading } = useAdminUpdateStockLocation(location.id) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + mutateAsync({ + name: values.name, + address: values.address, + }) + }) + + return ( +
+ + +
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.postalCode")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts new file mode 100644 index 0000000000..516dd003a1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/index.ts @@ -0,0 +1 @@ +export { LocationEdit as Component } from "./location-edit" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx b/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx new file mode 100644 index 0000000000..b30df632ce --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-edit/location-edit.tsx @@ -0,0 +1,47 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminStockLocations } from "medusa-react" +import { useTranslation } from "react-i18next" +import { json, useParams } from "react-router-dom" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { EditLocationForm } from "./components/edit-location-form/edit-location-form" + +export const LocationEdit = () => { + const [open, onOpenChange] = useRouteModalState() + const { id } = useParams() + + const { stock_locations, isLoading, isError, error } = useAdminStockLocations( + { + id, + expand: "address", + } + ) + + const { t } = useTranslation() + + if (isError) { + throw error + } + + const stock_location = stock_locations?.[0] + + if (!isLoading && !stock_location) { + throw json({ message: "Not found" }, 404) + } + + return ( + + + + + {t("locations.editLocation")} + + + {isLoading || !stock_location ? ( +
Loading...
+ ) : ( + + )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts new file mode 100644 index 0000000000..f1308b3734 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/index.ts @@ -0,0 +1 @@ +export * from "./locations-list-table" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx new file mode 100644 index 0000000000..795c81ecfd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/components/locations-list-table/locations-list-table.tsx @@ -0,0 +1,278 @@ +import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" +import { StockLocationExpandedDTO } from "@medusajs/types" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, + usePrompt, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + useAdminDeleteStockLocation, + useAdminStockLocations, +} from "medusa-react" +import { MouseEvent, useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate, useSearchParams } from "react-router-dom" + +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content/empty-table-content" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +const PAGE_SIZE = 50 + +export const LocationsListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { stock_locations, count, isLoading, isError, error } = + useAdminStockLocations({ + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + expand: "address", + }) + + const columns = useColumns() + + const table = useReactTable({ + data: stock_locations ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }) + + if (isLoading) { + return
Loading...
+ } + + if (isError) { + if (error) { + throw error + } + } + + return ( + +
+ Locations +
+ + + +
+
+ {(stock_locations?.length ?? 0) > 0 ? ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + navigate(`/settings/locations/${row.original.id}`) + } + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ ) : ( + + )} +
+ ) +} + +const LocationActions = ({ + location, +}: { + location: StockLocationExpandedDTO +}) => { + const { t } = useTranslation() + const prompt = usePrompt() + const { mutateAsync } = useAdminDeleteStockLocation(location.id) + + const handleDelete = async (e: MouseEvent) => { + e.stopPropagation() + + const res = await prompt({ + title: t("general.areYouSure"), + description: t("locations.deleteLocationWarning", { + name: location.name, + }), + verificationText: location.name, + verificationInstruction: t("general.typeToConfirm"), + confirmText: t("general.delete"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + + + + + + + + + e.stopPropagation()}> +
+ + {t("general.edit")} +
+
+ + + +
+ + {t("general.delete")} +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: (cell) => cell.getValue(), + }), + columnHelper.accessor("address", { + header: t("fields.address"), + cell: (cell) => { + const value = cell.getValue() + + if (!value) { + return "-" + } + + return `${value.address_1}${value.city ? `, ${value.city}` : ""}` + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} + +const NoLocations = () => { + const [params] = useSearchParams() + const { t } = useTranslation() + + const noParams = params.toString().length === 0 + + if (noParams) { + return ( + + ) + } + + return +} diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/index.ts b/packages/admin-next/dashboard/src/routes/locations/location-list/index.ts new file mode 100644 index 0000000000..883c2ba632 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/index.ts @@ -0,0 +1 @@ +export { LocationList as Component } from "./location-list" diff --git a/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx b/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx new file mode 100644 index 0000000000..02363aba29 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/locations/location-list/location-list.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom" +import { LocationsListTable } from "./components/locations-list-table" + +export const LocationList = () => { + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/login/login.tsx b/packages/admin-next/dashboard/src/routes/login/login.tsx index e042eccff2..dadf791ba5 100644 --- a/packages/admin-next/dashboard/src/routes/login/login.tsx +++ b/packages/admin-next/dashboard/src/routes/login/login.tsx @@ -1,13 +1,14 @@ import { zodResolver } from "@hookform/resolvers/zod" import { Button, Heading, Input, Text } from "@medusajs/ui" +import { useAdminGetSession, useAdminLogin } from "medusa-react" import { useForm } from "react-hook-form" -import { Link, useLocation, useNavigate } from "react-router-dom" +import { Link, Navigate, useLocation, useNavigate } from "react-router-dom" import * as z from "zod" import { Form } from "../../components/common/form" -import { useAuth } from "../../providers/auth-provider" +import { isAxiosError } from "../../lib/is-axios-error" -const schema = z.object({ +const LoginSchema = z.object({ email: z.string().email(), password: z.string(), }) @@ -15,40 +16,58 @@ const schema = z.object({ export const Login = () => { const navigate = useNavigate() const location = useLocation() - const { login } = useAuth() const from = location.state?.from?.pathname || "/" - const form = useForm>({ - resolver: zodResolver(schema), + const form = useForm>({ + resolver: zodResolver(LoginSchema), defaultValues: { email: "", password: "", }, }) - const onSubmit = form.handleSubmit(async ({ email, password }) => { - await login(email, password) - .then(() => { - navigate(from) - }) - .catch((e) => { - switch (e?.response?.status) { - case 401: - form.setError("password", { - type: "manual", - message: "Invalid email or password", - }) - break - default: - form.setError("password", { - type: "manual", - message: "Something went wrong", - }) - } - }) + const { user, isLoading } = useAdminGetSession() + + const { mutateAsync } = useAdminLogin({ + retry: false, }) + const onSubmit = form.handleSubmit(async ({ email, password }) => { + await mutateAsync( + { + email, + password, + }, + { + onSuccess: () => { + navigate(from, { replace: true }) + }, + onError: (e) => { + if (isAxiosError(e)) { + if (e.response?.status === 401) { + form.setError("password", { + type: "manual", + message: "Invalid email or password", + }) + + return + } + } + + form.setError("password", { + type: "manual", + message: "Something went wrong", + }) + }, + } + ) + }) + + if (user && !isLoading) { + return + } + return (
@@ -79,7 +98,7 @@ export const Login = () => { Password Forgot password? diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/product-attribute-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/product-attribute-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-attribute-section/product-attribute-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-attribute-section/product-attribute-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-general-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-general-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-general-section/product-general-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/product-general-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-general-section/product-general-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-general-section/product-general-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-media-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-media-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-media-section/product-media-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/product-media-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-media-section/product-media-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-media-section/product-media-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-option-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-option-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-option-section/product-option-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/product-option-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-option-section/product-option-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-option-section/product-option-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/product-sales-channel-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-sales-channel-section/product-sales-channel-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/product-thumbnail-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/product-thumbnail-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-thumbnail-section/product-thumbnail-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-thumbnail-section/product-thumbnail-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-variant-section/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-variant-section/index.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/index.ts diff --git a/packages/admin-next/dashboard/src/routes/products/components/product-variant-section/product-variant-section.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx similarity index 100% rename from packages/admin-next/dashboard/src/routes/products/components/product-variant-section/product-variant-section.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx diff --git a/packages/admin-next/dashboard/src/routes/products/product-detail/index.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/index.ts new file mode 100644 index 0000000000..afc54745c9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-detail/index.ts @@ -0,0 +1,2 @@ +export { productLoader as loader } from "./loader" +export { ProductDetail as Component } from "./product-detail" diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-details/loader.ts b/packages/admin-next/dashboard/src/routes/products/product-detail/loader.ts similarity index 91% rename from packages/admin-next/dashboard/src/routes/products/views/product-details/loader.ts rename to packages/admin-next/dashboard/src/routes/products/product-detail/loader.ts index 0f27a0d25f..7f5fad255e 100644 --- a/packages/admin-next/dashboard/src/routes/products/views/product-details/loader.ts +++ b/packages/admin-next/dashboard/src/routes/products/product-detail/loader.ts @@ -3,7 +3,7 @@ import { Response } from "@medusajs/medusa-js" import { adminProductKeys } from "medusa-react" import { LoaderFunctionArgs } from "react-router-dom" -import { medusa, queryClient } from "../../../../lib/medusa" +import { medusa, queryClient } from "../../../lib/medusa" const productDetailQuery = (id: string) => ({ queryKey: adminProductKeys.detail(id), diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-details/details.tsx b/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx similarity index 66% rename from packages/admin-next/dashboard/src/routes/products/views/product-details/details.tsx rename to packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx index 5139d932ae..7f0f42009b 100644 --- a/packages/admin-next/dashboard/src/routes/products/views/product-details/details.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-detail/product-detail.tsx @@ -1,43 +1,43 @@ -import { useAdminProduct } from "medusa-react"; -import { useLoaderData, useParams } from "react-router-dom"; +import { useAdminProduct } from "medusa-react" +import { useLoaderData, useParams } from "react-router-dom" -import { JsonView } from "../../../../components/common/json-view"; -import { ProductAttributeSection } from "../../components/product-attribute-section"; -import { ProductGeneralSection } from "../../components/product-general-section"; -import { ProductMediaSection } from "../../components/product-media-section"; -import { ProductOptionSection } from "../../components/product-option-section"; -import { ProductSalesChannelSection } from "../../components/product-sales-channel-section"; -import { ProductThumbnailSection } from "../../components/product-thumbnail-section"; -import { ProductVariantSection } from "../../components/product-variant-section"; -import { productLoader } from "./loader"; +import { JsonViewSection } from "../../../components/common/json-view-section" +import { ProductAttributeSection } from "./components/product-attribute-section" +import { ProductGeneralSection } from "./components/product-general-section" +import { ProductMediaSection } from "./components/product-media-section" +import { ProductOptionSection } from "./components/product-option-section" +import { ProductSalesChannelSection } from "./components/product-sales-channel-section" +import { ProductThumbnailSection } from "./components/product-thumbnail-section" +import { ProductVariantSection } from "./components/product-variant-section" +import { productLoader } from "./loader" -import after from "medusa-admin:widgets/product/details/after"; -import before from "medusa-admin:widgets/product/details/before"; -import sideAfter from "medusa-admin:widgets/product/details/side/after"; -import sideBefore from "medusa-admin:widgets/product/details/side/before"; +import after from "medusa-admin:widgets/product/details/after" +import before from "medusa-admin:widgets/product/details/before" +import sideAfter from "medusa-admin:widgets/product/details/side/after" +import sideBefore from "medusa-admin:widgets/product/details/side/before" -export const ProductDetails = () => { +export const ProductDetail = () => { const initialData = useLoaderData() as Awaited< ReturnType - >; + > - const { id } = useParams(); + const { id } = useParams() const { product, isLoading, isError, error } = useAdminProduct( id!, undefined, { initialData: initialData, } - ); + ) // TODO: Move to loading.tsx and set as Suspense fallback for the route if (isLoading) { - return
Loading
; + return
Loading
} // TODO: Move to error.tsx and set as ErrorBoundary for the route if (isError || !product) { - const err = error ? JSON.parse(JSON.stringify(error)) : null; + const err = error ? JSON.parse(JSON.stringify(error)) : null return (
{(err as Error & { status: number })?.status === 404 ? ( @@ -46,7 +46,7 @@ export const ProductDetails = () => {
Something went wrong!
)}
- ); + ) } return ( @@ -56,9 +56,9 @@ export const ProductDetails = () => {
- ); + ) })} -
+
@@ -71,7 +71,7 @@ export const ProductDetails = () => {
- ); + ) })} @@ -80,7 +80,7 @@ export const ProductDetails = () => {
- ); + ) })}
{after.widgets.map((w, i) => { @@ -88,17 +88,17 @@ export const ProductDetails = () => {
- ); + ) })} - +
-
+
{sideBefore.widgets.map((w, i) => { return (
- ); + ) })} @@ -107,10 +107,10 @@ export const ProductDetails = () => {
- ); + ) })}
- ); -}; + ) +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts new file mode 100644 index 0000000000..ee665245d3 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/index.ts @@ -0,0 +1 @@ +export * from "./product-list-table" diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx new file mode 100644 index 0000000000..0114f4ffe9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -0,0 +1,284 @@ +import { EllipsisHorizontal, Trash } from "@medusajs/icons" +import type { Product } from "@medusajs/medusa" +import { + Button, + Checkbox, + CommandBar, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminDeleteProduct, useAdminProducts } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { useLoaderData, useNavigate } from "react-router-dom" + +import { + ProductAvailabilityCell, + ProductCollectionCell, + ProductStatusCell, + ProductTitleCell, + ProductVariantCell, +} from "../../../../../components/common/product-table-cells" + +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { productsLoader } from "../../loader" + +const PAGE_SIZE = 50 + +export const ProductListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const [rowSelection, setRowSelection] = useState({}) + + const initialData = useLoaderData() as Awaited< + ReturnType> + > + + const { products, count } = useAdminProducts( + { + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + }, + { + initialData, + } + ) + + const columns = useColumns() + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const table = useReactTable({ + data: (products ?? []) as Product[], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + return ( + +
+ {t("products.domain")} + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/products/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+ +
+ + + + {t("general.countSelected", { + count: Object.keys(rowSelection).length, + })} + + + { + console.log("Delete") + }} + shortcut="d" + label={t("general.delete")} + /> + + +
+ ) +} + +const ProductActions = ({ id }: { id: string }) => { + const { mutateAsync } = useAdminDeleteProduct(id) + + const handleDelete = async () => { + await mutateAsync() + } + + return ( + + + + + + + + +
+ + Delete +
+
+
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + const columns = useMemo( + () => [ + columnHelper.display({ + id: "select", + header: ({ table }) => { + return ( + + table.toggleAllPageRowsSelected(!!value) + } + /> + ) + }, + cell: ({ row }) => { + return ( + row.toggleSelected(!!value)} + onClick={(e) => { + e.stopPropagation() + }} + /> + ) + }, + }), + columnHelper.accessor("title", { + header: t("fields.title"), + cell: ({ row }) => { + return + }, + }), + columnHelper.accessor("collection", { + header: t("fields.collection"), + cell: (cell) => { + const collection = cell.getValue() + + return + }, + }), + columnHelper.accessor("sales_channels", { + header: t("fields.availability"), + cell: (cell) => { + const salesChannels = cell.getValue() + + return + }, + }), + columnHelper.accessor("variants", { + header: t("fields.inventory"), + cell: (cell) => { + const variants = cell.getValue() + + return + }, + }), + columnHelper.accessor("status", { + header: t("fields.status"), + cell: (cell) => { + const value = cell.getValue() + + return + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return + }, + }), + ], + [t] + ) + + return columns +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/index.ts b/packages/admin-next/dashboard/src/routes/products/product-list/index.ts new file mode 100644 index 0000000000..085cd656a4 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/index.ts @@ -0,0 +1,2 @@ +export { productsLoader as productLoader } from "./loader" +export { ProductList as Component } from "./product-list" diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-list/loader.ts b/packages/admin-next/dashboard/src/routes/products/product-list/loader.ts similarity index 55% rename from packages/admin-next/dashboard/src/routes/products/views/product-list/loader.ts rename to packages/admin-next/dashboard/src/routes/products/product-list/loader.ts index 2828984d46..9ad18e2dc5 100644 --- a/packages/admin-next/dashboard/src/routes/products/views/product-list/loader.ts +++ b/packages/admin-next/dashboard/src/routes/products/product-list/loader.ts @@ -1,23 +1,23 @@ -import { AdminProductsListRes } from "@medusajs/medusa"; -import { Response } from "@medusajs/medusa-js"; -import { QueryClient } from "@tanstack/react-query"; -import { adminProductKeys } from "medusa-react"; +import { AdminProductsListRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { QueryClient } from "@tanstack/react-query" +import { adminProductKeys } from "medusa-react" -import { medusa, queryClient } from "../../../../lib/medusa"; +import { medusa, queryClient } from "../../../lib/medusa" const productsListQuery = () => ({ queryKey: adminProductKeys.list({ limit: 20, offset: 0 }), queryFn: async () => medusa.admin.products.list({ limit: 20, offset: 0 }), -}); +}) export const productsLoader = (client: QueryClient) => { return async () => { - const query = productsListQuery(); + const query = productsListQuery() return ( queryClient.getQueryData>( query.queryKey ) ?? (await client.fetchQuery(query)) - ); - }; -}; + ) + } +} diff --git a/packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx b/packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx new file mode 100644 index 0000000000..11513802c8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/products/product-list/product-list.tsx @@ -0,0 +1,18 @@ +import after from "medusa-admin:widgets/product/list/after" +import before from "medusa-admin:widgets/product/list/before" + +import { ProductListTable } from "./components/product-list-table" + +export const ProductList = () => { + return ( +
+ {before.widgets.map((w, i) => ( + + ))} + + {after.widgets.map((w, i) => ( + + ))} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts b/packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts deleted file mode 100644 index a1671c01de..0000000000 --- a/packages/admin-next/dashboard/src/routes/products/views/product-details/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ProductDetails as Component } from "./details"; -export { productLoader as loader } from "./loader"; diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts b/packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts deleted file mode 100644 index bad072e574..0000000000 --- a/packages/admin-next/dashboard/src/routes/products/views/product-list/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ProductList as Component } from "./list"; -export { productsLoader as productLoader } from "./loader"; diff --git a/packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx b/packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx deleted file mode 100644 index bf546c833b..0000000000 --- a/packages/admin-next/dashboard/src/routes/products/views/product-list/list.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import type { Product } from "@medusajs/medusa" -import { - Checkbox, - CommandBar, - Container, - DropdownMenu, - Heading, - IconButton, - Table, - Text, - clx, -} from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" -import { useAdminDeleteProduct, useAdminProducts } from "medusa-react" -import { useMemo, useState } from "react" -import { useLoaderData, useNavigate } from "react-router-dom" - -import { Thumbnail } from "../../../../components/common/thumbnail" -import { productsLoader } from "./loader" - -import { EllipsisVertical, Trash } from "@medusajs/icons" -import after from "medusa-admin:widgets/product/list/after" -import before from "medusa-admin:widgets/product/list/before" -import { useTranslation } from "react-i18next" - -const PAGE_SIZE = 50 - -export const ProductList = () => { - const navigate = useNavigate() - const { t } = useTranslation() - - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: PAGE_SIZE, - }) - - const [rowSelection, setRowSelection] = useState({}) - - const initialData = useLoaderData() as Awaited< - ReturnType> - > - - const { products, count } = useAdminProducts( - { - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - }, - { - initialData, - } - ) - - const columns = useColumns() - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const table = useReactTable({ - data: (products ?? []) as Product[], - columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - }) - - return ( -
- {before.widgets.map((w, i) => ( - - ))} - -
- {t("products.domain")} -
-
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/products/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
- -
- - - - {t("general.countSelected", { - count: Object.keys(rowSelection).length, - })} - - - { - console.log("Delete") - }} - shortcut="d" - label={t("general.delete")} - /> - - -
- {after.widgets.map((w, i) => ( - - ))} -
- ) -} - -const ProductActions = ({ id }: { id: string }) => { - const { mutateAsync } = useAdminDeleteProduct(id) - - const handleDelete = async () => { - await mutateAsync() - } - - return ( - - - - - - - - -
- - Delete -
-
-
-
- ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - const columns = useMemo( - () => [ - columnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row }) => { - return ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - }, - }), - columnHelper.accessor("title", { - header: t("fields.title"), - cell: (cell) => { - const title = cell.getValue() - const thumbnail = cell.row.original.thumbnail - - return ( -
- - - {title} - -
- ) - }, - }), - columnHelper.accessor("variants", { - header: t("products.variants"), - cell: (cell) => { - const variants = cell.getValue() - - return ( - - {variants.length} - - ) - }, - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => { - return - }, - }), - ], - [t] - ) - - return columns -} diff --git a/packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx b/packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx deleted file mode 100644 index 27b4d5684c..0000000000 --- a/packages/admin-next/dashboard/src/routes/profile/components/edit-profile-details-drawer/edit-profile-details-drawer.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { Button, Drawer, Heading, Input, Select, Switch } from "@medusajs/ui" -import { adminAuthKeys, useAdminUpdateUser } from "medusa-react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { Trans, useTranslation } from "react-i18next" -import * as zod from "zod" -import { Form } from "../../../../components/common/form" -import { languages } from "../../../../i18n/config" -import { queryClient } from "../../../../lib/medusa" - -const EditProfileDetailsSchema = zod.object({ - first_name: zod.string().optional(), - last_name: zod.string().optional(), - language: zod.string(), - user_insights: zod.boolean(), -}) - -type EditProfileDetailsDrawerProps = { - id: string - firstName?: string - lastName?: string - userInsights?: boolean -} - -export const EditProfileDetailsDrawer = ({ - id, - firstName = "", - lastName = "", - userInsights = false, -}: EditProfileDetailsDrawerProps) => { - const [open, setOpen] = useState(false) - const [selectOpen, setSelectOpen] = useState(false) - const { mutateAsync, isLoading } = useAdminUpdateUser(id) - - const { i18n } = useTranslation() - - const changeLanguage = (code: string) => { - i18n.changeLanguage(code) - } - - const sortedLanguages = languages.sort((a, b) => - a.display_name.localeCompare(b.display_name) - ) - - const form = useForm>({ - defaultValues: { - first_name: firstName, - last_name: lastName, - language: i18n.language, - user_insights: userInsights, - }, - resolver: zodResolver(EditProfileDetailsSchema), - }) - - const { t } = useTranslation() - - const onOpenChange = (open: boolean) => { - if (!open) { - form.reset() - - /** - * If the select is open while closing the drawer, we need to close it as well. - * Otherwise it will cause "pointer-events: none" to stay applied to the body, - * making the page unresponsive. - */ - setSelectOpen(false) - } - - setOpen(open) - } - - const onSubmit = form.handleSubmit(async (values) => { - await mutateAsync( - { - first_name: values.first_name, - last_name: values.last_name, - }, - { - onSuccess: ({ user }) => { - form.reset({ - first_name: user.first_name, - last_name: user.last_name, - }) - - // Invalidate the current user session. - queryClient.invalidateQueries(adminAuthKeys.details()) - }, - onError: (error) => { - console.log(error) - return - }, - } - ) - - changeLanguage(values.language) - - onOpenChange(false) - }) - - return ( - - - - - - - {t("profile.editProfileDetails")} - - -
-
-
- ( - - {t("fields.firstName")} - - - - - - )} - /> - ( - - {t("fields.lastName")} - - - - - - )} - /> -
- ( - -
- Language - {t("profile.languageHint")} -
-
- - - - -
-
- )} - /> - ( - -
- User Insights - - - -
- - - , - ]} - /> - - - -
- )} - /> -
-
-
- -
- - - - -
-
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts new file mode 100644 index 0000000000..948c77df96 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/index.ts @@ -0,0 +1 @@ +export * from "./profile-general-section" diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx new file mode 100644 index 0000000000..49ef382d96 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx @@ -0,0 +1,63 @@ +import { User } from "@medusajs/medusa" +import { Button, Container, Heading, StatusBadge, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" +import { languages } from "../../../../../i18n/config" + +type ProfileGeneralSectionProps = { + user: Omit +} + +export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => { + const { i18n, t } = useTranslation() + return ( + +
+
+ {t("profile.domain")} + + {t("profile.manageYourProfileDetails")} + +
+ + + +
+
+ + {t("fields.name")} + + + {user.first_name} {user.last_name} + +
+
+ + {t("fields.email")} + + + {user.email} + +
+
+ + {t("profile.language")} + + + {languages.find((lang) => lang.code === i18n.language) + ?.display_name || "-"} + +
+
+ + {t("profile.usageInsights")} + + + {t("general.disabled")} + +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts b/packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts new file mode 100644 index 0000000000..9d981a433d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/index.ts @@ -0,0 +1 @@ +export { ProfileDetail as Component } from "./profile-detail" diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx new file mode 100644 index 0000000000..c69975be28 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/profile-detail.tsx @@ -0,0 +1,26 @@ +import { useAdminGetSession } from "medusa-react" +import { Outlet, json } from "react-router-dom" +import { ProfileGeneralSection } from "./components/profile-general-section" + +export const ProfileDetail = () => { + const { user, isLoading, isError, error } = useAdminGetSession() + + if (isLoading) { + return
Loading...
+ } + + if (isError || !user) { + if (error) { + throw error + } + + throw json("An unknown error has occured", 500) + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx new file mode 100644 index 0000000000..7c616718aa --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-edit/components/edit-profile-form/edit-profile-form.tsx @@ -0,0 +1,208 @@ +import { User } from "@medusajs/medusa" +import * as zod from "zod" + +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, Drawer, Input, Select, Switch } from "@medusajs/ui" +import { adminAuthKeys, useAdminUpdateUser } from "medusa-react" +import { useForm } from "react-hook-form" +import { Trans, useTranslation } from "react-i18next" +import { Form } from "../../../../../components/common/form" +import { languages } from "../../../../../i18n/config" +import { queryClient } from "../../../../../lib/medusa" + +type EditProfileProps = { + user: Omit + usageInsights: boolean + onSuccess: () => void +} + +const EditProfileSchema = zod.object({ + first_name: zod.string().optional(), + last_name: zod.string().optional(), + language: zod.string(), + usage_insights: zod.boolean(), +}) + +export const EditProfileForm = ({ + user, + usageInsights, + onSuccess, +}: EditProfileProps) => { + const { t, i18n } = useTranslation() + const { mutateAsync, isLoading } = useAdminUpdateUser(user.id) + + const form = useForm>({ + defaultValues: { + first_name: user.first_name ?? "", + last_name: user.last_name ?? "", + language: i18n.language, + usage_insights: usageInsights, + }, + resolver: zodResolver(EditProfileSchema), + }) + + const changeLanguage = (code: string) => { + i18n.changeLanguage(code) + } + + const sortedLanguages = languages.sort((a, b) => + a.display_name.localeCompare(b.display_name) + ) + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync( + { + first_name: values.first_name, + last_name: values.last_name, + }, + { + onSuccess: ({ user }) => { + form.reset({ + first_name: user.first_name, + last_name: user.last_name, + }) + + // Invalidate the current user session. + queryClient.invalidateQueries(adminAuthKeys.details()) + }, + onError: () => { + return + }, + } + ) + + changeLanguage(values.language) + + onSuccess() + }) + + return ( +
+ + +
+
+ ( + + {t("fields.firstName")} + + + + + + )} + /> + ( + + {t("fields.lastName")} + + + + + + )} + /> +
+ ( + +
+ {t("profile.language")} + {t("profile.languageHint")} +
+
+ + + + +
+
+ )} + /> + ( + +
+ {t("profile.usageInsights")} + + + +
+ + + , + ]} + /> + + + +
+ )} + /> +
+
+ +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts b/packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts new file mode 100644 index 0000000000..99be2393cb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-edit/index.ts @@ -0,0 +1 @@ +export { ProfileEdit as Component } from "./profile-edit" diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx new file mode 100644 index 0000000000..4154cb7f49 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/profile/profile-edit/profile-edit.tsx @@ -0,0 +1,52 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminGetSession } from "medusa-react" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form" + +export const ProfileEdit = () => { + const [open, setOpen] = useState(false) + const navigate = useNavigate() + + const { user, isLoading, isError, error } = useAdminGetSession() + + const { t } = useTranslation() + + useEffect(() => { + setOpen(true) + }, []) + + const onOpenChange = (open: boolean) => { + if (!open) { + setTimeout(() => { + navigate(`/settings/profile`, { replace: true }) + }, 200) + } + + setOpen(open) + } + + if (isError) { + throw error + } + + return ( + + + + {t("profile.editProfile")} + + {isLoading || !user ? ( +
Loading...
+ ) : ( + onOpenChange(false)} + /> + )} +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts b/packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts deleted file mode 100644 index 0402621d02..0000000000 --- a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Profile as Component } from "./profile"; diff --git a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx b/packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx deleted file mode 100644 index 6e607d3608..0000000000 --- a/packages/admin-next/dashboard/src/routes/profile/views/profile-details/profile.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Container, Heading, Text } from "@medusajs/ui" - -import { Spinner } from "@medusajs/icons" -import { useAdminGetSession } from "medusa-react" -import { useTranslation } from "react-i18next" -import { languages } from "../../../../i18n/config" -import { EditProfileDetailsDrawer } from "../../components/edit-profile-details-drawer/edit-profile-details-drawer" - -export const Profile = () => { - const { user, isLoading } = useAdminGetSession() - const { i18n, t } = useTranslation() - - if (isLoading || !user) { - return ( -
- -
- ) - } - - return ( -
- -
- {t("profile.domain")} - - {t("profile.manageYourProfileDetails")} - -
-
- - Name - - - {user.first_name} {user.last_name} - -
-
- - Email - - - {user.email} - -
-
- - Language - - - {languages.find((lang) => lang.code === i18n.language) - ?.display_name || "-"} - -
-
- - Usage insights - -
-
- -
-
-
- ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx b/packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx deleted file mode 100644 index 8752f83a90..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/components/edit-region-details-drawer/edit-region-details-drawer.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Drawer } from "@medusajs/ui" -import { useState } from "react" - -export const EditRegionDetailsDrawer = () => { - const [open, setOpen] = useState(false) - - return ( - - - - ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-create/index.ts new file mode 100644 index 0000000000..cbd7dbdfe1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/index.ts @@ -0,0 +1 @@ +export { RegionCreate as Component } from "./region-create" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx new file mode 100644 index 0000000000..e900d160a9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx @@ -0,0 +1,201 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Switch, Text } from "@medusajs/ui" +import { useAdminCreateRegion } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { useNavigate } from "react-router-dom" +import { Form } from "../../../components/common/form" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" + +const CreateRegionFormSchema = zod.object({ + name: zod.string().min(1), + currency_code: zod.string(), + includes_tax: zod.boolean(), + countries: zod.array(zod.string()), + fulfillment_providers: zod.array(zod.string()).min(1), + payment_providers: zod.array(zod.string()).min(1), + tax_rate: zod.number().min(0).max(100), + tax_code: zod.string().optional(), +}) + +export const RegionCreate = () => { + const [open, onOpenChange] = useRouteModalState() + const navigate = useNavigate() + + const { t } = useTranslation() + + const form = useForm>({ + defaultValues: { + name: "", + currency_code: "", + includes_tax: false, + countries: [], + fulfillment_providers: [], + payment_providers: [], + tax_code: "", + }, + resolver: zodResolver(CreateRegionFormSchema), + }) + + const { mutateAsync, isLoading } = useAdminCreateRegion() + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync( + { + name: values.name, + countries: values.countries, + currency_code: values.currency_code, + fulfillment_providers: values.fulfillment_providers, + payment_providers: values.payment_providers, + tax_rate: values.tax_rate, + tax_code: values.tax_code, + includes_tax: values.includes_tax, + }, + { + onSuccess: ({ region }) => { + navigate(`../${region.id}`) + }, + } + ) + }) + + return ( + + +
+ + +
+ + + + +
+
+ +
+ +
+
+ { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.currency")} + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.taxRate")} + + + + + + ) + }} + /> + { + return ( + + + {t("fields.taxCode")} + + + + + + + ) + }} + /> +
+
+ { + return ( + +
+
+ + {t("fields.taxInclusivePricing")} + + + + +
+ {t("regions.taxInclusiveHint")} + +
+
+ ) + }} + /> +
+
+ + {t("fields.providers")} + + + {t("regions.providersHint")} + +
+
+
+
+
+ + {t("fields.metadata")} + +
+
+
+
+
+ +
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts new file mode 100644 index 0000000000..73ca48c45c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/index.ts @@ -0,0 +1 @@ +export * from "./region-general-section" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx new file mode 100644 index 0000000000..860ef29da6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-general-section/region-general-section.tsx @@ -0,0 +1,230 @@ +import { + BuildingTax, + EllipsisHorizontal, + PencilSquare, + Trash, +} from "@medusajs/icons" +import { Country, Region } from "@medusajs/medusa" +import { + Badge, + Button, + Container, + Drawer, + DropdownMenu, + Heading, + IconButton, + StatusBadge, + Text, + Tooltip, + usePrompt, +} from "@medusajs/ui" +import { useAdminDeleteRegion, useAdminUpdateRegion } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" +import * as zod from "zod" + +type RegionGeneralSectionProps = { + region: Region +} + +export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => { + const { t } = useTranslation() + + return ( + +
+
+ {region.name} + +
+ +
+
+ + {t("fields.currency")} + +
+ + {region.currency_code} + + + {region.currency?.name} + +
+
+
+ + {t("fields.taxInclusivePricing")} + + + {region.includes_tax ? t("general.enabled") : t("general.disabled")} + +
+
+ + {t("fields.paymentProviders")} + + + {region.payment_providers.length > 0 + ? region.payment_providers.map((p) => p.id).join(", ") + : "-"} + +
+
+ + {t("fields.fulfillmentProviders")} + + + {region.fulfillment_providers.length > 0 + ? region.fulfillment_providers.map((p) => p.id).join(", ") + : "-"} + +
+
+ ) +} + +const RegionActions = ({ region }: { region: Region }) => { + const { t } = useTranslation() + const { mutateAsync } = useAdminDeleteRegion(region.id) + const prompt = usePrompt() + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("regions.deleteRegionWarning", { + name: region.name, + }), + verificationText: region.name, + verificationInstruction: t("general.typeToConfirm"), + confirmText: t("general.delete"), + cancelText: t("general.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync(undefined) + } + + return ( + + + + + + + + + +
+ + {t("general.edit")} +
+
+ + + +
+ + Tax settings +
+
+ + + +
+ + Delete +
+
+
+
+ ) +} + +const RegionCountries = ({ countries }: { countries: Country[] }) => { + const { t } = useTranslation() + + return ( +
+ + {countries + .slice(0, 2) + .map((c) => c.display_name) + .join(", ")} + + {countries.length > 2 && ( + + {countries.slice(2).map((c) => ( +
  • {c.display_name}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: countries.length - 2, + })} + +
    + )} +
    + ) +} + +const EditRegionSchema = zod.object({ + name: zod.string().min(1), + includes_tax: zod.boolean(), + currency_code: zod.string(), + countries: zod.array(zod.string()), +}) + +const EditRegionDrawer = ({ region }: { region: Region }) => { + const { t } = useTranslation() + const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id) + + const form = useForm>({ + defaultValues: { + name: region.name, + currency_code: region.currency_code, + includes_tax: region.includes_tax, + countries: region.countries.map((c) => c.iso_2), + }, + }) + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync({ + name: values.name, + currency_code: values.currency_code, + includes_tax: values.includes_tax, + }) + }) + + return ( + + + + {t("regions.editRegion")} + + + + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx new file mode 100644 index 0000000000..488fe486a5 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/components/region-shipping-option-section/region-shipping-option-section.tsx @@ -0,0 +1,156 @@ +import { Region, ShippingOption } from "@medusajs/medusa" +import { Container, StatusBadge, Table, clx } from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminShippingOptions } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +type RegionShippingOptionSectionProps = { + region: Region +} + +const PAGE_SIZE = 20 + +// TODO: Need to fix pagination and search for shipping options +export const RegionShippingOptionSection = ({ + region, +}: RegionShippingOptionSectionProps) => { + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const { shipping_options, count, isError, error, isLoading } = + useAdminShippingOptions({ + region_id: region.id, + }) + + const [rowSelection, setRowSelection] = useState({}) + + const columns = useShippingOptionColumns() + + const table = useReactTable({ + data: shipping_options ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }) + + if (isLoading) { + return
    Loading...
    + } + + if (isError) { + throw error + } + + return ( + +
    {/* Filters go here */}
    + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
    + +
    + ) +} + +const columnHelper = createColumnHelper() + +const useShippingOptionColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: (cell) => cell.getValue(), + }), + columnHelper.accessor("admin_only", { + header: t("fields.availability"), + cell: (cell) => { + const value = cell.getValue() + + return ( + + {value ? t("general.admin") : t("general.store")} + + ) + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts new file mode 100644 index 0000000000..17c680a88e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/index.ts @@ -0,0 +1,2 @@ +export { regionLoader as loader } from "./loader" +export { RegionDetail as Component } from "./region-detail" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts b/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts new file mode 100644 index 0000000000..e8c97d2216 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/loader.ts @@ -0,0 +1,23 @@ +import { AdminProductsRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminRegionKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const regionQuery = (id: string) => ({ + queryKey: adminRegionKeys.detail(id), + queryFn: async () => medusa.admin.regions.retrieve(id), +}) + +export const regionLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = regionQuery(id!) + + console.log("regionLoader", query) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx new file mode 100644 index 0000000000..eeca1c0a65 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-detail/region-detail.tsx @@ -0,0 +1,42 @@ +import { useAdminRegion } from "medusa-react" +import { Outlet, useNavigate, useParams } from "react-router-dom" + +import { JsonViewSection } from "../../../components/common/json-view-section" +import { RegionGeneralSection } from "./components/region-general-section" +import { RegionShippingOptionSection } from "./components/region-shipping-option-section/region-shipping-option-section" + +export const RegionDetail = () => { + const { id } = useParams() + const { region, isLoading, isError, error } = useAdminRegion(id!) + const navigate = useNavigate() + + // TODO: Move to loading.tsx and set as Suspense fallback for the route + if (isLoading) { + return
    Loading
    + } + + // TODO: Move to error.tsx and set as ErrorBoundary for the route + if (isError || !region) { + const err = error ? JSON.parse(JSON.stringify(error)) : null + return ( +
    + {(err as Error & { status: number })?.status === 404 ? ( +
    Not found
    + ) : ( +
    Something went wrong!
    + )} +
    + ) + } + + console.log("RegionDetail") + + return ( +
    + + + + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx new file mode 100644 index 0000000000..00c008540a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/edit-region-form.tsx @@ -0,0 +1,105 @@ +import { Region } from "@medusajs/medusa" +import { Button, Drawer, Input, Switch } from "@medusajs/ui" +import { useAdminUpdateRegion } from "medusa-react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type EditRegionFormProps = { + region: Region +} + +const EditRegionSchema = zod.object({ + name: zod.string().min(1), + includes_tax: zod.boolean(), + currency_code: zod.string(), + countries: zod.array(zod.string()), +}) + +export const EditRegionForm = ({ region }: EditRegionFormProps) => { + const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id) + + const form = useForm>({ + defaultValues: { + name: region.name, + currency_code: region.currency_code, + includes_tax: region.includes_tax, + countries: region.countries.map((c) => c.iso_2), + }, + }) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync({ + name: values.name, + currency_code: values.currency_code, + includes_tax: values.includes_tax, + }) + }) + + return ( +
    + + +
    + { + return ( + + {t("fields.name")} + + + + + + ) + }} + /> + { + return ( + +
    +
    + + {t("fields.taxInclusivePricing")} + + {t("regions.taxInclusiveHint")} +
    + + + +
    + +
    + ) + }} + /> +
    +
    + +
    + + + + +
    +
    +
    + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts new file mode 100644 index 0000000000..b3dbbd11b0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/components/edit-region-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-region-form" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts new file mode 100644 index 0000000000..931fd4c574 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/index.ts @@ -0,0 +1 @@ +export { RegionEdit as Component } from "./region-edit" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx new file mode 100644 index 0000000000..9cc8af616d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-edit/region-edit.tsx @@ -0,0 +1,49 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminRegion } from "medusa-react" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { json, useNavigate, useParams } from "react-router-dom" + +import { EditRegionForm } from "./components/edit-region-form" + +export const RegionEdit = () => { + const [open, setOpen] = useState(false) + const { id } = useParams() + const { region, isLoading, isError, error } = useAdminRegion(id!) + const navigate = useNavigate() + + useEffect(() => { + setOpen(true) + }, []) + + const onOpenChange = (open: boolean) => { + if (!open) { + setTimeout(() => { + navigate(`/settings/regions/${id}`, { replace: true }) + }, 200) + } + + setOpen(open) + } + + const { t } = useTranslation() + + if (isError) { + throw error + } + + if (!region && !isLoading) { + throw json("An unknown error has occured", 500) + } + + return ( + + + + {t("regions.editRegion")} + + {region && } + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts new file mode 100644 index 0000000000..e91f99376a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/index.ts @@ -0,0 +1 @@ +export * from "./region-list-table" diff --git a/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx new file mode 100644 index 0000000000..e90b7b7987 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-list/components/region-list-table/region-list-table.tsx @@ -0,0 +1,302 @@ +import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" +import { Region } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + Tooltip, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminRegions } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +const PAGE_SIZE = 50 + +export const RegionListTable = () => { + const navigate = useNavigate() + const { t } = useTranslation() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { regions, count, isLoading, isError, error } = useAdminRegions({ + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + }) + + const columns = useColumns() + + const table = useReactTable({ + data: regions ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + if (isError) { + throw error + } + + return ( + +
    + {t("regions.domain")} + + + +
    + + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/settings/regions/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
    + +
    + ) +} + +const RegionActions = ({ region }: { region: Region }) => { + const { t } = useTranslation() + + return ( + + e.stopPropagation()}> + + + + + + + e.stopPropagation()}> +
    + + {t("general.edit")} +
    +
    + + + e.stopPropagation()}> +
    + + {t("general.delete")} +
    +
    +
    +
    + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: (cell) => cell.getValue(), + }), + columnHelper.accessor("countries", { + header: t("fields.countries"), + cell: (cell) => { + const countries = cell.getValue() + + const displayValue = countries + .slice(0, 2) + .map((c) => c.display_name) + .join(", ") + + const additionalCountries = countries + .slice(2) + .map((c) => c.display_name) + + return ( +
    + {displayValue} + {additionalCountries.length > 0 && ( + + {additionalCountries.map((c) => ( +
  • {c}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: additionalCountries.length, + })} + +
    + )} +
    + ) + }, + }), + columnHelper.accessor("payment_providers", { + header: t("fields.paymentProviders"), + cell: (cell) => { + const providers = cell.getValue() + + const displayValue = providers + .slice(0, 2) + .map((p) => p.id) + .join(", ") + + const additionalProviders = providers.slice(2).map((c) => c.id) + + return ( +
    + {displayValue} + {additionalProviders.length > 0 && ( + + {additionalProviders.map((c) => ( +
  • {c}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: additionalProviders.length, + })} + +
    + )} +
    + ) + }, + }), + columnHelper.accessor("fulfillment_providers", { + header: t("fields.fulfillmentProviders"), + cell: (cell) => { + const providers = cell.getValue() + + const displayValue = providers + .slice(0, 2) + .map((p) => p.id) + .join(", ") + + const additionalProviders = providers.slice(2).map((c) => c.id) + + return ( +
    + {displayValue} + {additionalProviders.length > 0 && ( + + {additionalProviders.map((c) => ( +
  • {c}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: additionalProviders.length, + })} + +
    + )} +
    + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return + }, + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-list/index.ts b/packages/admin-next/dashboard/src/routes/regions/region-list/index.ts similarity index 100% rename from packages/admin-next/dashboard/src/routes/regions/views/region-list/index.ts rename to packages/admin-next/dashboard/src/routes/regions/region-list/index.ts diff --git a/packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx b/packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx new file mode 100644 index 0000000000..fb22082bb6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/regions/region-list/region-list.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom" +import { RegionListTable } from "./components/region-list-table" + +export const RegionList = () => { + console.log("RegionList") + + return ( +
    + + +
    + ) +} diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts b/packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts deleted file mode 100644 index cd1dedc129..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/views/region-details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RegionDetails as Component } from "./region-details" diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx b/packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx deleted file mode 100644 index 2f00216e38..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/views/region-details/region-details.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { EllipsisHorizontal } from "@medusajs/icons" -import { - Badge, - Container, - Heading, - IconButton, - Text, - Tooltip, -} from "@medusajs/ui" -import { useAdminRegion } from "medusa-react" -import { useTranslation } from "react-i18next" -import { useParams } from "react-router-dom" - -export const RegionDetails = () => { - const { id } = useParams() - const { region, isLoading, isError, error } = useAdminRegion(id!) - - const { t } = useTranslation() - - // TODO: Move to loading.tsx and set as Suspense fallback for the route - if (isLoading) { - return
    Loading
    - } - - // TODO: Move to error.tsx and set as ErrorBoundary for the route - if (isError || !region) { - const err = error ? JSON.parse(JSON.stringify(error)) : null - return ( -
    - {(err as Error & { status: number })?.status === 404 ? ( -
    Not found
    - ) : ( -
    Something went wrong!
    - )} -
    - ) - } - - return ( -
    - -
    -
    - {region.name} -
    - - {region.countries - .slice(0, 2) - .map((c) => c.display_name) - .join(", ")} - - {region.countries.length > 2 && ( - - {region.countries.slice(2).map((c) => ( -
  • {c.display_name}
  • - ))} - - } - > - - {t("general.plusCountMore", { - count: region.countries.length - 2, - })} - -
    - )} -
    -
    - - - -
    -
    - - Currency - -
    - - {region.currency_code} - - - {region.currency.name} - -
    -
    -
    - - Default Tax Rate - - - {region.tax_rate} - -
    -
    - - Default Tax Code - - - {region.tax_code ?? "-"} - -
    -
    - - Tax Inclusive Pricing - - - {region.includes_tax ? t("general.enabled") : t("general.disabled")} - -
    -
    - - Payment Providers - - - {region.payment_providers.length > 0 - ? region.payment_providers.map((p) => p.id).join(", ") - : "-"} - -
    -
    - - Fulfillment Providers - - - {region.fulfillment_providers.length > 0 - ? region.fulfillment_providers.map((p) => p.id).join(", ") - : "-"} - -
    -
    - - -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx b/packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx deleted file mode 100644 index 84c4592430..0000000000 --- a/packages/admin-next/dashboard/src/routes/regions/views/region-list/region-list.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useAdminRegions } from "medusa-react" -import { Link } from "react-router-dom" - -export const RegionList = () => { - const { regions, isLoading, isError, error } = useAdminRegions() - - if (isLoading) { - return
    Loading...
    - } - - if (isError || !regions) { - return
    Error
    - } - - return ( -
    - {regions.map((region) => { - return ( -
    - {region.name} -
    - ) - })} -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts b/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts deleted file mode 100644 index a2677176a7..0000000000 --- a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sales-channel-details-section" diff --git a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx b/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx deleted file mode 100644 index 559c432748..0000000000 --- a/packages/admin-next/dashboard/src/routes/sales-channels/components/sales-channel-details-section/sales-channel-details-section.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons" -import { SalesChannel } from "@medusajs/medusa" -import { - Button, - Container, - Drawer, - DropdownMenu, - Heading, - IconButton, - Input, - StatusBadge, - Switch, - Text, - Textarea, -} from "@medusajs/ui" -import { useAdminUpdateSalesChannel } from "medusa-react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import * as zod from "zod" - -import { Form } from "../../../../components/common/form" - -type Props = { - salesChannel: SalesChannel -} - -export const SalesChannelDetailsSection = ({ salesChannel }: Props) => { - const [drawerOpen, setDrawerOpen] = useState(false) - const { t } = useTranslation() - - return ( -
    - -
    -
    - {salesChannel.name} - - {salesChannel.description} - -
    -
    - - {t( - `general.${salesChannel.is_disabled ? "disabled" : "enabled"}` - )} - - - - - - - - - setDrawerOpen(!drawerOpen)} - > - - {t("general.edit")} - - - - - {t("general.delete")} - - - -
    -
    -
    - -
    - ) -} - -const EditSalesChannelDetailsSchema = zod.object({ - name: zod.string().min(1), - description: zod.string().optional(), - is_active: zod.boolean(), -}) - -type DrawerProps = { - salesChannel: SalesChannel - open: boolean - setOpen: (open: boolean) => void -} - -const EditSalesChannelDetailsDrawer = ({ - salesChannel, - open, - setOpen, -}: DrawerProps) => { - const form = useForm>({ - defaultValues: { - name: salesChannel.name, - description: salesChannel.description ?? "", - is_active: !salesChannel.is_disabled, - }, - resolver: zodResolver(EditSalesChannelDetailsSchema), - }) - const { mutateAsync, isLoading } = useAdminUpdateSalesChannel(salesChannel.id) - const { t } = useTranslation() - - const onSubmit = form.handleSubmit(async (values) => { - await mutateAsync( - { - name: values.name, - description: values.description ?? undefined, - is_disabled: !values.is_active, - }, - { - onSuccess: () => { - setOpen(false) - }, - } - ) - }) - - return ( - -
    - - - {t("salesChannels.editSalesChannel")} - - - { - return ( - - {t("fields.name")} - - - - - - ) - }} - /> - { - return ( - - {t("fields.description")} - -