@vendure/dashboard 3.2.3 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/plugin/utils/ast-utils.d.ts +10 -0
  2. package/dist/plugin/utils/ast-utils.js +96 -0
  3. package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
  4. package/dist/plugin/utils/ast-utils.spec.js +120 -0
  5. package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
  6. package/dist/plugin/utils/config-loader.js +325 -0
  7. package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
  8. package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +7 -1
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
  10. package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
  11. package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
  12. package/dist/plugin/vite-plugin-config-loader.js +18 -9
  13. package/dist/plugin/vite-plugin-config.js +4 -6
  14. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  15. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  16. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  17. package/package.json +16 -11
  18. package/src/app/app-providers.tsx +9 -9
  19. package/src/app/main.tsx +1 -1
  20. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  21. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  22. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  23. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  24. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  25. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  26. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  27. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  30. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  31. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
  32. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  33. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  34. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  35. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
  36. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  37. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
  38. package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
  39. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
  40. package/src/app/routes/_authenticated.tsx +12 -1
  41. package/src/app/styles.css +15 -0
  42. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  43. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  44. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  45. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  46. package/src/lib/components/data-table/data-table-types.ts +1 -0
  47. package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
  48. package/src/lib/components/data-table/data-table.tsx +49 -44
  49. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  50. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  51. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  52. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  53. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  54. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  55. package/src/lib/components/data-table/refresh-button.tsx +25 -0
  56. package/src/lib/components/layout/nav-user.tsx +20 -15
  57. package/src/lib/components/layout/prerelease-popup.tsx +1 -5
  58. package/src/lib/components/shared/alerts.tsx +19 -1
  59. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  60. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  61. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  62. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  63. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  64. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  65. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  66. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  67. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  68. package/src/lib/components/shared/customer-selector.tsx +13 -14
  69. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  70. package/src/lib/components/shared/entity-assets.tsx +3 -3
  71. package/src/lib/components/shared/error-page.tsx +2 -2
  72. package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
  73. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
  74. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  75. package/src/lib/components/shared/vendure-image.tsx +1 -1
  76. package/src/lib/components/ui/calendar.tsx +508 -63
  77. package/src/lib/framework/alert/alert-extensions.tsx +31 -0
  78. package/src/lib/framework/alert/alert-item.tsx +47 -0
  79. package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
  80. package/src/lib/framework/alert/types.ts +13 -0
  81. package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
  82. package/src/lib/framework/defaults.ts +34 -0
  83. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  84. package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
  85. package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
  86. package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
  87. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  88. package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
  89. package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
  90. package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
  91. package/src/lib/framework/page/detail-page.tsx +62 -9
  92. package/src/lib/framework/page/list-page.tsx +42 -4
  93. package/src/lib/framework/page/page-api.ts +1 -1
  94. package/src/lib/framework/page/use-detail-page.ts +82 -0
  95. package/src/lib/framework/registry/registry-types.ts +6 -2
  96. package/src/lib/graphql/fragments.tsx +8 -0
  97. package/src/lib/graphql/graphql-env.d.ts +25 -9
  98. package/src/lib/hooks/use-auth.tsx +13 -1
  99. package/src/lib/hooks/use-channel.ts +13 -0
  100. package/src/lib/hooks/use-local-format.ts +28 -1
  101. package/src/lib/hooks/use-page.tsx +2 -3
  102. package/src/lib/hooks/use-permissions.ts +13 -0
  103. package/src/lib/index.ts +7 -8
  104. package/src/lib/providers/auth.tsx +22 -9
  105. package/src/lib/providers/channel-provider.tsx +9 -1
  106. package/src/lib/providers/server-config.tsx +7 -1
  107. package/src/lib/providers/user-settings.tsx +24 -0
  108. package/vite/utils/ast-utils.spec.ts +128 -0
  109. package/vite/utils/ast-utils.ts +119 -0
  110. package/vite/utils/config-loader.ts +410 -0
  111. package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
  112. package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
  113. package/vite/vite-plugin-admin-api-schema.ts +2 -12
  114. package/vite/vite-plugin-config-loader.ts +25 -13
  115. package/vite/vite-plugin-config.ts +1 -0
  116. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  117. package/vite/vite-plugin-gql-tada.ts +2 -2
  118. package/vite/vite-plugin-ui-config.ts +3 -2
  119. package/dist/plugin/config-loader.js +0 -141
  120. package/src/lib/components/shared/asset-preview.tsx +0 -345
  121. package/src/lib/components/ui/avatar.tsx +0 -38
  122. package/vite/config-loader.ts +0 -181
  123. /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
