@vendure/dashboard 3.3.6-master-202507010243 → 3.3.6-master-202507010922
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/package.json +4 -4
- package/src/app/common/duplicate-bulk-action.tsx +134 -0
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +7 -2
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +9 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +10 -0
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +66 -6
- package/src/app/routes/_authenticated/_countries/countries.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -5
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +8 -5
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +5 -2
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -5
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +19 -106
- package/src/app/routes/_authenticated/_products/products.graphql.ts +0 -17
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +6 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +5 -1
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -5
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +8 -4
- package/src/app/routes/_authenticated/_zones/zones.graphql.ts +1 -1
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +8 -4
- package/src/lib/components/shared/custom-fields-form.tsx +18 -1
- package/src/lib/framework/document-extension/extend-detail-form-query.ts +50 -0
- package/src/lib/framework/document-extension/extend-document.spec.ts +335 -0
- package/src/lib/framework/document-introspection/add-custom-fields.ts +48 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +19 -1
- package/src/lib/framework/extension-api/extension-api-types.ts +15 -2
- package/src/lib/framework/form-engine/custom-form-component-extensions.ts +13 -3
- package/src/lib/framework/form-engine/utils.ts +43 -15
- package/src/lib/framework/layout-engine/page-layout.tsx +1 -0
- package/src/lib/framework/page/detail-page-route-loader.tsx +13 -1
- package/src/lib/framework/page/use-detail-page.ts +11 -2
- package/src/lib/framework/registry/registry-types.ts +1 -0
- package/src/lib/graphql/common-operations.ts +18 -0
- package/src/lib/graphql/{fragments.tsx → fragments.ts} +1 -2
- package/src/lib/graphql/graphql-env.d.ts +10 -8
- package/src/lib/hooks/use-extended-detail-query.ts +37 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { extendDocument } from '@/framework/document-extension/extend-document.js';
|
|
2
|
+
import { getDetailQueryDocuments } from '@/framework/form-engine/custom-form-component-extensions.js';
|
|
3
|
+
import { DocumentNode } from 'graphql';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @description
|
|
7
|
+
* Extends a detail page query document with any registered extensions provided by
|
|
8
|
+
* the `extendDetailDocument` function for the given page.
|
|
9
|
+
*/
|
|
10
|
+
export function extendDetailFormQuery<T extends DocumentNode>(
|
|
11
|
+
detailQuery: T,
|
|
12
|
+
pageId?: string,
|
|
13
|
+
): {
|
|
14
|
+
extendedQuery: T;
|
|
15
|
+
errorMessage?: string | null;
|
|
16
|
+
} {
|
|
17
|
+
let result: T = detailQuery;
|
|
18
|
+
let errorMessage: string | null = null;
|
|
19
|
+
|
|
20
|
+
if (!pageId) {
|
|
21
|
+
// If no pageId is provided, return the original query without any extensions
|
|
22
|
+
return { extendedQuery: detailQuery };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const detailQueryExtensions = getDetailQueryDocuments(pageId);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
result = detailQueryExtensions.reduce(
|
|
29
|
+
(acc, extension) => extendDocument(acc, extension),
|
|
30
|
+
detailQuery,
|
|
31
|
+
) as T;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
34
|
+
// Continue with the original query instead of the extended one
|
|
35
|
+
result = detailQuery;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Store error for useEffect to handle
|
|
39
|
+
if (errorMessage) {
|
|
40
|
+
// Log the error and continue with the original query
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.warn(`${errorMessage}. Continuing with original query.`, {
|
|
43
|
+
pageId,
|
|
44
|
+
extensionsCount: detailQueryExtensions.length,
|
|
45
|
+
error: errorMessage,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { extendedQuery: result, errorMessage };
|
|
50
|
+
}
|
|
@@ -546,4 +546,339 @@ describe('extendDocument', () => {
|
|
|
546
546
|
`),
|
|
547
547
|
);
|
|
548
548
|
});
|
|
549
|
+
|
|
550
|
+
it('should extend detail query with fragments', () => {
|
|
551
|
+
const detailDocument = graphql(`
|
|
552
|
+
fragment ProductDetail on Product {
|
|
553
|
+
id
|
|
554
|
+
name
|
|
555
|
+
slug
|
|
556
|
+
description
|
|
557
|
+
featuredAsset {
|
|
558
|
+
id
|
|
559
|
+
preview
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
query ProductDetail($id: ID!) {
|
|
564
|
+
product(id: $id) {
|
|
565
|
+
...ProductDetail
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
`);
|
|
569
|
+
|
|
570
|
+
const extended = extendDocument(
|
|
571
|
+
detailDocument as any,
|
|
572
|
+
`
|
|
573
|
+
fragment ProductDetail on Product {
|
|
574
|
+
enabled
|
|
575
|
+
createdAt
|
|
576
|
+
updatedAt
|
|
577
|
+
assets {
|
|
578
|
+
id
|
|
579
|
+
preview
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
query ProductDetail($id: ID!) {
|
|
584
|
+
product(id: $id) {
|
|
585
|
+
customFields
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
`,
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const printed = print(extended);
|
|
592
|
+
|
|
593
|
+
expect(expectedSDL(printed)).toBe(
|
|
594
|
+
expectedSDL(`
|
|
595
|
+
query ProductDetail($id: ID!) {
|
|
596
|
+
product(id: $id) {
|
|
597
|
+
...ProductDetail
|
|
598
|
+
customFields
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
fragment ProductDetail on Product {
|
|
602
|
+
id
|
|
603
|
+
name
|
|
604
|
+
slug
|
|
605
|
+
description
|
|
606
|
+
featuredAsset {
|
|
607
|
+
id
|
|
608
|
+
preview
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
fragment ProductDetail on Product {
|
|
612
|
+
enabled
|
|
613
|
+
createdAt
|
|
614
|
+
updatedAt
|
|
615
|
+
assets {
|
|
616
|
+
id
|
|
617
|
+
preview
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
`),
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should extend detail query with nested translations', () => {
|
|
625
|
+
const detailDocument = graphql(`
|
|
626
|
+
query ProductDetail($id: ID!) {
|
|
627
|
+
product(id: $id) {
|
|
628
|
+
id
|
|
629
|
+
name
|
|
630
|
+
slug
|
|
631
|
+
translations {
|
|
632
|
+
id
|
|
633
|
+
languageCode
|
|
634
|
+
name
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
`);
|
|
639
|
+
|
|
640
|
+
const extended = extendDocument(
|
|
641
|
+
detailDocument as any,
|
|
642
|
+
`
|
|
643
|
+
query ProductDetail($id: ID!) {
|
|
644
|
+
product(id: $id) {
|
|
645
|
+
translations {
|
|
646
|
+
slug
|
|
647
|
+
description
|
|
648
|
+
}
|
|
649
|
+
facetValues {
|
|
650
|
+
id
|
|
651
|
+
name
|
|
652
|
+
code
|
|
653
|
+
facet {
|
|
654
|
+
id
|
|
655
|
+
name
|
|
656
|
+
code
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
`,
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
const printed = print(extended);
|
|
665
|
+
|
|
666
|
+
expect(expectedSDL(printed)).toBe(
|
|
667
|
+
expectedSDL(`
|
|
668
|
+
query ProductDetail($id: ID!) {
|
|
669
|
+
product(id: $id) {
|
|
670
|
+
id
|
|
671
|
+
name
|
|
672
|
+
slug
|
|
673
|
+
translations {
|
|
674
|
+
id
|
|
675
|
+
languageCode
|
|
676
|
+
name
|
|
677
|
+
slug
|
|
678
|
+
description
|
|
679
|
+
}
|
|
680
|
+
facetValues {
|
|
681
|
+
id
|
|
682
|
+
name
|
|
683
|
+
code
|
|
684
|
+
facet {
|
|
685
|
+
id
|
|
686
|
+
name
|
|
687
|
+
code
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
`),
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('should extend detail query with asset fragments', () => {
|
|
697
|
+
const detailDocument = graphql(`
|
|
698
|
+
fragment Asset on Asset {
|
|
699
|
+
id
|
|
700
|
+
preview
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
query ProductDetail($id: ID!) {
|
|
704
|
+
product(id: $id) {
|
|
705
|
+
id
|
|
706
|
+
featuredAsset {
|
|
707
|
+
...Asset
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
`);
|
|
712
|
+
|
|
713
|
+
const extended = extendDocument(
|
|
714
|
+
detailDocument as any,
|
|
715
|
+
`
|
|
716
|
+
fragment Asset on Asset {
|
|
717
|
+
name
|
|
718
|
+
source
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
query ProductDetail($id: ID!) {
|
|
722
|
+
product(id: $id) {
|
|
723
|
+
assets {
|
|
724
|
+
...Asset
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
`,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const printed = print(extended);
|
|
732
|
+
|
|
733
|
+
expect(expectedSDL(printed)).toBe(
|
|
734
|
+
expectedSDL(`
|
|
735
|
+
query ProductDetail($id: ID!) {
|
|
736
|
+
product(id: $id) {
|
|
737
|
+
id
|
|
738
|
+
featuredAsset {
|
|
739
|
+
...Asset
|
|
740
|
+
}
|
|
741
|
+
assets {
|
|
742
|
+
...Asset
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
fragment Asset on Asset {
|
|
747
|
+
id
|
|
748
|
+
preview
|
|
749
|
+
}
|
|
750
|
+
fragment Asset on Asset {
|
|
751
|
+
name
|
|
752
|
+
source
|
|
753
|
+
}
|
|
754
|
+
`),
|
|
755
|
+
);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should extend detail query with custom fields', () => {
|
|
759
|
+
const detailDocument = graphql(`
|
|
760
|
+
query ProductDetail($id: ID!) {
|
|
761
|
+
product(id: $id) {
|
|
762
|
+
id
|
|
763
|
+
name
|
|
764
|
+
customFields
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
`);
|
|
768
|
+
|
|
769
|
+
const extended = extendDocument(
|
|
770
|
+
detailDocument as any,
|
|
771
|
+
`
|
|
772
|
+
query ProductDetail($id: ID!) {
|
|
773
|
+
product(id: $id) {
|
|
774
|
+
enabled
|
|
775
|
+
createdAt
|
|
776
|
+
updatedAt
|
|
777
|
+
customFields
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
`,
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const printed = print(extended);
|
|
784
|
+
|
|
785
|
+
expect(expectedSDL(printed)).toBe(
|
|
786
|
+
expectedSDL(`
|
|
787
|
+
query ProductDetail($id: ID!) {
|
|
788
|
+
product(id: $id) {
|
|
789
|
+
id
|
|
790
|
+
name
|
|
791
|
+
customFields
|
|
792
|
+
enabled
|
|
793
|
+
createdAt
|
|
794
|
+
updatedAt
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
`),
|
|
798
|
+
);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('should extend detail query with complex nested structure', () => {
|
|
802
|
+
const detailDocument = graphql(`
|
|
803
|
+
query ProductDetail($id: ID!) {
|
|
804
|
+
product(id: $id) {
|
|
805
|
+
id
|
|
806
|
+
name
|
|
807
|
+
featuredAsset {
|
|
808
|
+
id
|
|
809
|
+
preview
|
|
810
|
+
}
|
|
811
|
+
facetValues {
|
|
812
|
+
id
|
|
813
|
+
name
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
`);
|
|
818
|
+
|
|
819
|
+
const extended = extendDocument(
|
|
820
|
+
detailDocument as any,
|
|
821
|
+
`
|
|
822
|
+
query ProductDetail($id: ID!) {
|
|
823
|
+
product(id: $id) {
|
|
824
|
+
featuredAsset {
|
|
825
|
+
name
|
|
826
|
+
source
|
|
827
|
+
}
|
|
828
|
+
facetValues {
|
|
829
|
+
code
|
|
830
|
+
facet {
|
|
831
|
+
id
|
|
832
|
+
name
|
|
833
|
+
code
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
translations {
|
|
837
|
+
id
|
|
838
|
+
languageCode
|
|
839
|
+
name
|
|
840
|
+
slug
|
|
841
|
+
description
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
`,
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const printed = print(extended);
|
|
849
|
+
|
|
850
|
+
expect(expectedSDL(printed)).toBe(
|
|
851
|
+
expectedSDL(`
|
|
852
|
+
query ProductDetail($id: ID!) {
|
|
853
|
+
product(id: $id) {
|
|
854
|
+
id
|
|
855
|
+
name
|
|
856
|
+
featuredAsset {
|
|
857
|
+
id
|
|
858
|
+
preview
|
|
859
|
+
name
|
|
860
|
+
source
|
|
861
|
+
}
|
|
862
|
+
facetValues {
|
|
863
|
+
id
|
|
864
|
+
name
|
|
865
|
+
code
|
|
866
|
+
facet {
|
|
867
|
+
id
|
|
868
|
+
name
|
|
869
|
+
code
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
translations {
|
|
873
|
+
id
|
|
874
|
+
languageCode
|
|
875
|
+
name
|
|
876
|
+
slug
|
|
877
|
+
description
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
`),
|
|
882
|
+
);
|
|
883
|
+
});
|
|
549
884
|
});
|
|
@@ -25,6 +25,35 @@ type RelationCustomFieldFragment = ResultOf<typeof relationCustomFieldFragment>;
|
|
|
25
25
|
|
|
26
26
|
let globalCustomFieldsMap: Map<string, CustomFieldConfig[]> = new Map();
|
|
27
27
|
|
|
28
|
+
// Memoization cache using WeakMap to avoid memory leaks
|
|
29
|
+
const memoizationCache = new WeakMap<DocumentNode, Map<string, TypedDocumentNode<any, any>>>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a cache key for the options object
|
|
33
|
+
*/
|
|
34
|
+
function createOptionsKey(options?: {
|
|
35
|
+
customFieldsMap?: Map<string, CustomFieldConfig[]>;
|
|
36
|
+
includeCustomFields?: string[];
|
|
37
|
+
}): string {
|
|
38
|
+
if (!options) return 'default';
|
|
39
|
+
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (options.customFieldsMap) {
|
|
43
|
+
// Create a deterministic key for the customFieldsMap
|
|
44
|
+
const mapEntries = Array.from(options.customFieldsMap.entries())
|
|
45
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
46
|
+
.map(([key, value]) => `${key}:${value.length}`);
|
|
47
|
+
parts.push(`map:${mapEntries.join(',')}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (options.includeCustomFields) {
|
|
51
|
+
parts.push(`include:${options.includeCustomFields.sort().join(',')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return parts.join('|') || 'default';
|
|
55
|
+
}
|
|
56
|
+
|
|
28
57
|
/**
|
|
29
58
|
* @description
|
|
30
59
|
* This function is used to set the global custom fields map.
|
|
@@ -56,6 +85,8 @@ export function getCustomFieldsMap() {
|
|
|
56
85
|
/**
|
|
57
86
|
* Given a GraphQL AST (DocumentNode), this function looks for fragment definitions and adds and configured
|
|
58
87
|
* custom fields to those fragments.
|
|
88
|
+
*
|
|
89
|
+
* This function is memoized to return a stable identity for given inputs.
|
|
59
90
|
*/
|
|
60
91
|
export function addCustomFields<T, V extends Variables = Variables>(
|
|
61
92
|
documentNode: DocumentNode | TypedDocumentNode<T, V>,
|
|
@@ -64,6 +95,21 @@ export function addCustomFields<T, V extends Variables = Variables>(
|
|
|
64
95
|
includeCustomFields?: string[];
|
|
65
96
|
},
|
|
66
97
|
): TypedDocumentNode<T, V> {
|
|
98
|
+
const optionsKey = createOptionsKey(options);
|
|
99
|
+
|
|
100
|
+
// Check if we have a cached result for this document and options
|
|
101
|
+
let documentCache = memoizationCache.get(documentNode);
|
|
102
|
+
if (!documentCache) {
|
|
103
|
+
documentCache = new Map();
|
|
104
|
+
memoizationCache.set(documentNode, documentCache);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const cachedResult = documentCache.get(optionsKey);
|
|
108
|
+
if (cachedResult) {
|
|
109
|
+
return cachedResult as TypedDocumentNode<T, V>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If not cached, compute the result
|
|
67
113
|
const clone = JSON.parse(JSON.stringify(documentNode)) as DocumentNode;
|
|
68
114
|
const customFields = options?.customFieldsMap || globalCustomFieldsMap;
|
|
69
115
|
|
|
@@ -209,6 +255,8 @@ export function addCustomFields<T, V extends Variables = Variables>(
|
|
|
209
255
|
}
|
|
210
256
|
}
|
|
211
257
|
|
|
258
|
+
// Cache the result before returning
|
|
259
|
+
documentCache.set(optionsKey, clone);
|
|
212
260
|
return clone;
|
|
213
261
|
}
|
|
214
262
|
|
|
@@ -2,7 +2,10 @@ import { addBulkAction, addListQueryDocument } from '@/framework/data-table/data
|
|
|
2
2
|
import { parse } from 'graphql';
|
|
3
3
|
|
|
4
4
|
import { registerDashboardWidget } from '../dashboard-widget/widget-extensions.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
addCustomFormComponent,
|
|
7
|
+
addDetailQueryDocument,
|
|
8
|
+
} from '../form-engine/custom-form-component-extensions.js';
|
|
6
9
|
import {
|
|
7
10
|
registerDashboardActionBarItem,
|
|
8
11
|
registerDashboardPageBlock,
|
|
@@ -106,6 +109,21 @@ export function defineDashboardExtension(extension: DashboardExtension) {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
}
|
|
112
|
+
if (extension.detailForms) {
|
|
113
|
+
for (const detailForm of extension.detailForms) {
|
|
114
|
+
if (detailForm.extendDetailDocument) {
|
|
115
|
+
const document =
|
|
116
|
+
typeof detailForm.extendDetailDocument === 'function'
|
|
117
|
+
? detailForm.extendDetailDocument()
|
|
118
|
+
: detailForm.extendDetailDocument;
|
|
119
|
+
|
|
120
|
+
addDetailQueryDocument(
|
|
121
|
+
detailForm.pageId,
|
|
122
|
+
typeof document === 'string' ? parse(document) : document,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
109
127
|
const callbacks = globalRegistry.get('extensionSourceChangeCallbacks');
|
|
110
128
|
if (callbacks.size) {
|
|
111
129
|
for (const callback of callbacks) {
|
|
@@ -120,7 +120,7 @@ export interface DashboardPageBlockDefinition {
|
|
|
120
120
|
* @docsCategory extensions
|
|
121
121
|
* @since 3.4.0
|
|
122
122
|
*/
|
|
123
|
-
export interface
|
|
123
|
+
export interface DashboardDataTableExtensionDefinition {
|
|
124
124
|
/**
|
|
125
125
|
* @description
|
|
126
126
|
* The ID of the page where the data table is located, e.g. `'product-list'`, `'order-list'`.
|
|
@@ -145,6 +145,18 @@ export interface DashboardDataTableDefinition {
|
|
|
145
145
|
extendListDocument?: string | DocumentNode | (() => DocumentNode | string);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export interface DashboardDetailFormExtensionDefinition {
|
|
149
|
+
/**
|
|
150
|
+
* @description
|
|
151
|
+
* The ID of the page where the detail form is located, e.g. `'product-detail'`, `'order-detail'`.
|
|
152
|
+
*/
|
|
153
|
+
pageId: string;
|
|
154
|
+
/**
|
|
155
|
+
* @description
|
|
156
|
+
*/
|
|
157
|
+
extendDetailDocument?: string | DocumentNode | (() => DocumentNode | string);
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
/**
|
|
149
161
|
* @description
|
|
150
162
|
* **Status: Developer Preview**
|
|
@@ -195,5 +207,6 @@ export interface DashboardExtension {
|
|
|
195
207
|
* @description
|
|
196
208
|
* Allows you to customize aspects of existing data tables in the dashboard.
|
|
197
209
|
*/
|
|
198
|
-
dataTables?:
|
|
210
|
+
dataTables?: DashboardDataTableExtensionDefinition[];
|
|
211
|
+
detailForms?: DashboardDetailFormExtensionDefinition[];
|
|
199
212
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DocumentNode } from 'graphql';
|
|
2
|
+
|
|
1
3
|
import { DashboardCustomFormComponent } from '../extension-api/extension-api-types.js';
|
|
2
4
|
import { globalRegistry } from '../registry/global-registry.js';
|
|
3
5
|
|
|
@@ -8,9 +10,7 @@ globalRegistry.register(
|
|
|
8
10
|
new Map<string, React.FunctionComponent<CustomFormComponentInputProps>>(),
|
|
9
11
|
);
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
return globalRegistry.get('customFormComponents');
|
|
13
|
-
}
|
|
13
|
+
globalRegistry.register('detailQueryDocumentRegistry', new Map<string, DocumentNode[]>());
|
|
14
14
|
|
|
15
15
|
export function getCustomFormComponent(
|
|
16
16
|
id: string,
|
|
@@ -26,3 +26,13 @@ export function addCustomFormComponent({ id, component }: DashboardCustomFormCom
|
|
|
26
26
|
}
|
|
27
27
|
customFormComponents.set(id, component);
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
export function getDetailQueryDocuments(pageId: string): DocumentNode[] {
|
|
31
|
+
return globalRegistry.get('detailQueryDocumentRegistry').get(pageId) || [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function addDetailQueryDocument(pageId: string, document: DocumentNode) {
|
|
35
|
+
const listQueryDocumentRegistry = globalRegistry.get('detailQueryDocumentRegistry');
|
|
36
|
+
const existingDocuments = listQueryDocumentRegistry.get(pageId) || [];
|
|
37
|
+
listQueryDocumentRegistry.set(pageId, [...existingDocuments, document]);
|
|
38
|
+
}
|
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
import { FieldInfo } from '../document-introspection/get-document-structure.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Transforms relation fields in an entity, extracting IDs from relation objects.
|
|
5
|
+
* This is primarily used for custom fields of type "ID".
|
|
6
|
+
*
|
|
7
|
+
* @param fields - Array of field information
|
|
8
|
+
* @param entity - The entity to transform
|
|
9
|
+
* @returns A new entity with transformed relation fields
|
|
10
|
+
*/
|
|
11
|
+
export function transformRelationFields<E extends Record<string, any>>(fields: FieldInfo[], entity: E): E {
|
|
12
|
+
// Create a shallow copy to avoid mutating the original entity
|
|
13
|
+
const processedEntity = { ...entity };
|
|
5
14
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
// Skip processing if there are no custom fields
|
|
16
|
+
if (!entity.customFields || !processedEntity.customFields) {
|
|
17
|
+
return processedEntity;
|
|
18
|
+
}
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
// Find the customFields field info
|
|
21
|
+
const customFieldsInfo = fields.find(field => field.name === 'customFields' && field.typeInfo);
|
|
22
|
+
if (!customFieldsInfo?.typeInfo) {
|
|
23
|
+
return processedEntity;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Process only ID type custom fields
|
|
27
|
+
const idTypeCustomFields = customFieldsInfo.typeInfo.filter(field => field.type === 'ID');
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
for (const customField of idTypeCustomFields) {
|
|
30
|
+
const relationField = customField.name;
|
|
31
|
+
|
|
32
|
+
if (customField.list) {
|
|
33
|
+
// For list fields, the accessor is the field name without the "Ids" suffix
|
|
34
|
+
const propertyAccessorKey = customField.name.replace(/Ids$/, '');
|
|
35
|
+
const relationValue = entity.customFields[propertyAccessorKey];
|
|
36
|
+
|
|
37
|
+
if (relationValue) {
|
|
38
|
+
const relationIdValue = relationValue.map((v: { id: string }) => v.id);
|
|
39
|
+
if (relationIdValue && relationIdValue.length > 0) {
|
|
40
|
+
processedEntity.customFields[relationField] = relationIdValue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
// For single fields, the accessor is the field name without the "Id" suffix
|
|
45
|
+
const propertyAccessorKey = customField.name.replace(/Id$/, '');
|
|
46
|
+
const relationValue = entity.customFields[propertyAccessorKey];
|
|
21
47
|
|
|
48
|
+
if (relationValue) {
|
|
49
|
+
const relationIdValue = relationValue.id;
|
|
22
50
|
if (relationIdValue) {
|
|
23
51
|
processedEntity.customFields[relationField] = relationIdValue;
|
|
24
52
|
}
|
|
@@ -194,6 +194,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
|
|
|
194
194
|
if (extensionBlock) {
|
|
195
195
|
const ExtensionBlock = (
|
|
196
196
|
<PageBlock
|
|
197
|
+
key={childBlock.key}
|
|
197
198
|
column={extensionBlock.location.column}
|
|
198
199
|
blockId={extensionBlock.id}
|
|
199
200
|
title={extensionBlock.title}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NEW_ENTITY_PATH } from '@/constants.js';
|
|
2
2
|
|
|
3
3
|
import { PageBreadcrumb } from '@/components/layout/generated-breadcrumbs.js';
|
|
4
|
+
import { extendDetailFormQuery } from '@/framework/document-extension/extend-detail-form-query.js';
|
|
4
5
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
5
6
|
import { FileBaseRouteOptions, ParsedLocation } from '@tanstack/react-router';
|
|
6
7
|
import { addCustomFields } from '../document-introspection/add-custom-fields.js';
|
|
@@ -9,6 +10,12 @@ import { DetailEntity } from './page-types.js';
|
|
|
9
10
|
import { getDetailQueryOptions } from './use-detail-page.js';
|
|
10
11
|
|
|
11
12
|
export interface DetailPageRouteLoaderConfig<T extends TypedDocumentNode<any, any>> {
|
|
13
|
+
/**
|
|
14
|
+
* @description
|
|
15
|
+
* The pageId is used to ensure any detail form extensions (such as extensions to
|
|
16
|
+
* the detail query document) get correctly applied at the route loader level.
|
|
17
|
+
*/
|
|
18
|
+
pageId?: string;
|
|
12
19
|
queryDocument: T;
|
|
13
20
|
breadcrumb: (
|
|
14
21
|
isNew: boolean,
|
|
@@ -18,6 +25,7 @@ export interface DetailPageRouteLoaderConfig<T extends TypedDocumentNode<any, an
|
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
|
|
28
|
+
pageId,
|
|
21
29
|
queryDocument,
|
|
22
30
|
breadcrumb,
|
|
23
31
|
}: DetailPageRouteLoaderConfig<T>) {
|
|
@@ -34,10 +42,14 @@ export function detailPageRouteLoader<T extends TypedDocumentNode<any, any>>({
|
|
|
34
42
|
throw new Error('ID param is required');
|
|
35
43
|
}
|
|
36
44
|
const isNew = params.id === NEW_ENTITY_PATH;
|
|
45
|
+
const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
|
|
46
|
+
addCustomFields(queryDocument),
|
|
47
|
+
pageId,
|
|
48
|
+
);
|
|
37
49
|
const result = isNew
|
|
38
50
|
? null
|
|
39
51
|
: await context.queryClient.ensureQueryData(
|
|
40
|
-
getDetailQueryOptions(
|
|
52
|
+
getDetailQueryOptions(extendedQueryDocument, { id: params.id }),
|
|
41
53
|
{ id: params.id },
|
|
42
54
|
);
|
|
43
55
|
|