@vendure/dashboard 3.3.8-master-202507220240 → 3.3.8-master-202507240240

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 (28) hide show
  1. package/dist/plugin/utils/plugin-discovery.d.ts +1 -1
  2. package/dist/plugin/utils/plugin-discovery.js +61 -21
  3. package/dist/plugin/vite-plugin-tailwind-source.js +13 -1
  4. package/dist/plugin/vite-plugin-vendure-dashboard.d.ts +1 -1
  5. package/dist/plugin/vite-plugin-vendure-dashboard.js +2 -2
  6. package/package.json +39 -26
  7. package/src/app/routes/_authenticated/_facets/components/add-facet-value-dialog.tsx +146 -0
  8. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +84 -62
  9. package/src/app/routes/_authenticated/_facets/facets.graphql.ts +9 -0
  10. package/src/lib/components/data-table/use-generated-columns.tsx +20 -5
  11. package/src/lib/components/ui/aspect-ratio.tsx +9 -0
  12. package/src/lib/components/ui/carousel.tsx +241 -0
  13. package/src/lib/components/ui/chart.tsx +351 -0
  14. package/src/lib/components/ui/context-menu.tsx +252 -0
  15. package/src/lib/components/ui/drawer.tsx +133 -0
  16. package/src/lib/components/ui/input-otp.tsx +77 -0
  17. package/src/lib/components/ui/menubar.tsx +274 -0
  18. package/src/lib/components/ui/navigation-menu.tsx +168 -0
  19. package/src/lib/components/ui/progress.tsx +29 -0
  20. package/src/lib/components/ui/radio-group.tsx +45 -0
  21. package/src/lib/components/ui/resizable.tsx +54 -0
  22. package/src/lib/components/ui/slider.tsx +63 -0
  23. package/src/lib/components/ui/toggle-group.tsx +73 -0
  24. package/src/lib/components/ui/toggle.tsx +45 -0
  25. package/src/lib/index.ts +18 -0
  26. package/vite/utils/plugin-discovery.ts +67 -18
  27. package/vite/vite-plugin-tailwind-source.ts +20 -4
  28. package/vite/vite-plugin-vendure-dashboard.ts +3 -3