@@ -1,7 +1,19 @@
1
1
  import * as React from 'react';
2
2
  import { AuthContext } from '../providers/auth.js';
3
3
 
4
-
4
+ /**
5
+ * @description
6
+ * **Status: Developer Preview**
7
+ *
8
+ * Provides access to the {@link ChannelContext} which contains information
9
+ * about the active channel.
10
+ *
11
+ *
12
+ * @docsCategory hooks
13
+ * @docsPage useAuth
14
+ * @docsWeight 0
15
+ * @since 3.3.0
16
+ */
5
17
  export function useAuth() {
6
18
  const context = React.useContext(AuthContext);
7
19
  if (!context) {
@@ -3,6 +3,19 @@ import * as React from 'react';
3
3
 
4
4
  // Hook to use the channel context
5
5
 
6
+ /**
7
+ * @description
8
+ * **Status: Developer Preview**
9
+ *
10
+ * Provides access to the {@link ChannelContext} which contains information
11
+ * about the active channel.
12
+ *
13
+ *
14
+ * @docsCategory hooks
15
+ * @docsPage useChannel
16
+ * @docsWeight 0
17
+ * @since 3.3.0
18
+ */
6
19
  export function useChannel() {
7
20
  const context = React.useContext(ChannelContext);
8
21
  if (context === undefined) {
@@ -1,4 +1,3 @@
1
- import { useLingui } from '@lingui/react';
2
1
  import { useCallback, useMemo } from 'react';
3
2
 
4
3
  import { useServerConfig } from './use-server-config.js';
@@ -19,6 +18,10 @@ import { useServerConfig } from './use-server-config.js';
19
18
  * toMajorUnits,
20
19
  * } = useLocalFormat();
21
20
  * ```
21
+ *
22
+ * @docsCategory hooks
23
+ * @docsPage useLocalFormat
24
+ * @docsWeight 0
22
25
  */
23
26
  export function useLocalFormat() {
24
27
  const { moneyStrategyPrecision } = useServerConfig() ?? { moneyStrategyPrecision: 2 };
@@ -65,6 +68,29 @@ export function useLocalFormat() {
65
68
  [locale],
66
69
  );
67
70
 
71
+ const formatRelativeDate = useCallback(
72
+ (value: string | Date, options?: Intl.RelativeTimeFormatOptions) => {
73
+ const now = new Date();
74
+ const date = new Date(value);
75
+ const diffSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
76
+ // if less than 1 minute, use seconds. Else use minutes, hours, days, months, years
77
+ if (diffSeconds < 60) {
78
+ return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'seconds');
79
+ } else if (diffSeconds < 3600) {
80
+ return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'minutes');
81
+ } else if (diffSeconds < 86400) {
82
+ return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'hours');
83
+ } else if (diffSeconds < 2592000) {
84
+ return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'days');
85
+ } else if (diffSeconds < 31536000) {
86
+ return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'months');
87
+ } else {
88
+ return new Intl.RelativeTimeFormat(locale, options).format(diffSeconds * -1, 'years');
89
+ }
90
+ },
91
+ [locale],
92
+ );
93
+
68
94
  const formatLanguageName = useCallback(
69
95
  (value: string): string => {
70
96
  try {
@@ -111,6 +137,7 @@ export function useLocalFormat() {
111
137
  formatCurrency,
112
138
  formatNumber,
113
139
  formatDate,
140
+ formatRelativeDate,
114
141
  formatLanguageName,
115
142
  formatCurrencyName,
116
143
  toMajorUnits,
@@ -1,11 +1,10 @@
1
- import { PageProvider } from "@/framework/layout-engine/page-layout.js";
2
1
  import { useContext } from "react";
2
+ import { PageContext } from '@/framework/layout-engine/page-provider.js';
3
3
 
4
4
  export function usePage() {
5
- const page = useContext(PageProvider);
5
+ const page = useContext(PageContext);
6
6
  if (!page) {
7
7
  throw new Error('PageProvider not found');
8
8
  }
9
9
  return page;
10
10
  }
11
-
@@ -3,6 +3,19 @@ import { Permission } from '@vendure/common/lib/generated-types';
3
3
 
4
4
  import { useUserSettings } from './use-user-settings.js';
5
5
 
6
+ /**
7
+ * @description
8
+ * **Status: Developer Preview**
9
+ *
10
+ * Returns a `hasPermissions` function that can be used to determine whether the active user
11
+ * has the given permissions on the active channel.
12
+ *
13
+ *
14
+ * @docsCategory hooks
15
+ * @docsPage usePermissions
16
+ * @docsWeight 0
17
+ * @since 3.3.0
18
+ */
6
19
  export function usePermissions() {
7
20
  const { channels } = useAuth();
8
21
  const { settings } = useUserSettings();
package/src/lib/index.ts CHANGED
@@ -28,10 +28,11 @@ export * from './components/layout/nav-user.js';
28
28
  export * from './components/login/login-form.js';
29
29
  export * from './components/shared/alerts.js';
30
30
  export * from './components/shared/animated-number.js';
31
- export * from './components/shared/asset-gallery.js';
32
- export * from './components/shared/asset-picker-dialog.js';
33
- export * from './components/shared/asset-preview-dialog.js';
34
- export * from './components/shared/asset-preview.js';
31
+ export * from './components/shared/asset/asset-gallery.js';
32
+ export * from './components/shared/asset/asset-picker-dialog.js';
33
+ export * from './components/shared/asset/asset-preview-dialog.js';
34
+ export * from './components/shared/asset/asset-preview.js';
35
+ export * from './components/shared/asset/focal-point-control.js';
35
36
  export * from './components/shared/assigned-facet-values.js';
36
37
  export * from './components/shared/channel-code-label.js';
37
38
  export * from './components/shared/channel-selector.js';
@@ -50,7 +51,6 @@ export * from './components/shared/entity-assets.js';
50
51
  export * from './components/shared/error-page.js';
51
52
  export * from './components/shared/facet-value-chip.js';
52
53
  export * from './components/shared/facet-value-selector.js';
53
- export * from './components/shared/focal-point-control.js';
54
54
  export * from './components/shared/form-field-wrapper.js';
55
55
  export * from './components/shared/history-timeline/history-entry.js';
56
56
  export * from './components/shared/history-timeline/history-note-checkbox.js';
@@ -74,7 +74,6 @@ export * from './components/shared/zone-selector.js';
74
74
  export * from './components/ui/accordion.js';
75
75
  export * from './components/ui/alert-dialog.js';
76
76
  export * from './components/ui/alert.js';
77
- export * from './components/ui/avatar.js';
78
77
  export * from './components/ui/badge.js';
79
78
  export * from './components/ui/breadcrumb.js';
80
79
  export * from './components/ui/button.js';
@@ -113,8 +112,8 @@ export * from './framework/dashboard-widget/metrics-widget/index.js';
113
112
  export * from './framework/dashboard-widget/metrics-widget/metrics-widget.graphql.js';
114
113
  export * from './framework/dashboard-widget/orders-summary/index.js';
115
114
  export * from './framework/dashboard-widget/orders-summary/order-summary-widget.graphql.js';
116
- export * from './framework/dashboard-widget/widget-extensions.js';
117
115
  export * from './framework/dashboard-widget/types.js';
116
+ export * from './framework/dashboard-widget/widget-extensions.js';
118
117
  export * from './framework/defaults.js';
119
118
  export * from './framework/document-introspection/add-custom-fields.js';
120
119
  export * from './framework/document-introspection/get-document-structure.js';
@@ -124,9 +123,9 @@ export * from './framework/extension-api/extension-api-types.js';
124
123
  export * from './framework/extension-api/use-dashboard-extensions.js';
125
124
  export * from './framework/form-engine/form-schema-tools.js';
126
125
  export * from './framework/form-engine/use-generated-form.js';
126
+ export * from './framework/layout-engine/layout-extensions.js';
127
127
  export * from './framework/layout-engine/location-wrapper.js';
128
128
  export * from './framework/layout-engine/page-layout.js';
129
- export * from './framework/layout-engine/layout-extensions.js';
130
129
  export * from './framework/nav-menu/nav-menu-extensions.js';
131
130
  export * from './framework/page/detail-page-route-loader.js';
132
131
  export * from './framework/page/detail-page.js';
@@ -3,7 +3,17 @@ import { ResultOf, graphql } from '@/graphql/graphql.js';
3
3
  import { useUserSettings } from '@/hooks/use-user-settings.js';
4
4
  import { useMutation, useQuery } from '@tanstack/react-query';
5
5
  import * as React from 'react';
6
+ import { useAuth } from '@/hooks/use-auth.js';
6
7
 
8
+ /**
9
+ * @description
10
+ * **Status: Developer Preview**
11
+ *
12
+ * @docsCategory hooks
13
+ * @docsPage useAuth
14
+ * @docsWeight 0
15
+ * @since 3.3.0
16
+ */
7
17
  export interface AuthContext {
8
18
  status: 'authenticated' | 'verifying' | 'unauthenticated';
9
19
  authenticationError?: string;
@@ -69,17 +79,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
69
79
  const onLogoutSuccessFn = React.useRef<() => void>(() => {});
70
80
  const isAuthenticated = status === 'authenticated';
71
81
 
72
- const { data: currentUserData, isLoading } = useQuery({
73
- queryKey: ['currentUser'],
82
+ const { data: currentUserData, isLoading , error: currentUserError} = useQuery({
83
+ queryKey: ['currentUser', isAuthenticated],
74
84
  queryFn: () => api.query(CurrentUserQuery),
75
85
  retry: false,
86
+ staleTime: 1000,
76
87
  });
77
88
 
89
+ const currentUser = currentUserError ? undefined : currentUserData;
90
+
78
91
  React.useEffect(() => {
79
- if (!settings.activeChannelId && currentUserData?.me?.channels?.length) {
80
- setActiveChannelId(currentUserData.me.channels[0].id);
92
+ if (!settings.activeChannelId && currentUser?.me?.channels?.length) {
93
+ setActiveChannelId(currentUser.me.channels[0].id);
81
94
  }
82
- }, [settings.activeChannelId, currentUserData?.me?.channels]);
95
+ }, [settings.activeChannelId, currentUser?.me?.channels]);
83
96
 
84
97
  const loginMutationFn = api.mutate(LoginMutation);
85
98
  const loginMutation = useMutation({
@@ -123,7 +136,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
123
136
 
124
137
  React.useEffect(() => {
125
138
  if (!isLoading) {
126
- if (currentUserData?.me?.id) {
139
+ if (currentUser?.me?.id) {
127
140
  setStatus('authenticated');
128
141
  } else {
129
142
  setStatus('unauthenticated');
@@ -131,7 +144,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
131
144
  } else {
132
145
  setStatus('verifying');
133
146
  }
134
- }, [isLoading, currentUserData]);
147
+ }, [isLoading, currentUser]);
135
148
 
136
149
  return (
137
150
  <AuthContext.Provider
@@ -139,8 +152,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
139
152
  isAuthenticated,
140
153
  authenticationError,
141
154
  status,
142
- user: currentUserData?.activeAdministrator,
143
- channels: currentUserData?.me?.channels,
155
+ user: currentUser?.activeAdministrator,
156
+ channels: currentUser?.me?.channels,
144
157
  login,
145
158
  logout,
146
159
  }}
@@ -41,7 +41,14 @@ const ChannelsQuery = graphql(
41
41
  type ActiveChannel = ResultOf<typeof ChannelsQuery>['activeChannel'];
42
42
  type Channel = ResultOf<typeof channelFragment>;
43
43
 
44
- // Define the context interface
44
+ /**
45
+ * @description
46
+ * **Status: Developer Preview**
47
+ *
48
+ * @docsCategory hooks
49
+ * @docsPage useChannel
50
+ * @since 3.3.0
51
+ */
45
52
  export interface ChannelContext {
46
53
  activeChannel: ActiveChannel | undefined;
47
54
  channels: Channel[];
@@ -73,6 +80,7 @@ export function ChannelProvider({ children }: { children: React.ReactNode }) {
73
80
  const { data: channelsData, isLoading: isChannelsLoading } = useQuery({
74
81
  queryKey: ['channels'],
75
82
  queryFn: () => api.query(ChannelsQuery),
83
+ retry: false,
76
84
  });
77
85
 
78
86
  // Set the selected channel and update localStorage
@@ -1,5 +1,6 @@
1
1
  import { api } from '@/graphql/api.js';
2
2
  import { graphql } from '@/graphql/graphql.js';
3
+ import { useAuth } from '@/hooks/use-auth.js';
3
4
  import { useQuery } from '@tanstack/react-query';
4
5
  import { ResultOf } from 'gql.tada';
5
6
  import React from 'react';
@@ -260,9 +261,14 @@ export interface ServerConfig {
260
261
 
261
262
  // create a provider for the global settings
262
263
  export const ServerConfigProvider = ({ children }: { children: React.ReactNode }) => {
264
+ const { user } = useAuth();
265
+ const queryKey = ['getServerConfig', user?.id];
263
266
  const { data } = useQuery({
264
- queryKey: ['getServerConfig'],
267
+ queryKey,
265
268
  queryFn: () => api.query(getServerConfigDocument),
269
+ retry: false,
270
+ enabled: !!user?.id,
271
+ staleTime: 1000,
266
272
  });
267
273
  const value: ServerConfig = {
268
274
  availableLanguages: data?.globalSettings.availableLanguages ?? [],
@@ -1,5 +1,13 @@
1
1
  import React, { createContext, useState, useEffect } from 'react';
2
2
  import { Theme } from './theme-provider.js';
3
+ import { ColumnFiltersState } from '@tanstack/react-table';
4
+
5
+ export interface TableSettings {
6
+ columnVisibility?: Record<string, boolean>;
7
+ columnOrder?: string[];
8
+ columnFilters?: ColumnFiltersState;
9
+ pageSize?: number;
10
+ }
3
11
 
4
12
  export interface UserSettings {
5
13
  displayLanguage: string;
@@ -11,6 +19,7 @@ export interface UserSettings {
11
19
  activeChannelId: string;
12
20
  devMode: boolean;
13
21
  hasSeenOnboarding: boolean;
22
+ tableSettings?: Record<string, TableSettings>;
14
23
  }
15
24
 
16
25
  const defaultSettings: UserSettings = {
@@ -23,6 +32,7 @@ const defaultSettings: UserSettings = {
23
32
  activeChannelId: '',
24
33
  devMode: false,
25
34
  hasSeenOnboarding: false,
35
+ tableSettings: {},
26
36
  };
27
37
 
28
38
  export interface UserSettingsContextType {
@@ -36,6 +46,11 @@ export interface UserSettingsContextType {
36
46
  setActiveChannelId: (channelId: string) => void;
37
47
  setDevMode: (devMode: boolean) => void;
38
48
  setHasSeenOnboarding: (hasSeen: boolean) => void;
49
+ setTableSettings: <K extends keyof TableSettings>(
50
+ tableId: string,
51
+ key: K,
52
+ value: TableSettings[K],
53
+ ) => void;
39
54
  }
40
55
 
41
56
  export const UserSettingsContext = createContext<UserSettingsContextType | undefined>(undefined);
@@ -83,6 +98,15 @@ export const UserSettingsProvider: React.FC<React.PropsWithChildren<{}>> = ({ ch
83
98
  setActiveChannelId: channelId => updateSetting('activeChannelId', channelId),
84
99
  setDevMode: devMode => updateSetting('devMode', devMode),
85
100
  setHasSeenOnboarding: hasSeen => updateSetting('hasSeenOnboarding', hasSeen),
101
+ setTableSettings: (tableId, key, value) => {
102
+ setSettings(prev => ({
103
+ ...prev,
104
+ tableSettings: {
105
+ ...prev.tableSettings,
106
+ [tableId]: { ...(prev.tableSettings?.[tableId] || {}), [key]: value },
107
+ },
108
+ }));
109
+ },
86
110
  };
87
111
 
88
112
  return <UserSettingsContext.Provider value={contextValue}>{children}</UserSettingsContext.Provider>;
@@ -0,0 +1,128 @@
1
+ import ts from 'typescript';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { findConfigExport, getPluginInfo } from './ast-utils.js';
5
+
6
+ describe('getPluginInfo', () => {
7
+ it('should return undefined when no plugin class is found', () => {
8
+ const sourceText = `
9
+ class NotAPlugin {
10
+ constructor() {}
11
+ }
12
+ `;
13
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
14
+ const result = getPluginInfo(sourceFile);
15
+ expect(result).toBeUndefined();
16
+ });
17
+
18
+ it('should return plugin info when a valid plugin class is found', () => {
19
+ const sourceText = `
20
+ @VendurePlugin({
21
+ imports: [],
22
+ providers: []
23
+ })
24
+ class TestPlugin {
25
+ constructor() {}
26
+ }
27
+ `;
28
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
29
+ const result = getPluginInfo(sourceFile);
30
+ expect(result).toEqual({
31
+ name: 'TestPlugin',
32
+ pluginPath: 'path/to',
33
+ dashboardEntryPath: undefined,
34
+ });
35
+ });
36
+
37
+ it('should handle multiple classes but only return the plugin one', () => {
38
+ const sourceText = `
39
+ class NotAPlugin {
40
+ constructor() {}
41
+ }
42
+
43
+ @VendurePlugin({
44
+ imports: [],
45
+ providers: []
46
+ })
47
+ class TestPlugin {
48
+ constructor() {}
49
+ }
50
+
51
+ class AnotherClass {
52
+ constructor() {}
53
+ }
54
+ `;
55
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
56
+ const result = getPluginInfo(sourceFile);
57
+ expect(result).toEqual({
58
+ name: 'TestPlugin',
59
+ pluginPath: 'path/to',
60
+ dashboardEntryPath: undefined,
61
+ });
62
+ });
63
+
64
+ it('should determine the dashboard entry path when it is provided', () => {
65
+ const sourceText = `
66
+ @VendurePlugin({
67
+ imports: [],
68
+ providers: [],
69
+ dashboard: './dashboard/index.tsx',
70
+ })
71
+ class TestPlugin {
72
+ constructor() {}
73
+ }
74
+ `;
75
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
76
+ const result = getPluginInfo(sourceFile);
77
+ expect(result).toEqual({
78
+ name: 'TestPlugin',
79
+ pluginPath: 'path/to',
80
+ dashboardEntryPath: './dashboard/index.tsx',
81
+ });
82
+ });
83
+ });
84
+
85
+ describe('findConfigExport', () => {
86
+ it('should return undefined when no VendureConfig export is found', () => {
87
+ const sourceText = `
88
+ export const notConfig = {
89
+ some: 'value'
90
+ };
91
+ `;
92
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
93
+ const result = findConfigExport(sourceFile);
94
+ expect(result).toBeUndefined();
95
+ });
96
+
97
+ it('should find exported variable with VendureConfig type', () => {
98
+ const sourceText = `
99
+ import { VendureConfig } from '@vendure/core';
100
+
101
+ export const config: VendureConfig = {
102
+ authOptions: {
103
+ tokenMethod: 'bearer'
104
+ }
105
+ };
106
+ `;
107
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
108
+ const result = findConfigExport(sourceFile);
109
+ expect(result).toBe('config');
110
+ });
111
+
112
+ it('should find exported variable with VendureConfig type among other exports', () => {
113
+ const sourceText = `
114
+ import { VendureConfig } from '@vendure/core';
115
+
116
+ export const otherExport = 'value';
117
+ export const config: VendureConfig = {
118
+ authOptions: {
119
+ tokenMethod: 'bearer'
120
+ }
121
+ };
122
+ export const anotherExport = 123;
123
+ `;
124
+ const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
125
+ const result = findConfigExport(sourceFile);
126
+ expect(result).toBe('config');
127
+ });
128
+ });
@@ -0,0 +1,119 @@
1
+ import path from 'path';
2
+ import ts from 'typescript';
3
+
4
+ import { PluginInfo } from './config-loader.js';
5
+
6
+ /**
7
+ * Get the plugin info from the source file.
8
+ */
9
+ export function getPluginInfo(sourceFile: ts.SourceFile): PluginInfo | undefined {
10
+ const classDeclaration = sourceFile.statements.find(statement => {
11
+ return (
12
+ statement.kind === ts.SyntaxKind.ClassDeclaration &&
13
+ statement.getText().includes('@VendurePlugin(')
14
+ );
15
+ });
16
+ if (classDeclaration) {
17
+ const identifier = classDeclaration.getChildren().find(child => {
18
+ return child.kind === ts.SyntaxKind.Identifier;
19
+ });
20
+ const dashboardEntryPath = classDeclaration
21
+ .getChildren()
22
+ .map(child => {
23
+ if (child.kind === ts.SyntaxKind.SyntaxList) {
24
+ const pluginDecorator = child.getChildren().find(_child => {
25
+ return _child.kind === ts.SyntaxKind.Decorator;
26
+ });
27
+ if (pluginDecorator) {
28
+ const callExpression = findFirstDescendantOfKind(
29
+ pluginDecorator,
30
+ ts.SyntaxKind.CallExpression,
31
+ );
32
+ if (callExpression) {
33
+ const objectLiteral = findFirstDescendantOfKind(
34
+ callExpression,
35
+ ts.SyntaxKind.ObjectLiteralExpression,
36
+ );
37
+ if (objectLiteral && ts.isObjectLiteralExpression(objectLiteral)) {
38
+ // Now find the specific 'dashboard' property
39
+ const dashboardProperty = objectLiteral.properties.find(
40
+ prop =>
41
+ ts.isPropertyAssignment(prop) && prop.name?.getText() === 'dashboard',
42
+ );
43
+
44
+ if (
45
+ dashboardProperty &&
46
+ ts.isPropertyAssignment(dashboardProperty) &&
47
+ ts.isStringLiteral(dashboardProperty.initializer)
48
+ ) {
49
+ const dashboardPath = dashboardProperty.initializer.text;
50
+ return dashboardPath;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ })
57
+ .filter(Boolean)?.[0];
58
+ if (identifier) {
59
+ return {
60
+ name: identifier.getText(),
61
+ pluginPath: path.dirname(sourceFile.fileName),
62
+ dashboardEntryPath,
63
+ };
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Given the AST of a TypeScript file, finds the name of the variable exported as VendureConfig.
70
+ */
71
+ export function findConfigExport(sourceFile: ts.SourceFile): string | undefined {
72
+ let exportedSymbolName: string | undefined;
73
+
74
+ function visit(node: ts.Node) {
75
+ if (
76
+ ts.isVariableStatement(node) &&
77
+ node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
78
+ ) {
79
+ node.declarationList.declarations.forEach(declaration => {
80
+ if (ts.isVariableDeclaration(declaration)) {
81
+ const typeNode = declaration.type;
82
+ if (typeNode && ts.isTypeReferenceNode(typeNode)) {
83
+ const typeName = typeNode.typeName;
84
+ if (ts.isIdentifier(typeName) && typeName.text === 'VendureConfig') {
85
+ if (ts.isIdentifier(declaration.name)) {
86
+ exportedSymbolName = declaration.name.text;
87
+ }
88
+ }
89
+ }
90
+ }
91
+ });
92
+ }
93
+ ts.forEachChild(node, visit);
94
+ }
95
+
96
+ visit(sourceFile);
97
+ return exportedSymbolName;
98
+ }
99
+
100
+ function findFirstDescendantOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined {
101
+ let foundNode: ts.Node | undefined;
102
+
103
+ function visit(_node: ts.Node) {
104
+ if (foundNode) {
105
+ // Stop searching if we already found it
106
+ return;
107
+ }
108
+ if (_node.kind === kind) {
109
+ foundNode = _node;
110
+ return;
111
+ }
112
+ // Recursively visit children
113
+ ts.forEachChild(_node, visit);
114
+ }
115
+
116
+ // Start the traversal from the initial node's children
117
+ ts.forEachChild(node, visit);
118
+ return foundNode;
119
+ }