@wilshop/dashboard 3.5.6 → 3.5.7
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.
- package/dist/plugin/dashboard.plugin.d.ts +1 -1
- package/dist/plugin/dashboard.plugin.js +1 -1
- package/dist/vite/utils/compiler.js +50 -24
- package/dist/vite/utils/path-transformer.d.ts +20 -0
- package/dist/vite/utils/path-transformer.js +116 -0
- package/dist/vite/utils/plugin-discovery.js +3 -2
- package/dist/vite/utils/ui-config.js +15 -1
- package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
- package/dist/vite/vite-plugin-lingui-babel.js +90 -8
- package/dist/vite/vite-plugin-translations.js +2 -2
- package/dist/vite/vite-plugin-ui-config.d.ts +31 -0
- package/package.json +10 -6
- package/src/app/common/delete-bulk-action.tsx +1 -1
- package/src/app/common/duplicate-bulk-action.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -3
- package/src/app/routes/_authenticated/_collections/collections.tsx +169 -48
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +36 -5
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +31 -29
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +8 -5
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +79 -54
- package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +43 -3
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +19 -3
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +1 -0
- package/src/app/routes/_authenticated/_orders/components/refund-order-dialog.tsx +372 -0
- package/src/app/routes/_authenticated/_orders/hooks/use-refund-order.ts +345 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +41 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +51 -0
- package/src/app/routes/_authenticated/_orders/utils/refund-utils.ts +100 -0
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +1 -1
- package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +4 -1
- package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +18 -2
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
- package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +6 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
- package/src/app/routes/_authenticated/_profile/profile.graphql.ts +7 -0
- package/src/app/routes/_authenticated/_profile/profile.tsx +25 -1
- package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +18 -2
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +4 -1
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +14 -2
- package/src/i18n/common-strings.ts +7 -0
- package/src/i18n/locales/ar.po +669 -399
- package/src/i18n/locales/bg.po +1889 -46
- package/src/i18n/locales/cs.po +676 -406
- package/src/i18n/locales/de.po +676 -406
- package/src/i18n/locales/en.po +669 -399
- package/src/i18n/locales/es.po +676 -406
- package/src/i18n/locales/fa.po +676 -406
- package/src/i18n/locales/fr.po +676 -406
- package/src/i18n/locales/he.po +676 -406
- package/src/i18n/locales/hr.po +676 -406
- package/src/i18n/locales/it.po +676 -406
- package/src/i18n/locales/ja.po +676 -406
- package/src/i18n/locales/nb.po +676 -406
- package/src/i18n/locales/ne.po +676 -406
- package/src/i18n/locales/pl.po +676 -406
- package/src/i18n/locales/pt_BR.po +676 -406
- package/src/i18n/locales/pt_PT.po +676 -406
- package/src/i18n/locales/ru.po +676 -406
- package/src/i18n/locales/sv.po +676 -406
- package/src/i18n/locales/tr.po +676 -406
- package/src/i18n/locales/uk.po +676 -406
- package/src/i18n/locales/zh_Hans.po +676 -406
- package/src/i18n/locales/zh_Hant.po +676 -406
- package/src/lib/components/data-input/facet-value-input.tsx +2 -2
- package/src/lib/components/data-input/index.ts +1 -0
- package/src/lib/components/data-input/select-with-options.tsx +23 -7
- package/src/lib/components/data-input/struct-form-input.tsx +53 -21
- package/src/lib/components/data-input/text-input.tsx +1 -1
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +2 -1
- package/src/lib/components/data-table/data-table-context.tsx +2 -10
- package/src/lib/components/data-table/data-table-utils.ts +34 -12
- package/src/lib/components/data-table/data-table.tsx +68 -30
- package/src/lib/components/data-table/global-views-bar.tsx +1 -1
- package/src/lib/components/data-table/my-views-button.tsx +1 -1
- package/src/lib/components/data-table/save-view-button.tsx +1 -1
- package/src/lib/components/data-table/use-generated-columns.tsx +9 -2
- package/src/lib/components/data-table/views-sheet.tsx +1 -1
- package/src/lib/components/layout/channel-switcher.tsx +16 -17
- package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
- package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +1 -1
- package/src/lib/components/shared/configurable-operation-input.tsx +23 -0
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +45 -0
- package/src/lib/components/shared/configurable-operation-selector.tsx +5 -0
- package/src/lib/components/shared/paginated-list-context.ts +10 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +6 -32
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +1 -1
- package/src/lib/components/ui/alert.tsx +2 -0
- package/src/lib/constants.ts +7 -319
- package/src/lib/framework/dashboard-widget/base-widget.tsx +3 -12
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/metrics-widget/chart.tsx +1 -1
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +2 -20
- package/src/lib/framework/extension-api/input-component-extensions.tsx +4 -0
- package/src/lib/framework/form-engine/custom-form-component.tsx +13 -3
- package/src/lib/framework/form-engine/form-engine-types.ts +3 -5
- package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
- package/src/lib/framework/form-engine/use-generated-form.tsx +6 -2
- package/src/lib/framework/form-engine/utils.spec.ts +129 -2
- package/src/lib/framework/form-engine/utils.ts +36 -9
- package/src/lib/framework/form-engine/value-transformers.ts +6 -0
- package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
- package/src/lib/framework/page/detail-page.tsx +22 -37
- package/src/lib/framework/page/list-page.stories.tsx +41 -2
- package/src/lib/framework/page/list-page.tsx +8 -0
- package/src/lib/graphql/graphql-env.d.ts +33 -16
- package/src/lib/graphql/schema-enums.ts +13 -0
- package/src/lib/hooks/use-alerts-context.ts +10 -0
- package/src/lib/hooks/use-alerts.ts +1 -1
- package/src/lib/hooks/use-data-table-context.ts +11 -0
- package/src/lib/hooks/use-dynamic-translations.ts +7 -0
- package/src/lib/hooks/use-job-queue-polling.ts +160 -0
- package/src/lib/hooks/use-paginated-list.ts +28 -0
- package/src/lib/hooks/use-widget-dimensions.ts +12 -0
- package/src/lib/hooks/use-widget-filters.ts +21 -0
- package/src/lib/index.ts +12 -0
- package/src/lib/providers/alerts-provider.tsx +3 -11
- package/src/lib/virtual.d.ts +5 -0
- package/src/lib/utils/global-languages.ts +0 -268
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { createContext, PropsWithChildren, useContext } from 'react';
|
|
3
|
+
import { createContext, PropsWithChildren } from 'react';
|
|
5
4
|
|
|
6
5
|
export interface DefinedDateRange {
|
|
7
6
|
from: Date;
|
|
@@ -12,25 +11,8 @@ export interface WidgetFilters {
|
|
|
12
11
|
dateRange: DefinedDateRange;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
const WidgetFiltersContext = createContext<WidgetFilters | undefined>(undefined);
|
|
14
|
+
export const WidgetFiltersContext = createContext<WidgetFilters | undefined>(undefined);
|
|
16
15
|
|
|
17
16
|
export function WidgetFiltersProvider({ children, filters }: PropsWithChildren<{ filters: WidgetFilters }>) {
|
|
18
17
|
return <WidgetFiltersContext.Provider value={filters}>{children}</WidgetFiltersContext.Provider>;
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @description
|
|
23
|
-
* Exposes a context object for use in building Insights page widgets.
|
|
24
|
-
*
|
|
25
|
-
* @docsCategory hooks
|
|
26
|
-
* @docsPage useWidgetFilters
|
|
27
|
-
* @since 3.5.0
|
|
28
|
-
*/
|
|
29
|
-
export function useWidgetFilters() {
|
|
30
|
-
const { t } = useLingui();
|
|
31
|
-
const context = useContext(WidgetFiltersContext);
|
|
32
|
-
if (context === undefined) {
|
|
33
|
-
throw new Error(t`useWidgetFilters must be used within a WidgetFiltersProvider`);
|
|
34
|
-
}
|
|
35
|
-
return context;
|
|
36
|
-
}
|
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
CustomerGroupInput,
|
|
5
5
|
FacetValueInput,
|
|
6
6
|
MoneyInput,
|
|
7
|
+
NumberInput,
|
|
7
8
|
ProductMultiInput,
|
|
8
9
|
RichTextInput,
|
|
9
10
|
SelectWithOptions,
|
|
10
11
|
} from '@/vdb/components/data-input/index.js';
|
|
11
12
|
import { PasswordFormInput } from '@/vdb/components/data-input/password-form-input.js';
|
|
13
|
+
import { TextInput } from '@/vdb/components/data-input/text-input.js';
|
|
12
14
|
import { TextareaInput } from '@/vdb/components/data-input/textarea-input.js';
|
|
13
15
|
import { DashboardFormComponent } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
14
16
|
import { globalRegistry } from '../registry/global-registry.js';
|
|
@@ -38,6 +40,8 @@ inputComponents.set('relation-form-input', DefaultRelationInput);
|
|
|
38
40
|
inputComponents.set('select-form-input', SelectWithOptions);
|
|
39
41
|
inputComponents.set('product-multi-form-input', ProductMultiInput);
|
|
40
42
|
inputComponents.set('combination-mode-form-input', CombinationModeInput);
|
|
43
|
+
inputComponents.set('number-form-input', NumberInput);
|
|
44
|
+
inputComponents.set('text-form-input', TextInput);
|
|
41
45
|
|
|
42
46
|
export function getInputComponent(id: string | undefined): DashboardFormComponent | undefined {
|
|
43
47
|
if (!id) {
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { getInputComponent } from '@/vdb/framework/extension-api/input-component-extensions.js';
|
|
2
|
-
|
|
2
|
+
import { DefaultInputForType } from '@/vdb/framework/form-engine/default-input-for-type.js';
|
|
3
3
|
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
4
4
|
|
|
5
|
+
const warnedComponents = new Set<string>();
|
|
6
|
+
|
|
5
7
|
export function CustomFormComponent(props: DashboardFormComponentProps) {
|
|
6
8
|
if (!props.fieldDef) {
|
|
7
9
|
return null;
|
|
8
10
|
}
|
|
9
|
-
const
|
|
11
|
+
const componentId = props.fieldDef.ui?.component;
|
|
12
|
+
const Component = getInputComponent(componentId);
|
|
10
13
|
|
|
11
14
|
if (!Component) {
|
|
12
|
-
|
|
15
|
+
if (componentId && !warnedComponents.has(componentId)) {
|
|
16
|
+
warnedComponents.add(componentId);
|
|
17
|
+
console.warn(
|
|
18
|
+
`Custom form component "${componentId}" not found for field "${props.fieldDef.name}". ` +
|
|
19
|
+
`Falling back to default input for type "${props.fieldDef.type}".`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return <DefaultInputForType {...props} />;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
return <Component {...props} />;
|
|
@@ -110,11 +110,9 @@ export type DashboardFormComponentProps<
|
|
|
110
110
|
* // implementation omitted
|
|
111
111
|
* }
|
|
112
112
|
*
|
|
113
|
-
* // highlight
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* }
|
|
117
|
-
* // highlight-end
|
|
113
|
+
* MyCustomInput.metadata = { // [!code highlight]
|
|
114
|
+
* isListInput: true // [!code highlight]
|
|
115
|
+
* } // [!code highlight]
|
|
118
116
|
* ```
|
|
119
117
|
*
|
|
120
118
|
* @docsCategory extensions-api
|
|
@@ -309,7 +309,10 @@ export function createFormSchemaFromFields(
|
|
|
309
309
|
customFieldConfigs && customFieldConfigs.length > 0
|
|
310
310
|
? processCustomFieldsSchema(customFieldConfigs, isTranslationContext)
|
|
311
311
|
: {};
|
|
312
|
-
|
|
312
|
+
// Use .passthrough() to preserve custom field values that aren't in the schema
|
|
313
|
+
// This is essential for nested entities (e.g., ProductVariantPrice) whose custom
|
|
314
|
+
// field configs aren't passed to the parent form schema builder
|
|
315
|
+
schemaConfig[field.name] = z.object(customFieldsSchema).passthrough().optional();
|
|
313
316
|
} else if (field.typeInfo) {
|
|
314
317
|
const isNestedTranslationContext = field.name === 'translations' || isTranslationContext;
|
|
315
318
|
let nestedType: ZodType = createFormSchemaFromFields(
|
|
@@ -98,9 +98,13 @@ export function useGeneratedForm<
|
|
|
98
98
|
const defaultValues = getDefaultValuesFromFields(updateFields, activeChannel?.defaultLanguageCode);
|
|
99
99
|
const processedEntity = ensureTranslationsForAllLanguages(entity, availableLanguages, defaultValues);
|
|
100
100
|
|
|
101
|
+
// Also ensure defaultValues has translations for all languages (for creation case)
|
|
102
|
+
const processedDefaultValues =
|
|
103
|
+
ensureTranslationsForAllLanguages(defaultValues, availableLanguages, defaultValues) ?? defaultValues;
|
|
104
|
+
|
|
101
105
|
const values = processedEntity
|
|
102
106
|
? transformRelationFields(updateFields, setValues(processedEntity))
|
|
103
|
-
:
|
|
107
|
+
: processedDefaultValues;
|
|
104
108
|
|
|
105
109
|
const form = useForm({
|
|
106
110
|
resolver: async (values, context, options) => {
|
|
@@ -111,7 +115,7 @@ export function useGeneratedForm<
|
|
|
111
115
|
return result;
|
|
112
116
|
},
|
|
113
117
|
mode: 'onChange',
|
|
114
|
-
defaultValues,
|
|
118
|
+
defaultValues: processedDefaultValues,
|
|
115
119
|
values,
|
|
116
120
|
});
|
|
117
121
|
let submitHandler = (event: FormEvent): any => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { graphql, VariablesOf } from 'gql.tada';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
|
|
4
|
+
import { FieldInfo, getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
|
|
5
5
|
|
|
6
|
-
import { removeEmptyIdFields } from './utils.js';
|
|
6
|
+
import { removeEmptyIdFields, transformRelationFields } from './utils.js';
|
|
7
7
|
|
|
8
8
|
const createProductDocument = graphql(`
|
|
9
9
|
mutation CreateProduct($input: CreateProductInput!) {
|
|
@@ -35,3 +35,130 @@ describe('removeEmptyIdFields', () => {
|
|
|
35
35
|
expect(result).toEqual({ input: { translations: [] } });
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
describe('transformRelationFields', () => {
|
|
40
|
+
const createFieldsWithListRelation = (): FieldInfo[] => [
|
|
41
|
+
{
|
|
42
|
+
name: 'customFields',
|
|
43
|
+
type: 'CustomFields',
|
|
44
|
+
nullable: true,
|
|
45
|
+
list: false,
|
|
46
|
+
isPaginatedList: false,
|
|
47
|
+
isScalar: false,
|
|
48
|
+
typeInfo: [
|
|
49
|
+
{
|
|
50
|
+
name: 'featuredProductsIds',
|
|
51
|
+
type: 'ID',
|
|
52
|
+
nullable: true,
|
|
53
|
+
list: true,
|
|
54
|
+
isPaginatedList: false,
|
|
55
|
+
isScalar: true,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const createFieldsWithSingleRelation = (): FieldInfo[] => [
|
|
62
|
+
{
|
|
63
|
+
name: 'customFields',
|
|
64
|
+
type: 'CustomFields',
|
|
65
|
+
nullable: true,
|
|
66
|
+
list: false,
|
|
67
|
+
isPaginatedList: false,
|
|
68
|
+
isScalar: false,
|
|
69
|
+
typeInfo: [
|
|
70
|
+
{
|
|
71
|
+
name: 'featuredProductId',
|
|
72
|
+
type: 'ID',
|
|
73
|
+
nullable: true,
|
|
74
|
+
list: false,
|
|
75
|
+
isPaginatedList: false,
|
|
76
|
+
isScalar: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
it('should extract IDs from list relation and delete original field', () => {
|
|
83
|
+
const entity = {
|
|
84
|
+
customFields: {
|
|
85
|
+
featuredProducts: [
|
|
86
|
+
{ id: '1', name: 'Product 1' },
|
|
87
|
+
{ id: '2', name: 'Product 2' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const result = transformRelationFields(createFieldsWithListRelation(), entity);
|
|
92
|
+
|
|
93
|
+
expect(result.customFields).toEqual({ featuredProductsIds: ['1', '2'] });
|
|
94
|
+
expect(result.customFields).not.toHaveProperty('featuredProducts');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should handle empty array for clearing list relations', () => {
|
|
98
|
+
const entity = { customFields: { featuredProducts: [] } };
|
|
99
|
+
const result = transformRelationFields(createFieldsWithListRelation(), entity);
|
|
100
|
+
|
|
101
|
+
expect(result.customFields).toEqual({ featuredProductsIds: [] });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle undefined/null list relation by deleting field', () => {
|
|
105
|
+
const undefinedResult = transformRelationFields(createFieldsWithListRelation(), { customFields: {} });
|
|
106
|
+
const nullResult = transformRelationFields(createFieldsWithListRelation(), {
|
|
107
|
+
customFields: { featuredProducts: null },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(undefinedResult.customFields).not.toHaveProperty('featuredProductsIds');
|
|
111
|
+
expect(nullResult.customFields).not.toHaveProperty('featuredProducts');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should extract ID from single relation and delete original field', () => {
|
|
115
|
+
const entity = { customFields: { featuredProduct: { id: '1', name: 'Product 1' } } };
|
|
116
|
+
const result = transformRelationFields(createFieldsWithSingleRelation(), entity);
|
|
117
|
+
|
|
118
|
+
expect(result.customFields).toEqual({ featuredProductId: '1' });
|
|
119
|
+
expect(result.customFields).not.toHaveProperty('featuredProduct');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should not mutate the original entity', () => {
|
|
123
|
+
const entity = { customFields: { featuredProducts: [{ id: '1' }] } };
|
|
124
|
+
const result = transformRelationFields(createFieldsWithListRelation(), entity);
|
|
125
|
+
|
|
126
|
+
expect(entity.customFields.featuredProducts).toEqual([{ id: '1' }]);
|
|
127
|
+
expect(result).not.toBe(entity);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should preserve other custom fields while transforming relations', () => {
|
|
131
|
+
const fields: FieldInfo[] = [
|
|
132
|
+
{
|
|
133
|
+
name: 'customFields',
|
|
134
|
+
type: 'CustomFields',
|
|
135
|
+
nullable: true,
|
|
136
|
+
list: false,
|
|
137
|
+
isPaginatedList: false,
|
|
138
|
+
isScalar: false,
|
|
139
|
+
typeInfo: [
|
|
140
|
+
{
|
|
141
|
+
name: 'featuredProductsIds',
|
|
142
|
+
type: 'ID',
|
|
143
|
+
nullable: true,
|
|
144
|
+
list: true,
|
|
145
|
+
isPaginatedList: false,
|
|
146
|
+
isScalar: true,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'notes',
|
|
150
|
+
type: 'String',
|
|
151
|
+
nullable: true,
|
|
152
|
+
list: false,
|
|
153
|
+
isPaginatedList: false,
|
|
154
|
+
isScalar: true,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
const entity = { customFields: { featuredProducts: [{ id: '1' }], notes: 'Some notes' } };
|
|
160
|
+
const result = transformRelationFields(fields, entity);
|
|
161
|
+
|
|
162
|
+
expect(result.customFields).toEqual({ featuredProductsIds: ['1'], notes: 'Some notes' });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -58,12 +58,10 @@ export function transformRelationFields<E extends Record<string, any>>(fields: F
|
|
|
58
58
|
const propertyAccessorKey = customField.name.replace(/Ids$/, '');
|
|
59
59
|
const relationValue = entity.customFields[propertyAccessorKey];
|
|
60
60
|
|
|
61
|
-
if (relationValue) {
|
|
62
|
-
|
|
63
|
-
if (relationIdValue && relationIdValue.length > 0) {
|
|
64
|
-
processedEntity.customFields[relationField] = relationIdValue;
|
|
65
|
-
}
|
|
61
|
+
if (Array.isArray(relationValue)) {
|
|
62
|
+
processedEntity.customFields[relationField] = relationValue.map((v: { id: string }) => v.id);
|
|
66
63
|
}
|
|
64
|
+
delete processedEntity.customFields[propertyAccessorKey];
|
|
67
65
|
} else {
|
|
68
66
|
// For single fields, the accessor is the field name without the "Id" suffix
|
|
69
67
|
const propertyAccessorKey = customField.name.replace(/Id$/, '');
|
|
@@ -253,14 +251,43 @@ export function isStringStructField(input: StructField): input is StringStructFi
|
|
|
253
251
|
}
|
|
254
252
|
|
|
255
253
|
/**
|
|
256
|
-
* String struct field that has options (select dropdown)
|
|
254
|
+
* String struct field that has options (select dropdown).
|
|
255
|
+
* Checks for options defined either directly or via ui.options.
|
|
257
256
|
*/
|
|
258
257
|
export function isStringStructFieldWithOptions(
|
|
259
258
|
input: StructField,
|
|
260
259
|
): input is StringStructField & { options: any[] } {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
if (input.type !== 'string') {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
// Check for direct options property
|
|
264
|
+
if (input.hasOwnProperty('options') && Array.isArray((input as any).options)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
// Also check for ui.options (fallback pattern)
|
|
268
|
+
if (Array.isArray((input as any).ui?.options)) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extracts options from a field definition, normalizing the different locations
|
|
276
|
+
* where options can be defined (direct property or ui.options).
|
|
277
|
+
* Works for both ConfigurableFieldDef and StructField types.
|
|
278
|
+
*/
|
|
279
|
+
export function extractFieldOptions(
|
|
280
|
+
field: ConfigurableFieldDef | StructField,
|
|
281
|
+
): NonNullable<StringCustomFieldConfig['options']> {
|
|
282
|
+
// Check direct options property first
|
|
283
|
+
if ((field as any).options && Array.isArray((field as any).options)) {
|
|
284
|
+
return (field as any).options;
|
|
285
|
+
}
|
|
286
|
+
// Fall back to ui.options
|
|
287
|
+
if (field.ui?.options && Array.isArray(field.ui.options)) {
|
|
288
|
+
return field.ui.options;
|
|
289
|
+
}
|
|
290
|
+
return [];
|
|
264
291
|
}
|
|
265
292
|
|
|
266
293
|
/**
|
|
@@ -40,6 +40,12 @@ export const jsonStringValueTransformer: ValueTransformer = {
|
|
|
40
40
|
return value;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// For scalar string fields, return the raw value without JSON parsing.
|
|
44
|
+
// This prevents issues like "0" being parsed as number 0, or "-0" becoming -0 which is "0" in the input.
|
|
45
|
+
if (fieldDef.type === 'string' && !fieldDef.list) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
try {
|
|
44
50
|
// For JSON string mode, parse the string to get the native value
|
|
45
51
|
const parsed = JSON.parse(value);
|
|
@@ -16,7 +16,7 @@ export interface DetailPageRouteLoaderConfig<T extends TypedDocumentNode<any, an
|
|
|
16
16
|
* the detail query document) get correctly applied at the route loader level.
|
|
17
17
|
*/
|
|
18
18
|
pageId?: string;
|
|
19
|
-
queryDocument: T;
|
|
19
|
+
queryDocument: T | (() => T);
|
|
20
20
|
breadcrumb: (
|
|
21
21
|
isNew: boolean,
|
|
22
22
|
entity: DetailEntity<T>,
|
|
@@ -38,12 +38,14 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
|
|
|
38
38
|
params: any;
|
|
39
39
|
location: ParsedLocation;
|
|
40
40
|
}) => {
|
|
41
|
+
const resolvedQueryDocument = typeof queryDocument === 'function' ? queryDocument() : queryDocument;
|
|
42
|
+
|
|
41
43
|
if (!params.id) {
|
|
42
44
|
throw new Error('ID param is required');
|
|
43
45
|
}
|
|
44
46
|
const isNew = params.id === NEW_ENTITY_PATH;
|
|
45
47
|
const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
|
|
46
|
-
addCustomFields(
|
|
48
|
+
addCustomFields(resolvedQueryDocument),
|
|
47
49
|
pageId,
|
|
48
50
|
);
|
|
49
51
|
const result = isNew
|
|
@@ -53,8 +55,8 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
|
|
|
53
55
|
{ id: params.id },
|
|
54
56
|
);
|
|
55
57
|
|
|
56
|
-
const entityField = getQueryName(
|
|
57
|
-
const entityName = getQueryTypeFieldInfo(
|
|
58
|
+
const entityField = getQueryName(resolvedQueryDocument);
|
|
59
|
+
const entityName = getQueryTypeFieldInfo(resolvedQueryDocument)?.type;
|
|
58
60
|
|
|
59
61
|
if (!isNew && !result[entityField]) {
|
|
60
62
|
throw new Error(`${entityName} with the ID ${params.id} was not found`);
|
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
|
|
2
1
|
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
2
|
+
import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
|
|
3
3
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
|
-
import { Checkbox } from '@/vdb/components/ui/checkbox.js';
|
|
5
|
-
import { Input } from '@/vdb/components/ui/input.js';
|
|
6
4
|
import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
|
|
5
|
+
import { DefaultInputForType } from '@/vdb/framework/form-engine/default-input-for-type.js';
|
|
7
6
|
import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
8
7
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
9
8
|
import { Trans } from '@lingui/react/macro';
|
|
10
9
|
import { AnyRoute, useNavigate } from '@tanstack/react-router';
|
|
11
10
|
import { ResultOf, VariablesOf } from 'gql.tada';
|
|
12
11
|
import { toast } from 'sonner';
|
|
12
|
+
import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
|
|
13
13
|
import {
|
|
14
14
|
FieldInfo,
|
|
15
15
|
getEntityName,
|
|
16
16
|
getOperationVariablesFields,
|
|
17
17
|
} from '../document-introspection/get-document-structure.js';
|
|
18
|
-
|
|
19
|
-
import { NumberInput } from '@/vdb/components/data-input/number-input.js';
|
|
20
|
-
import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
|
|
21
|
-
import { FormControl } from '@/vdb/components/ui/form.js';
|
|
22
|
-
import { ControllerRenderProps, FieldPath, FieldValues } from 'react-hook-form';
|
|
23
18
|
import {
|
|
24
19
|
CustomFieldsPageBlock,
|
|
25
20
|
DetailFormGrid,
|
|
@@ -97,6 +92,18 @@ export interface DetailPageFieldProps<
|
|
|
97
92
|
field: ControllerRenderProps<TFieldValues, TName>;
|
|
98
93
|
}
|
|
99
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Maps GraphQL schema types (PascalCase) to form engine types (lowercase)
|
|
97
|
+
*/
|
|
98
|
+
const graphqlTypeMap: Record<string, string> = {
|
|
99
|
+
Int: 'int',
|
|
100
|
+
Float: 'float',
|
|
101
|
+
Boolean: 'boolean',
|
|
102
|
+
DateTime: 'datetime',
|
|
103
|
+
String: 'string',
|
|
104
|
+
ID: 'string',
|
|
105
|
+
};
|
|
106
|
+
|
|
100
107
|
/**
|
|
101
108
|
* Renders form input components based on field type
|
|
102
109
|
*/
|
|
@@ -104,33 +111,13 @@ function FieldInputRenderer<
|
|
|
104
111
|
TFieldValues extends FieldValues = FieldValues,
|
|
105
112
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
106
113
|
>({ fieldInfo, field }: DetailPageFieldProps<TFieldValues, TName>) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
case 'DateTime':
|
|
116
|
-
return (
|
|
117
|
-
<FormControl>
|
|
118
|
-
<DateTimeInput {...field} />
|
|
119
|
-
</FormControl>
|
|
120
|
-
);
|
|
121
|
-
case 'Boolean':
|
|
122
|
-
return (
|
|
123
|
-
<FormControl>
|
|
124
|
-
<Checkbox value={field.value} onCheckedChange={field.onChange} />
|
|
125
|
-
</FormControl>
|
|
126
|
-
);
|
|
127
|
-
default:
|
|
128
|
-
return (
|
|
129
|
-
<FormControl>
|
|
130
|
-
<Input {...field} />
|
|
131
|
-
</FormControl>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
114
|
+
const type = graphqlTypeMap[fieldInfo.type] ?? 'string';
|
|
115
|
+
return (
|
|
116
|
+
<DefaultInputForType
|
|
117
|
+
{...field}
|
|
118
|
+
fieldDef={{ type, name: fieldInfo.name } as any}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
134
121
|
}
|
|
135
122
|
|
|
136
123
|
/**
|
|
@@ -219,7 +206,6 @@ export function DetailPage<
|
|
|
219
206
|
control={form.control}
|
|
220
207
|
name={fieldInfo.name as never}
|
|
221
208
|
label={fieldInfo.name}
|
|
222
|
-
renderFormControl={false}
|
|
223
209
|
render={({ field }) => (
|
|
224
210
|
<FieldInputRenderer fieldInfo={fieldInfo} field={field} />
|
|
225
211
|
)}
|
|
@@ -237,7 +223,6 @@ export function DetailPage<
|
|
|
237
223
|
control={form.control}
|
|
238
224
|
name={fieldInfo.name as never}
|
|
239
225
|
label={fieldInfo.name}
|
|
240
|
-
renderFormControl={false}
|
|
241
226
|
render={({ field }) => (
|
|
242
227
|
<FieldInputRenderer fieldInfo={fieldInfo} field={field} />
|
|
243
228
|
)}
|
|
@@ -5,6 +5,7 @@ import { ListPage, ListPageProps } from '@/vdb/framework/page/list-page.js';
|
|
|
5
5
|
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
6
6
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
7
7
|
import { PlusIcon } from 'lucide-react';
|
|
8
|
+
import { expect } from 'storybook/test';
|
|
8
9
|
|
|
9
10
|
import { DemoRouterProvider } from '../../../../.storybook/providers.js';
|
|
10
11
|
|
|
@@ -72,8 +73,9 @@ const meta = {
|
|
|
72
73
|
description: 'GraphQL mutation document for deleting items',
|
|
73
74
|
},
|
|
74
75
|
customizeColumns: {
|
|
75
|
-
control:
|
|
76
|
-
description:
|
|
76
|
+
control: 'object',
|
|
77
|
+
description:
|
|
78
|
+
'Customize column rendering and behavior. Use `meta.disabled: true` to exclude columns from the table.',
|
|
77
79
|
},
|
|
78
80
|
defaultVisibility: {
|
|
79
81
|
control: 'object',
|
|
@@ -99,6 +101,11 @@ export const BasicList: Story = {
|
|
|
99
101
|
code: true,
|
|
100
102
|
enabled: true,
|
|
101
103
|
},
|
|
104
|
+
customizeColumns: {
|
|
105
|
+
updatedAt: {
|
|
106
|
+
meta: { disabled: true },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
102
109
|
},
|
|
103
110
|
};
|
|
104
111
|
|
|
@@ -166,6 +173,38 @@ export const WithSearch: Story = {
|
|
|
166
173
|
},
|
|
167
174
|
};
|
|
168
175
|
|
|
176
|
+
/**
|
|
177
|
+
* ListPage with disabled columns.
|
|
178
|
+
* Shows how to use `meta.disabled` to completely exclude columns from the table
|
|
179
|
+
* and the column visibility toggle. The disabled columns' data can still be
|
|
180
|
+
* accessed in other column renderers.
|
|
181
|
+
*/
|
|
182
|
+
export const WithDisabledColumns: Story = {
|
|
183
|
+
args: {
|
|
184
|
+
pageId: 'country-list-disabled',
|
|
185
|
+
listQuery: countriesListQuery,
|
|
186
|
+
title: 'Countries',
|
|
187
|
+
defaultVisibility: {
|
|
188
|
+
name: true,
|
|
189
|
+
code: true,
|
|
190
|
+
enabled: true,
|
|
191
|
+
},
|
|
192
|
+
customizeColumns: {
|
|
193
|
+
name: {
|
|
194
|
+
cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
|
|
195
|
+
},
|
|
196
|
+
// The createdAt and updatedAt columns are disabled and won't appear
|
|
197
|
+
// in the table or in the column visibility toggle
|
|
198
|
+
updatedAt: {
|
|
199
|
+
meta: { disabled: true },
|
|
200
|
+
},
|
|
201
|
+
createdAt: {
|
|
202
|
+
meta: { disabled: true },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
169
208
|
/**
|
|
170
209
|
* Complete example with action bar and all features.
|
|
171
210
|
* Shows the full ListPage configuration including custom action buttons.
|
|
@@ -154,6 +154,7 @@ export interface ListPageProps<
|
|
|
154
154
|
* };
|
|
155
155
|
* }}
|
|
156
156
|
* />
|
|
157
|
+
* ```
|
|
157
158
|
* @param searchTerm
|
|
158
159
|
*/
|
|
159
160
|
onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];
|
|
@@ -208,6 +209,13 @@ export interface ListPageProps<
|
|
|
208
209
|
* );
|
|
209
210
|
* },
|
|
210
211
|
* },
|
|
212
|
+
* // Use `meta.disabled` to completely exclude a column from the table,
|
|
213
|
+
* // including the column visibility toggle. This is useful when you need
|
|
214
|
+
* // to fetch certain fields for use in custom cell renderers, but don't
|
|
215
|
+
* // want those fields to appear as their own columns.
|
|
216
|
+
* productVariantCount: {
|
|
217
|
+
* meta: { disabled: true },
|
|
218
|
+
* },
|
|
211
219
|
* ```
|
|
212
220
|
*/
|
|
213
221
|
customizeColumns?: CustomizeColumnConfig<T>;
|