@@ -12,7 +12,7 @@ export declare function discoverPlugins({ vendureConfigPath, transformTsConfigPa
12
12
  * 1. Local Vendure plugins
13
13
  * 2. All non-local package imports that could contain plugins
14
14
  */
15
- export declare function analyzeSourceFiles(vendureConfigPath: string, logger: Logger, transformTsConfigPathMappings: TransformTsConfigPathMappingsFn): Promise<{
15
+ export declare function analyzeSourceFiles(vendureConfigPath: string, nodeModulesRoot: string, logger: Logger, transformTsConfigPathMappings: TransformTsConfigPathMappingsFn): Promise<{
16
16
  localPluginLocations: Map<string, string>;
17
17
  packageImports: string[];
18
18
  }>;
@@ -8,9 +8,10 @@ import * as ts from 'typescript';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { findTsConfigPaths } from './tsconfig-utils.js';
10
10
  export async function discoverPlugins({ vendureConfigPath, transformTsConfigPathMappings, logger, outputPath, pluginPackageScanner, }) {
11
+ var _a;
11
12
  const plugins = [];
12
13
  // Analyze source files to find local plugins and package imports
13
- const { localPluginLocations, packageImports } = await analyzeSourceFiles(vendureConfigPath, logger, transformTsConfigPathMappings);
14
+ const { localPluginLocations, packageImports } = await analyzeSourceFiles(vendureConfigPath, (_a = pluginPackageScanner === null || pluginPackageScanner === void 0 ? void 0 : pluginPackageScanner.nodeModulesRoot) !== null && _a !== void 0 ? _a : guessNodeModulesRoot(vendureConfigPath, logger), logger, transformTsConfigPathMappings);
14
15
  logger.debug(`[discoverPlugins] Found ${localPluginLocations.size} local plugins: ${JSON.stringify([...localPluginLocations.entries()], null, 2)}`);
15
16
  logger.debug(`[discoverPlugins] Found ${packageImports.length} package imports: ${JSON.stringify(packageImports, null, 2)}`);
16
17
  const filePaths = await findVendurePluginFiles({
@@ -92,12 +93,32 @@ function getDecoratorObjectProps(decorator) {
92
93
  }
93
94
  return [];
94
95
  }
96
+ async function isSymlinkedLocalPackage(packageName, nodeModulesRoot) {
97
+ try {
98
+ const packagePath = path.join(nodeModulesRoot, packageName);
99
+ const stats = await fs.lstat(packagePath);
100
+ if (stats.isSymbolicLink()) {
101
+ // Get the real path that the symlink points to
102
+ const realPath = await fs.realpath(packagePath);
103
+ // If the real path is within the project directory (i.e. not in some other node_modules),
104
+ // then it's a local package
105
+ if (!realPath.includes('node_modules')) {
106
+ return realPath;
107
+ }
108
+ }
109
+ }
110
+ catch (e) {
111
+ // Package doesn't exist or other error - not a local package
112
+ return undefined;
113
+ }
114
+ return undefined;
115
+ }
95
116
  /**
96
117
  * Analyzes TypeScript source files starting from the config file to discover:
97
118
  * 1. Local Vendure plugins
98
119
  * 2. All non-local package imports that could contain plugins
99
120
  */
100
- export async function analyzeSourceFiles(vendureConfigPath, logger, transformTsConfigPathMappings) {
121
+ export async function analyzeSourceFiles(vendureConfigPath, nodeModulesRoot, logger, transformTsConfigPathMappings) {
101
122
  const localPluginLocations = new Map();
102
123
  const visitedFiles = new Set();
103
124
  const packageImportsSet = new Set();
@@ -123,7 +144,7 @@ export async function analyzeSourceFiles(vendureConfigPath, logger, transformTsC
123
144
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
124
145
  // Track imports to follow
125
146
  const importsToFollow = [];
126
- function visit(node) {
147
+ async function visit(node) {
127
148
  // Look for VendurePlugin decorator
128
149
  const vendurePluginClassName = getVendurePluginClassName(node);
129
150
  if (vendurePluginClassName) {
@@ -139,7 +160,16 @@ export async function analyzeSourceFiles(vendureConfigPath, logger, transformTsC
139
160
  // Track non-local imports (packages)
140
161
  const npmPackageName = getNpmPackageNameFromImport(importPath);
141
162
  if (npmPackageName) {
142
- packageImportsSet.add(npmPackageName);
163
+ // Check if this is actually a symlinked local package
164
+ const localPackagePath = await isSymlinkedLocalPackage(npmPackageName, nodeModulesRoot);
165
+ if (localPackagePath) {
166
+ // If it is local, follow it like a local import
167
+ importsToFollow.push(localPackagePath);
168
+ logger.debug(`Found symlinked local package "${npmPackageName}" at ${localPackagePath}`);
169
+ }
170
+ else {
171
+ packageImportsSet.add(npmPackageName);
172
+ }
143
173
  }
144
174
  // Handle path aliases and local imports
145
175
  const pathAliasImports = getPotentialPathAliasImportPaths(importPath, tsConfigInfo);
@@ -153,9 +183,14 @@ export async function analyzeSourceFiles(vendureConfigPath, logger, transformTsC
153
183
  }
154
184
  }
155
185
  }
156
- ts.forEachChild(node, visit);
186
+ // Visit children
187
+ const promises = [];
188
+ ts.forEachChild(node, child => {
189
+ promises.push(visit(child));
190
+ });
191
+ await Promise.all(promises);
157
192
  }
158
- visit(sourceFile);
193
+ await visit(sourceFile);
159
194
  // Follow imports
160
195
  for (const importPath of importsToFollow) {
161
196
  // Try all possible file paths
@@ -265,27 +300,14 @@ export async function findVendurePluginFiles({ outputPath, vendureConfigPath, lo
265
300
  let nodeModulesRoot = providedNodeModulesRoot;
266
301
  const readStart = Date.now();
267
302
  if (!nodeModulesRoot) {
268
- // If the node_modules root path has not been explicitly
269
- // specified, we will try to guess it by resolving the
270
- // `@vendure/core` package.
271
- try {
272
- const coreUrl = import.meta.resolve('@vendure/core');
273
- logger.debug(`Found core URL: ${coreUrl}`);
274
- const corePath = fileURLToPath(coreUrl);
275
- logger.debug(`Found core path: ${corePath}`);
276
- nodeModulesRoot = path.join(path.dirname(corePath), '..', '..');
277
- }
278
- catch (e) {
279
- logger.warn(`Failed to resolve @vendure/core: ${e instanceof Error ? e.message : String(e)}`);
280
- nodeModulesRoot = path.dirname(vendureConfigPath);
281
- }
303
+ nodeModulesRoot = guessNodeModulesRoot(vendureConfigPath, logger);
282
304
  }
283
305
  const patterns = [
284
306
  // Local compiled plugins in temp dir
285
307
  path.join(outputPath, '**/*.js'),
286
308
  // Node modules patterns
287
309
  ...packageGlobs.map(pattern => path.join(nodeModulesRoot, pattern)),
288
- ];
310
+ ].map(p => p.replace(/\\/g, '/'));
289
311
  logger.debug(`Finding Vendure plugins using patterns: ${patterns.join('\n')}`);
290
312
  const globStart = Date.now();
291
313
  const files = await glob(patterns, {
@@ -345,3 +367,21 @@ export async function findVendurePluginFiles({ outputPath, vendureConfigPath, lo
345
367
  `(scanned ${files.length} files)`);
346
368
  return potentialPluginFiles;
347
369
  }
370
+ function guessNodeModulesRoot(vendureConfigPath, logger) {
371
+ let nodeModulesRoot;
372
+ // If the node_modules root path has not been explicitly
373
+ // specified, we will try to guess it by resolving the
374
+ // `@vendure/core` package.
375
+ try {
376
+ const coreUrl = import.meta.resolve('@vendure/core');
377
+ logger.debug(`Found core URL: ${coreUrl}`);
378
+ const corePath = fileURLToPath(coreUrl);
379
+ logger.debug(`Found core path: ${corePath}`);
380
+ nodeModulesRoot = path.join(path.dirname(corePath), '..', '..');
381
+ }
382
+ catch (e) {
383
+ logger.warn(`Failed to resolve @vendure/core: ${e instanceof Error ? e.message : String(e)}`);
384
+ nodeModulesRoot = path.dirname(vendureConfigPath);
385
+ }
386
+ return nodeModulesRoot;
387
+ }
@@ -22,7 +22,19 @@ export function dashboardTailwindSourcePlugin() {
22
22
  loadVendureConfigResult = await configLoaderApi.getVendureConfig();
23
23
  }
24
24
  const { pluginInfo } = loadVendureConfigResult;
25
- const dashboardExtensionDirs = (_a = pluginInfo === null || pluginInfo === void 0 ? void 0 : pluginInfo.map(({ dashboardEntryPath, pluginPath }) => dashboardEntryPath && path.join(pluginPath, path.dirname(dashboardEntryPath))).filter(x => x != null)) !== null && _a !== void 0 ? _a : [];
25
+ const dashboardExtensionDirs = (_a = pluginInfo === null || pluginInfo === void 0 ? void 0 : pluginInfo.flatMap(({ dashboardEntryPath, sourcePluginPath, pluginPath }) => {
26
+ if (!dashboardEntryPath) {
27
+ return [];
28
+ }
29
+ const sourcePaths = [];
30
+ if (sourcePluginPath) {
31
+ sourcePaths.push(path.join(path.dirname(sourcePluginPath), path.dirname(dashboardEntryPath)));
32
+ }
33
+ if (pluginPath) {
34
+ sourcePaths.push(path.join(path.dirname(pluginPath), path.dirname(dashboardEntryPath)));
35
+ }
36
+ return sourcePaths;
37
+ }).filter(x => x != null)) !== null && _a !== void 0 ? _a : [];
26
38
  const sources = dashboardExtensionDirs
27
39
  .map(extension => {
28
40
  return `@source '${extension}';`;
@@ -63,7 +63,7 @@ export type VitePluginVendureDashboardOptions = {
63
63
  * @description
64
64
  * The path to the directory where the generated GraphQL Tada files will be output.
65
65
  */
66
- gqlTadaOutputPath?: string;
66
+ gqlOutputPath?: string;
67
67
  tempCompilationDir?: string;
68
68
  disableTansStackRouterPlugin?: boolean;
69
69
  /**
@@ -55,8 +55,8 @@ export function vendureDashboardPlugin(options) {
55
55
  adminApiSchemaPlugin(),
56
56
  dashboardMetadataPlugin(),
57
57
  uiConfigPlugin({ adminUiConfig: options.adminUiConfig }),
58
- ...(options.gqlTadaOutputPath
59
- ? [gqlTadaPlugin({ gqlTadaOutputPath: options.gqlTadaOutputPath, tempDir, packageRoot })]
58
+ ...(options.gqlOutputPath
59
+ ? [gqlTadaPlugin({ gqlTadaOutputPath: options.gqlOutputPath, tempDir, packageRoot })]
60
60
  : []),
61
61
  transformIndexHtmlPlugin(),
62
62
  ];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.3.8-master-202507220240",
4
+ "version": "3.3.8-master-202507240240",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -54,23 +54,32 @@
54
54
  "@lingui/core": "^5.2.0",
55
55
  "@lingui/react": "^5.2.0",
56
56
  "@lingui/vite-plugin": "^5.2.0",
57
- "@radix-ui/react-accordion": "^1.2.3",
58
- "@radix-ui/react-alert-dialog": "^1.1.6",
59
- "@radix-ui/react-avatar": "^1.1.3",
60
- "@radix-ui/react-checkbox": "^1.1.4",
61
- "@radix-ui/react-collapsible": "^1.1.3",
62
- "@radix-ui/react-dialog": "^1.1.6",
63
- "@radix-ui/react-dropdown-menu": "^2.1.6",
64
- "@radix-ui/react-hover-card": "^1.1.6",
65
- "@radix-ui/react-label": "^2.1.2",
66
- "@radix-ui/react-popover": "^1.1.6",
67
- "@radix-ui/react-scroll-area": "^1.2.3",
57
+ "@radix-ui/react-accordion": "^1.2.11",
58
+ "@radix-ui/react-alert-dialog": "^1.1.14",
59
+ "@radix-ui/react-aspect-ratio": "^1.1.7",
60
+ "@radix-ui/react-avatar": "^1.1.10",
61
+ "@radix-ui/react-checkbox": "^1.3.2",
62
+ "@radix-ui/react-collapsible": "^1.1.11",
63
+ "@radix-ui/react-context-menu": "^2.2.15",
64
+ "@radix-ui/react-dialog": "^1.1.14",
65
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
66
+ "@radix-ui/react-hover-card": "^1.1.14",
67
+ "@radix-ui/react-label": "^2.1.7",
68
+ "@radix-ui/react-menubar": "^1.1.15",
69
+ "@radix-ui/react-navigation-menu": "^1.2.13",
70
+ "@radix-ui/react-popover": "^1.1.14",
71
+ "@radix-ui/react-progress": "^1.1.7",
72
+ "@radix-ui/react-radio-group": "^1.3.7",
73
+ "@radix-ui/react-scroll-area": "^1.2.9",
68
74
  "@radix-ui/react-select": "^2.2.5",
69
- "@radix-ui/react-separator": "^1.1.2",
70
- "@radix-ui/react-slot": "^1.1.2",
71
- "@radix-ui/react-switch": "^1.1.3",
72
- "@radix-ui/react-tabs": "^1.1.3",
73
- "@radix-ui/react-tooltip": "^1.1.8",
75
+ "@radix-ui/react-separator": "^1.1.7",
76
+ "@radix-ui/react-slider": "^1.3.5",
77
+ "@radix-ui/react-slot": "^1.2.3",
78
+ "@radix-ui/react-switch": "^1.2.5",
79
+ "@radix-ui/react-tabs": "^1.1.12",
80
+ "@radix-ui/react-toggle": "^1.1.9",
81
+ "@radix-ui/react-toggle-group": "^1.1.10",
82
+ "@radix-ui/react-tooltip": "^1.2.7",
74
83
  "@tailwindcss/vite": "^4.1.5",
75
84
  "@tanstack/eslint-plugin-query": "^5.66.1",
76
85
  "@tanstack/react-query": "^5.66.7",
@@ -86,38 +95,42 @@
86
95
  "@types/react-dom": "^19.0.4",
87
96
  "@types/react-grid-layout": "^1.3.5",
88
97
  "@uidotdev/usehooks": "^2.4.1",
89
- "@vendure/common": "^3.3.8-master-202507220240",
90
- "@vendure/core": "^3.3.8-master-202507220240",
98
+ "@vendure/common": "^3.3.8-master-202507240240",
99
+ "@vendure/core": "^3.3.8-master-202507240240",
91
100
  "@vitejs/plugin-react": "^4.3.4",
92
101
  "acorn": "^8.11.3",
93
102
  "acorn-walk": "^8.3.2",
94
103
  "awesome-graphql-client": "^2.1.0",
95
104
  "class-variance-authority": "^0.7.1",
96
105
  "clsx": "^2.1.1",
97
- "cmdk": "^1.0.0",
106
+ "cmdk": "^1.1.1",
98
107
  "date-fns": "^3.6.0",
108
+ "embla-carousel-react": "^8.6.0",
99
109
  "fast-glob": "^3.3.2",
100
110
  "gql.tada": "^1.8.10",
101
111
  "graphql": "^16.10.0",
112
+ "input-otp": "^1.4.2",
102
113
  "json-edit-react": "^1.23.1",
103
114
  "lucide-react": "^0.475.0",
104
115
  "motion": "^12.6.2",
105
116
  "next-themes": "^0.4.6",
106
117
  "react": "^19.0.0",
107
- "react-day-picker": "^9.6.7",
118
+ "react-day-picker": "^9.8.0",
108
119
  "react-dom": "^19.0.0",
109
120
  "react-dropzone": "^14.3.8",
110
121
  "react-grid-layout": "^1.5.1",
111
- "react-hook-form": "^7.54.2",
112
- "recharts": "^2.15.1",
113
- "sonner": "^2.0.1",
122
+ "react-hook-form": "^7.60.0",
123
+ "react-resizable-panels": "^3.0.3",
124
+ "recharts": "^2.15.4",
125
+ "sonner": "^2.0.6",
114
126
  "tailwind-merge": "^3.2.0",
115
127
  "tailwindcss": "^4.1.5",
116
128
  "tailwindcss-animate": "^1.0.7",
117
129
  "tsconfig-paths": "^4.2.0",
118
130
  "tw-animate-css": "^1.2.9",
131
+ "vaul": "^1.1.2",
119
132
  "vite": "^6.3.5",
120
- "zod": "^3.24.2"
133
+ "zod": "^3.25.76"
121
134
  },
122
135
  "devDependencies": {
123
136
  "@eslint/js": "^9.19.0",
@@ -133,5 +146,5 @@
133
146
  "lightningcss-linux-arm64-musl": "^1.29.3",
134
147
  "lightningcss-linux-x64-musl": "^1.29.1"
135
148
  },
136
- "gitHead": "a9e6949507bb2077f99d111d9e6c4124acbb2f1a"
149
+ "gitHead": "5a2448f30b88b5c0fd038adc2966a1147c9a0406"
137
150
  }
@@ -0,0 +1,146 @@
1
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
2
+ import { Button } from '@/vdb/components/ui/button.js';
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogTrigger,
10
+ } from '@/vdb/components/ui/dialog.js';
11
+ import { Form } from '@/vdb/components/ui/form.js';
12
+ import { Input } from '@/vdb/components/ui/input.js';
13
+ import { api } from '@/vdb/graphql/api.js';
14
+ import { graphql } from '@/vdb/graphql/graphql.js';
15
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
16
+ import { zodResolver } from '@hookform/resolvers/zod';
17
+ import { useMutation } from '@tanstack/react-query';
18
+ import { Plus } from 'lucide-react';
19
+ import { useCallback, useState } from 'react';
20
+ import { useForm } from 'react-hook-form';
21
+ import { toast } from 'sonner';
22
+ import * as z from 'zod';
23
+
24
+ const createFacetValuesDocument = graphql(`
25
+ mutation CreateFacetValues($input: [CreateFacetValueInput!]!) {
26
+ createFacetValues(input: $input) {
27
+ id
28
+ name
29
+ code
30
+ }
31
+ }
32
+ `);
33
+
34
+ const formSchema = z.object({
35
+ name: z.string().min(1, 'Name is required'),
36
+ code: z.string().min(1, 'Code is required'),
37
+ });
38
+
39
+ type FormValues = z.infer<typeof formSchema>;
40
+
41
+ export function AddFacetValueDialog({
42
+ facetId,
43
+ onSuccess,
44
+ }: Readonly<{
45
+ facetId: string;
46
+ onSuccess?: () => void;
47
+ }>) {
48
+ const [open, setOpen] = useState(false);
49
+ const { i18n } = useLingui();
50
+
51
+ const form = useForm<FormValues>({
52
+ resolver: zodResolver(formSchema),
53
+ defaultValues: {
54
+ name: '',
55
+ code: '',
56
+ },
57
+ });
58
+
59
+ const createFacetValueMutation = useMutation({
60
+ mutationFn: api.mutate(createFacetValuesDocument),
61
+ onSuccess: () => {
62
+ toast.success(i18n.t('Successfully created facet value'));
63
+ setOpen(false);
64
+ form.reset();
65
+ onSuccess?.();
66
+ },
67
+ onError: error => {
68
+ toast.error(i18n.t('Failed to create facet value'), {
69
+ description: error instanceof Error ? error.message : i18n.t('Unknown error'),
70
+ });
71
+ },
72
+ });
73
+
74
+ const onSubmit = useCallback(
75
+ (values: FormValues) => {
76
+ createFacetValueMutation.mutate({
77
+ input: [
78
+ {
79
+ facetId,
80
+ code: values.code,
81
+ translations: [
82
+ {
83
+ languageCode: 'en',
84
+ name: values.name,
85
+ },
86
+ ],
87
+ },
88
+ ],
89
+ });
90
+ },
91
+ [createFacetValueMutation, facetId],
92
+ );
93
+
94
+ return (
95
+ <Dialog open={open} onOpenChange={setOpen}>
96
+ <DialogTrigger asChild>
97
+ <Button variant="outline">
98
+ <Plus className="mr-2 h-4 w-4" />
99
+ <Trans>Add facet value</Trans>
100
+ </Button>
101
+ </DialogTrigger>
102
+ <DialogContent>
103
+ <DialogHeader>
104
+ <DialogTitle>
105
+ <Trans>Add facet value</Trans>
106
+ </DialogTitle>
107
+ </DialogHeader>
108
+ <Form {...form}>
109
+ <form
110
+ onSubmit={e => {
111
+ e.stopPropagation();
112
+ form.handleSubmit(onSubmit)(e);
113
+ }}
114
+ className="space-y-4"
115
+ >
116
+ <FormFieldWrapper
117
+ control={form.control}
118
+ name="name"
119
+ label={<Trans>Name</Trans>}
120
+ render={({ field }) => <Input {...field} />}
121
+ />
122
+ <FormFieldWrapper
123
+ control={form.control}
124
+ name="code"
125
+ label={<Trans>Code</Trans>}
126
+ render={({ field }) => <Input {...field} />}
127
+ />
128
+ <DialogFooter>
129
+ <Button
130
+ type="submit"
131
+ disabled={
132
+ createFacetValueMutation.isPending ||
133
+ !form.formState.isValid ||
134
+ !form.watch('name').trim() ||
135
+ !form.watch('code').trim()
136
+ }
137
+ >
138
+ <Trans>Create facet value</Trans>
139
+ </Button>
140
+ </DialogFooter>
141
+ </form>
142
+ </Form>
143
+ </DialogContent>
144
+ </Dialog>
145
+ );
146
+ }
@@ -5,8 +5,10 @@ import { addCustomFields } from '@/vdb/framework/document-introspection/add-cust
5
5
  import { graphql } from '@/vdb/graphql/graphql.js';
6
6
  import { Trans } from '@/vdb/lib/trans.js';
7
7
  import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
8
- import { useState } from 'react';
8
+ import { useRef, useState } from 'react';
9
+ import { AddFacetValueDialog } from './add-facet-value-dialog.js';
9
10
  import { EditFacetValue } from './edit-facet-value.js';
11
+ import { deleteFacetValuesDocument } from '../facets.graphql.js';
10
12
 
11
13
  export const facetValueListDocument = graphql(`
12
14
  query FacetValueList($options: FacetValueListOptions) {
@@ -26,76 +28,96 @@ export const facetValueListDocument = graphql(`
26
28
 
27
29
  export interface FacetValuesTableProps {
28
30
  facetId: string;
31
+ registerRefresher?: (refresher: () => void) => void;
29
32
  }
30
33
 
31
- export function FacetValuesTable({ facetId }: Readonly<FacetValuesTableProps>) {
34
+ export function FacetValuesTable({ facetId, registerRefresher }: Readonly<FacetValuesTableProps>) {
32
35
  const [sorting, setSorting] = useState<SortingState>([]);
33
36
  const [page, setPage] = useState(1);
34
37
  const [pageSize, setPageSize] = useState(10);
35
38
  const [filters, setFilters] = useState<ColumnFiltersState>([]);
39
+ const refreshRef = useRef<() => void>(() => {});
36
40
 
37
41
  return (
38
- <PaginatedListDataTable
39
- listQuery={addCustomFields(facetValueListDocument)}
40
- page={page}
41
- itemsPerPage={pageSize}
42
- sorting={sorting}
43
- columnFilters={filters}
44
- onPageChange={(_, page, perPage) => {
45
- setPage(page);
46
- setPageSize(perPage);
47
- }}
48
- onSortChange={(_, sorting) => {
49
- setSorting(sorting);
50
- }}
51
- onFilterChange={(_, filters) => {
52
- setFilters(filters);
53
- }}
54
- transformVariables={variables => {
55
- const filter = variables.options?.filter ?? {};
56
- return {
57
- options: {
58
- filter: {
59
- ...filter,
60
- facetId: { eq: facetId },
42
+ <>
43
+ <PaginatedListDataTable
44
+ listQuery={addCustomFields(facetValueListDocument)}
45
+ deleteMutation={deleteFacetValuesDocument}
46
+ page={page}
47
+ itemsPerPage={pageSize}
48
+ sorting={sorting}
49
+ columnFilters={filters}
50
+ onPageChange={(_, page, perPage) => {
51
+ setPage(page);
52
+ setPageSize(perPage);
53
+ }}
54
+ onSortChange={(_, sorting) => {
55
+ setSorting(sorting);
56
+ }}
57
+ onFilterChange={(_, filters) => {
58
+ setFilters(filters);
59
+ }}
60
+ registerRefresher={refresher => {
61
+ refreshRef.current = refresher;
62
+ registerRefresher?.(refresher);
63
+ }}
64
+ transformVariables={variables => {
65
+ const filter = variables.options?.filter ?? {};
66
+ return {
67
+ options: {
68
+ filter: {
69
+ ...filter,
70
+ facetId: { eq: facetId },
71
+ },
72
+ sort: variables.options?.sort,
73
+ take: pageSize,
74
+ skip: (page - 1) * pageSize,
75
+ },
76
+ };
77
+ }}
78
+ onSearchTermChange={searchTerm => {
79
+ return {
80
+ name: {
81
+ contains: searchTerm,
82
+ },
83
+ };
84
+ }}
85
+ additionalColumns={{
86
+ actions: {
87
+ header: 'Actions',
88
+ cell: ({ row }) => {
89
+ const [open, setOpen] = useState(false);
90
+ const facetValue = row.original;
91
+ return (
92
+ <Popover open={open} onOpenChange={setOpen}>
93
+ <PopoverTrigger asChild>
94
+ <Button type="button" variant="outline" size="sm">
95
+ <Trans>Edit</Trans>
96
+ </Button>
97
+ </PopoverTrigger>
98
+ <PopoverContent className="w-80">
99
+ <EditFacetValue
100
+ facetValueId={facetValue.id}
101
+ onSuccess={() => {
102
+ setOpen(false);
103
+ refreshRef.current?.();
104
+ }}
105
+ />
106
+ </PopoverContent>
107
+ </Popover>
108
+ );
61
109
  },
62
- sort: variables.options?.sort,
63
- take: pageSize,
64
- skip: (page - 1) * pageSize,
65
- },
66
- };
67
- }}
68
- onSearchTermChange={searchTerm => {
69
- return {
70
- name: {
71
- contains: searchTerm,
72
- },
73
- };
74
- }}
75
- additionalColumns={{
76
- actions: {
77
- header: 'Actions',
78
- cell: ({ row }) => {
79
- const [open, setOpen] = useState(false);
80
- const facetValue = row.original;
81
- return (
82
- <Popover open={open} onOpenChange={setOpen}>
83
- <PopoverTrigger asChild>
84
- <Button type="button" variant="outline" size="sm">
85
- <Trans>Edit</Trans>
86
- </Button>
87
- </PopoverTrigger>
88
- <PopoverContent className="w-80">
89
- <EditFacetValue
90
- facetValueId={facetValue.id}
91
- onSuccess={() => setOpen(false)}
92
- />
93
- </PopoverContent>
94
- </Popover>
95
- );
96
110
  },
97
- },
98
- }}
99
- />
111
+ }}
112
+ />
113
+ <div className="mt-4">
114
+ <AddFacetValueDialog
115
+ facetId={facetId}
116
+ onSuccess={() => {
117
+ refreshRef.current?.();
118
+ }}
119
+ />
120
+ </div>
121
+ </>
100
122
  );
101
123
  }
@@ -132,3 +132,12 @@ export const deleteFacetsDocument = graphql(`
132
132
  }
133
133
  }
134
134
  `);
135
+
136
+ export const deleteFacetValuesDocument = graphql(`
137
+ mutation DeleteFacetValues($ids: [ID!]!) {
138
+ deleteFacetValues(ids: $ids) {
139
+ result
140
+ message
141
+ }
142
+ }
143
+ `);