@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.
Files changed (49) hide show
  1. package/package.json +4 -4
  2. package/src/app/common/duplicate-bulk-action.tsx +134 -0
  3. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +5 -1
  4. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +7 -2
  5. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -1
  6. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +9 -0
  7. package/src/app/routes/_authenticated/_collections/collections.tsx +10 -0
  8. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +5 -1
  9. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +66 -6
  10. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +1 -1
  11. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -5
  12. package/src/app/routes/_authenticated/_customer-groups/customer-groups.graphql.ts +1 -1
  13. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +8 -5
  14. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +5 -1
  15. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +5 -1
  16. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +5 -2
  17. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +5 -1
  18. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +4 -5
  19. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +5 -1
  20. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +19 -106
  21. package/src/app/routes/_authenticated/_products/products.graphql.ts +0 -17
  22. package/src/app/routes/_authenticated/_products/products_.$id.tsx +5 -1
  23. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +5 -1
  24. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -1
  25. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +6 -2
  26. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +5 -1
  27. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +5 -1
  28. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +1 -1
  29. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -5
  30. package/src/app/routes/_authenticated/_tax-rates/tax-rates.graphql.ts +1 -1
  31. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +8 -4
  32. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +1 -1
  33. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +8 -4
  34. package/src/lib/components/shared/custom-fields-form.tsx +18 -1
  35. package/src/lib/framework/document-extension/extend-detail-form-query.ts +50 -0
  36. package/src/lib/framework/document-extension/extend-document.spec.ts +335 -0
  37. package/src/lib/framework/document-introspection/add-custom-fields.ts +48 -0
  38. package/src/lib/framework/extension-api/define-dashboard-extension.ts +19 -1
  39. package/src/lib/framework/extension-api/extension-api-types.ts +15 -2
  40. package/src/lib/framework/form-engine/custom-form-component-extensions.ts +13 -3
  41. package/src/lib/framework/form-engine/utils.ts +43 -15
  42. package/src/lib/framework/layout-engine/page-layout.tsx +1 -0
  43. package/src/lib/framework/page/detail-page-route-loader.tsx +13 -1
  44. package/src/lib/framework/page/use-detail-page.ts +11 -2
  45. package/src/lib/framework/registry/registry-types.ts +1 -0
  46. package/src/lib/graphql/common-operations.ts +18 -0
  47. package/src/lib/graphql/{fragments.tsx → fragments.ts} +1 -2
  48. package/src/lib/graphql/graphql-env.d.ts +10 -8
  49. 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 { addCustomFormComponent } from '../form-engine/custom-form-component-extensions.js';
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 DashboardDataTableDefinition {
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?: DashboardDataTableDefinition[];
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
- export function getCustomFormComponents() {
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
- export function transformRelationFields<E extends Record<string, any>>(fields: FieldInfo[], entity: E) {
4
- const processedEntity = { ...entity } as any;
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
- for (const field of fields) {
7
- if (field.name !== 'customFields' || !field.typeInfo) {
8
- continue;
9
- }
15
+ // Skip processing if there are no custom fields
16
+ if (!entity.customFields || !processedEntity.customFields) {
17
+ return processedEntity;
18
+ }
10
19
 
11
- if (!entity.customFields || !processedEntity.customFields) {
12
- continue;
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
- for (const customField of field.typeInfo) {
16
- if (customField.type === 'ID') {
17
- const relationField = customField.name;
18
- const propertyAccessorKey = customField.name.replace(/Id$/, '');
19
- const relationValue = entity.customFields[propertyAccessorKey];
20
- const relationIdValue = relationValue?.id;
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(addCustomFields(queryDocument), { id: params.id }),
52
+ getDetailQueryOptions(extendedQueryDocument, { id: params.id }),
41
53
  { id: params.id },
42
54
  );
43
55