@vendure/dashboard 3.3.6-master-202507090236 → 3.3.6-master-202507110238
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/tests/barrel-exports.spec.js +1 -1
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
- package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
- package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
- package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +268 -31
- package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
- package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -17
- package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
- package/src/lib/components/data-display/date-time.tsx +7 -1
- package/src/lib/components/data-input/relation-input.tsx +11 -0
- package/src/lib/components/data-input/relation-selector.tsx +9 -2
- package/src/lib/components/data-table/data-table-utils.ts +34 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +5 -2
- package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
- package/src/lib/components/shared/product-variant-selector.tsx +28 -4
- package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
- package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
- package/src/lib/framework/extension-api/types/layout.ts +21 -6
- package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
- package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
- package/src/lib/framework/page/use-detail-page.ts +10 -7
- package/vite/tests/barrel-exports.spec.ts +1 -1
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { graphql } from 'gql.tada';
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getFieldsFromDocumentNode,
|
|
6
|
+
getListQueryFields,
|
|
7
|
+
getOperationVariablesFields,
|
|
8
|
+
} from './get-document-structure.js';
|
|
5
9
|
|
|
6
10
|
vi.mock('virtual:admin-api-schema', () => {
|
|
7
11
|
return {
|
|
@@ -11,6 +15,7 @@ vi.mock('virtual:admin-api-schema', () => {
|
|
|
11
15
|
products: ['ProductList', false, false, true],
|
|
12
16
|
product: ['Product', false, false, false],
|
|
13
17
|
collection: ['Collection', false, false, false],
|
|
18
|
+
order: ['Order', false, false, false],
|
|
14
19
|
},
|
|
15
20
|
Mutation: {
|
|
16
21
|
updateProduct: ['Product', false, false, false],
|
|
@@ -148,7 +153,17 @@ vi.mock('virtual:admin-api-schema', () => {
|
|
|
148
153
|
quantity: ['Int', false, false, false],
|
|
149
154
|
},
|
|
150
155
|
},
|
|
151
|
-
scalars: [
|
|
156
|
+
scalars: [
|
|
157
|
+
'ID',
|
|
158
|
+
'String',
|
|
159
|
+
'Int',
|
|
160
|
+
'Boolean',
|
|
161
|
+
'Float',
|
|
162
|
+
'JSON',
|
|
163
|
+
'DateTime',
|
|
164
|
+
'Upload',
|
|
165
|
+
'CurrencyCode',
|
|
166
|
+
],
|
|
152
167
|
enums: {},
|
|
153
168
|
},
|
|
154
169
|
};
|
|
@@ -372,6 +387,9 @@ describe('getOperationVariablesFields', () => {
|
|
|
372
387
|
mutation AdjustDraftOrderLine($orderId: ID!, $input: AdjustDraftOrderLineInput!) {
|
|
373
388
|
adjustDraftOrderLine(orderId: $orderId, input: $input) {
|
|
374
389
|
id
|
|
390
|
+
lines {
|
|
391
|
+
id
|
|
392
|
+
}
|
|
375
393
|
}
|
|
376
394
|
}
|
|
377
395
|
`);
|
|
@@ -418,3 +436,304 @@ describe('getOperationVariablesFields', () => {
|
|
|
418
436
|
]);
|
|
419
437
|
});
|
|
420
438
|
});
|
|
439
|
+
|
|
440
|
+
describe('getFieldsFromDocumentNode', () => {
|
|
441
|
+
it('should extract fields from a simple path', () => {
|
|
442
|
+
const doc = graphql(`
|
|
443
|
+
query {
|
|
444
|
+
order(id: "1") {
|
|
445
|
+
id
|
|
446
|
+
lines {
|
|
447
|
+
id
|
|
448
|
+
quantity
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
`);
|
|
453
|
+
|
|
454
|
+
const fields = getFieldsFromDocumentNode(doc, ['order', 'lines']);
|
|
455
|
+
expect(fields).toEqual([
|
|
456
|
+
{
|
|
457
|
+
isPaginatedList: false,
|
|
458
|
+
isScalar: true,
|
|
459
|
+
list: false,
|
|
460
|
+
name: 'id',
|
|
461
|
+
nullable: false,
|
|
462
|
+
type: 'ID',
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
isPaginatedList: false,
|
|
466
|
+
isScalar: true,
|
|
467
|
+
list: false,
|
|
468
|
+
name: 'quantity',
|
|
469
|
+
nullable: false,
|
|
470
|
+
type: 'Int',
|
|
471
|
+
},
|
|
472
|
+
]);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should extract fields from root level', () => {
|
|
476
|
+
const doc = graphql(`
|
|
477
|
+
query {
|
|
478
|
+
product {
|
|
479
|
+
id
|
|
480
|
+
name
|
|
481
|
+
description
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
`);
|
|
485
|
+
|
|
486
|
+
const fields = getFieldsFromDocumentNode(doc, ['product']);
|
|
487
|
+
expect(fields).toEqual([
|
|
488
|
+
{
|
|
489
|
+
isPaginatedList: false,
|
|
490
|
+
isScalar: true,
|
|
491
|
+
list: false,
|
|
492
|
+
name: 'id',
|
|
493
|
+
nullable: false,
|
|
494
|
+
type: 'ID',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
isPaginatedList: false,
|
|
498
|
+
isScalar: true,
|
|
499
|
+
list: false,
|
|
500
|
+
name: 'name',
|
|
501
|
+
nullable: false,
|
|
502
|
+
type: 'String',
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
isPaginatedList: false,
|
|
506
|
+
isScalar: true,
|
|
507
|
+
list: false,
|
|
508
|
+
name: 'description',
|
|
509
|
+
nullable: false,
|
|
510
|
+
type: 'String',
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should handle fragments in the target selection', () => {
|
|
516
|
+
const doc = graphql(`
|
|
517
|
+
query {
|
|
518
|
+
product {
|
|
519
|
+
id
|
|
520
|
+
featuredAsset {
|
|
521
|
+
...AssetFields
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
fragment AssetFields on Asset {
|
|
527
|
+
id
|
|
528
|
+
name
|
|
529
|
+
preview
|
|
530
|
+
}
|
|
531
|
+
`);
|
|
532
|
+
|
|
533
|
+
const fields = getFieldsFromDocumentNode(doc, ['product', 'featuredAsset']);
|
|
534
|
+
expect(fields).toEqual([
|
|
535
|
+
{
|
|
536
|
+
isPaginatedList: false,
|
|
537
|
+
isScalar: true,
|
|
538
|
+
list: false,
|
|
539
|
+
name: 'id',
|
|
540
|
+
nullable: false,
|
|
541
|
+
type: 'ID',
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
isPaginatedList: false,
|
|
545
|
+
isScalar: true,
|
|
546
|
+
list: false,
|
|
547
|
+
name: 'name',
|
|
548
|
+
nullable: false,
|
|
549
|
+
type: 'String',
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
isPaginatedList: false,
|
|
553
|
+
isScalar: true,
|
|
554
|
+
list: false,
|
|
555
|
+
name: 'preview',
|
|
556
|
+
nullable: false,
|
|
557
|
+
type: 'String',
|
|
558
|
+
},
|
|
559
|
+
]);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should handle deep nested paths', () => {
|
|
563
|
+
const doc = graphql(`
|
|
564
|
+
query {
|
|
565
|
+
product {
|
|
566
|
+
variants {
|
|
567
|
+
prices {
|
|
568
|
+
currencyCode
|
|
569
|
+
price
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
`);
|
|
575
|
+
|
|
576
|
+
const fields = getFieldsFromDocumentNode(doc, ['product', 'variants', 'prices']);
|
|
577
|
+
expect(fields).toEqual([
|
|
578
|
+
{
|
|
579
|
+
isPaginatedList: false,
|
|
580
|
+
isScalar: true,
|
|
581
|
+
list: false,
|
|
582
|
+
name: 'currencyCode',
|
|
583
|
+
nullable: false,
|
|
584
|
+
type: 'CurrencyCode',
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
isPaginatedList: false,
|
|
588
|
+
isScalar: false,
|
|
589
|
+
list: false,
|
|
590
|
+
name: 'price',
|
|
591
|
+
nullable: false,
|
|
592
|
+
type: 'Money',
|
|
593
|
+
},
|
|
594
|
+
]);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should throw error for non-existent field', () => {
|
|
598
|
+
const doc = graphql(`
|
|
599
|
+
query {
|
|
600
|
+
product {
|
|
601
|
+
id
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
`);
|
|
605
|
+
|
|
606
|
+
expect(() => getFieldsFromDocumentNode(doc, ['product', 'nonExistentField'])).toThrow(
|
|
607
|
+
'Field "nonExistentField" not found at path product.nonExistentField',
|
|
608
|
+
);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should throw error for non-existent path', () => {
|
|
612
|
+
const doc = graphql(`
|
|
613
|
+
query {
|
|
614
|
+
product {
|
|
615
|
+
id
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
`);
|
|
619
|
+
|
|
620
|
+
expect(() => getFieldsFromDocumentNode(doc, ['nonExistentProduct'])).toThrow(
|
|
621
|
+
'Field "nonExistentProduct" not found at path nonExistentProduct',
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should throw error when field has no selection set but path continues', () => {
|
|
626
|
+
const doc = graphql(`
|
|
627
|
+
query {
|
|
628
|
+
product {
|
|
629
|
+
id
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
`);
|
|
633
|
+
|
|
634
|
+
expect(() => getFieldsFromDocumentNode(doc, ['product', 'id', 'something'])).toThrow(
|
|
635
|
+
'Field "id" has no selection set but path continues',
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should handle empty selection set', () => {
|
|
640
|
+
const doc = graphql(`
|
|
641
|
+
query {
|
|
642
|
+
product {
|
|
643
|
+
id
|
|
644
|
+
name
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
`);
|
|
648
|
+
|
|
649
|
+
// Test with a path that leads to a field with no selection set
|
|
650
|
+
expect(() => getFieldsFromDocumentNode(doc, ['product', 'id', 'something'])).toThrow(
|
|
651
|
+
'Field "id" has no selection set but path continues',
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should handle mixed field types and fragments', () => {
|
|
656
|
+
const doc = graphql(`
|
|
657
|
+
query {
|
|
658
|
+
product {
|
|
659
|
+
id
|
|
660
|
+
featuredAsset {
|
|
661
|
+
...AssetFields
|
|
662
|
+
fileSize
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
fragment AssetFields on Asset {
|
|
668
|
+
id
|
|
669
|
+
name
|
|
670
|
+
}
|
|
671
|
+
`);
|
|
672
|
+
|
|
673
|
+
const fields = getFieldsFromDocumentNode(doc, ['product', 'featuredAsset']);
|
|
674
|
+
expect(fields).toEqual([
|
|
675
|
+
{
|
|
676
|
+
isPaginatedList: false,
|
|
677
|
+
isScalar: true,
|
|
678
|
+
list: false,
|
|
679
|
+
name: 'id',
|
|
680
|
+
nullable: false,
|
|
681
|
+
type: 'ID',
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
isPaginatedList: false,
|
|
685
|
+
isScalar: true,
|
|
686
|
+
list: false,
|
|
687
|
+
name: 'name',
|
|
688
|
+
nullable: false,
|
|
689
|
+
type: 'String',
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
isPaginatedList: false,
|
|
693
|
+
isScalar: true,
|
|
694
|
+
list: false,
|
|
695
|
+
name: 'fileSize',
|
|
696
|
+
nullable: false,
|
|
697
|
+
type: 'Int',
|
|
698
|
+
},
|
|
699
|
+
]);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('should handle fields within fragment spreads', () => {
|
|
703
|
+
const doc = graphql(`
|
|
704
|
+
query {
|
|
705
|
+
order {
|
|
706
|
+
...OrderFields
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
fragment OrderFields on Order {
|
|
711
|
+
id
|
|
712
|
+
lines {
|
|
713
|
+
id
|
|
714
|
+
quantity
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
`);
|
|
718
|
+
|
|
719
|
+
const fields = getFieldsFromDocumentNode(doc, ['order', 'lines']);
|
|
720
|
+
expect(fields).toEqual([
|
|
721
|
+
{
|
|
722
|
+
isPaginatedList: false,
|
|
723
|
+
isScalar: true,
|
|
724
|
+
list: false,
|
|
725
|
+
name: 'id',
|
|
726
|
+
nullable: false,
|
|
727
|
+
type: 'ID',
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
isPaginatedList: false,
|
|
731
|
+
isScalar: true,
|
|
732
|
+
list: false,
|
|
733
|
+
name: 'quantity',
|
|
734
|
+
nullable: false,
|
|
735
|
+
type: 'Int',
|
|
736
|
+
},
|
|
737
|
+
]);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
2
|
-
import { VariablesOf } from 'gql.tada';
|
|
2
|
+
import { ResultOf, VariablesOf } from 'gql.tada';
|
|
3
3
|
import {
|
|
4
4
|
DocumentNode,
|
|
5
5
|
FieldNode,
|
|
@@ -32,33 +32,165 @@ export interface FieldInfo {
|
|
|
32
32
|
*/
|
|
33
33
|
export function getListQueryFields(documentNode: DocumentNode): FieldInfo[] {
|
|
34
34
|
const fields: FieldInfo[] = [];
|
|
35
|
-
const fragments
|
|
35
|
+
const fragments = collectFragments(documentNode);
|
|
36
|
+
const operationDefinition = findQueryOperation(documentNode);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
for (const query of operationDefinition.selectionSet.selections) {
|
|
39
|
+
if (query.kind === 'Field') {
|
|
40
|
+
const queryField = query;
|
|
41
|
+
const fieldInfo = getQueryInfo(queryField.name.value);
|
|
42
|
+
if (fieldInfo.isPaginatedList) {
|
|
43
|
+
processPaginatedList(queryField, fieldInfo, fields, fragments);
|
|
44
|
+
} else if (queryField.selectionSet) {
|
|
45
|
+
// Check for nested paginated lists
|
|
46
|
+
findNestedPaginatedLists(queryField, fieldInfo.type, fields, fragments);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fields;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Utility type to get all valid paths into a type
|
|
55
|
+
export type PathTo<T> = T extends object
|
|
56
|
+
? {
|
|
57
|
+
[K in keyof T & (string | number)]: [K] | [K, ...PathTo<T[K]>];
|
|
58
|
+
}[keyof T & (string | number)]
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @description
|
|
63
|
+
* This function is used to get the FieldInfo for the fields in the path of a DocumentNode.
|
|
64
|
+
*
|
|
65
|
+
* For example, in the following query:
|
|
66
|
+
*
|
|
67
|
+
* ```graphql
|
|
68
|
+
* query {
|
|
69
|
+
* product {
|
|
70
|
+
* id
|
|
71
|
+
* name
|
|
72
|
+
* variants {
|
|
73
|
+
* id
|
|
74
|
+
* name
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* The path `['product', 'variants']` will return the FieldInfo for the `variants` field,
|
|
81
|
+
* namely the `id` and `name` fields.
|
|
82
|
+
*/
|
|
83
|
+
export function getFieldsFromDocumentNode<
|
|
84
|
+
T extends TypedDocumentNode<any, any>,
|
|
85
|
+
P extends PathTo<ResultOf<T>>,
|
|
86
|
+
>(documentNode: T, path: P): FieldInfo[] {
|
|
87
|
+
const fragments = collectFragments(documentNode);
|
|
88
|
+
const operationDefinition = findQueryOperation(documentNode);
|
|
89
|
+
|
|
90
|
+
// Navigate to the target path
|
|
91
|
+
let currentSelections = operationDefinition.selectionSet.selections;
|
|
92
|
+
let currentType = 'Query';
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < path.length; i++) {
|
|
95
|
+
const pathSegment = path[i] as string;
|
|
96
|
+
const { fieldNode } = findFieldInSelections(
|
|
97
|
+
currentSelections,
|
|
98
|
+
pathSegment,
|
|
99
|
+
fragments,
|
|
100
|
+
path.slice(0, i + 1),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const fieldInfo = getObjectFieldInfo(currentType, pathSegment);
|
|
104
|
+
if (!fieldInfo) {
|
|
105
|
+
throw new Error(`Could not determine type for field "${pathSegment}"`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If this is the last path segment, collect the fields
|
|
109
|
+
if (i === path.length - 1) {
|
|
110
|
+
return collectFieldsFromNode(fieldNode, fieldInfo.type, fragments);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Continue navigating deeper
|
|
114
|
+
if (!fieldNode.selectionSet) {
|
|
115
|
+
throw new Error(`Field "${pathSegment}" has no selection set but path continues`);
|
|
116
|
+
}
|
|
117
|
+
currentSelections = fieldNode.selectionSet.selections;
|
|
118
|
+
currentType = fieldInfo.type;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function collectFragments(documentNode: DocumentNode): Record<string, FragmentDefinitionNode> {
|
|
125
|
+
const fragments: Record<string, FragmentDefinitionNode> = {};
|
|
38
126
|
documentNode.definitions.forEach(def => {
|
|
39
127
|
if (def.kind === 'FragmentDefinition') {
|
|
40
128
|
fragments[def.name.value] = def;
|
|
41
129
|
}
|
|
42
130
|
});
|
|
131
|
+
return fragments;
|
|
132
|
+
}
|
|
43
133
|
|
|
134
|
+
function findQueryOperation(documentNode: DocumentNode): OperationDefinitionNode {
|
|
44
135
|
const operationDefinition = documentNode.definitions.find(
|
|
45
136
|
(def): def is OperationDefinitionNode =>
|
|
46
137
|
def.kind === 'OperationDefinition' && def.operation === 'query',
|
|
47
138
|
);
|
|
139
|
+
if (!operationDefinition) {
|
|
140
|
+
throw new Error('Could not find query operation definition');
|
|
141
|
+
}
|
|
142
|
+
return operationDefinition;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function findMutationOperation(documentNode: DocumentNode): OperationDefinitionNode {
|
|
146
|
+
const operationDefinition = documentNode.definitions.find(
|
|
147
|
+
(def): def is OperationDefinitionNode =>
|
|
148
|
+
def.kind === 'OperationDefinition' && def.operation === 'mutation',
|
|
149
|
+
);
|
|
150
|
+
if (!operationDefinition) {
|
|
151
|
+
throw new Error('Could not find mutation operation definition');
|
|
152
|
+
}
|
|
153
|
+
return operationDefinition;
|
|
154
|
+
}
|
|
48
155
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
156
|
+
function findFieldInSelections(
|
|
157
|
+
selections: readonly any[],
|
|
158
|
+
pathSegment: string,
|
|
159
|
+
fragments: Record<string, FragmentDefinitionNode>,
|
|
160
|
+
currentPath: string[] = [],
|
|
161
|
+
): { fieldNode: FieldNode; fragmentSelections: readonly any[] } {
|
|
162
|
+
for (const selection of selections) {
|
|
163
|
+
if (selection.kind === 'Field' && selection.name.value === pathSegment) {
|
|
164
|
+
return { fieldNode: selection, fragmentSelections: [] };
|
|
165
|
+
} else if (selection.kind === 'FragmentSpread') {
|
|
166
|
+
const fragment = fragments[selection.name.value];
|
|
167
|
+
if (fragment) {
|
|
168
|
+
const fragmentField = fragment.selectionSet.selections.find(
|
|
169
|
+
s => s.kind === 'Field' && s.name.value === pathSegment,
|
|
170
|
+
) as FieldNode;
|
|
171
|
+
if (fragmentField) {
|
|
172
|
+
return { fieldNode: fragmentField, fragmentSelections: fragment.selectionSet.selections };
|
|
173
|
+
}
|
|
58
174
|
}
|
|
59
175
|
}
|
|
60
176
|
}
|
|
177
|
+
const pathString = currentPath.join('.');
|
|
178
|
+
throw new Error(`Field "${pathSegment}" not found at path ${pathString}`);
|
|
179
|
+
}
|
|
61
180
|
|
|
181
|
+
function collectFieldsFromNode(
|
|
182
|
+
fieldNode: FieldNode,
|
|
183
|
+
typeName: string,
|
|
184
|
+
fragments: Record<string, FragmentDefinitionNode>,
|
|
185
|
+
): FieldInfo[] {
|
|
186
|
+
const fields: FieldInfo[] = [];
|
|
187
|
+
if (fieldNode.selectionSet) {
|
|
188
|
+
for (const selection of fieldNode.selectionSet.selections) {
|
|
189
|
+
if (selection.kind === 'Field' || selection.kind === 'FragmentSpread') {
|
|
190
|
+
collectFields(typeName, selection, fields, fragments);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
62
194
|
return fields;
|
|
63
195
|
}
|
|
64
196
|
|
|
@@ -204,11 +336,8 @@ function unwrapVariableDefinitionType(type: TypeNode): NamedTypeNode {
|
|
|
204
336
|
* Helper function to get the first field selection from a query operation definition.
|
|
205
337
|
*/
|
|
206
338
|
function getFirstQueryField(documentNode: DocumentNode): FieldNode {
|
|
207
|
-
const operationDefinition = documentNode
|
|
208
|
-
|
|
209
|
-
def.kind === 'OperationDefinition' && def.operation === 'query',
|
|
210
|
-
);
|
|
211
|
-
const firstSelection = operationDefinition?.selectionSet.selections[0];
|
|
339
|
+
const operationDefinition = findQueryOperation(documentNode);
|
|
340
|
+
const firstSelection = operationDefinition.selectionSet.selections[0];
|
|
212
341
|
if (firstSelection?.kind === 'Field') {
|
|
213
342
|
return firstSelection;
|
|
214
343
|
} else {
|
|
@@ -305,15 +434,7 @@ export function getObjectPathToPaginatedList(
|
|
|
305
434
|
documentNode: DocumentNode,
|
|
306
435
|
currentPath: string[] = [],
|
|
307
436
|
): string[] {
|
|
308
|
-
|
|
309
|
-
const operationDefinition = documentNode.definitions.find(
|
|
310
|
-
(def): def is OperationDefinitionNode =>
|
|
311
|
-
def.kind === 'OperationDefinition' && def.operation === 'query',
|
|
312
|
-
);
|
|
313
|
-
if (!operationDefinition) {
|
|
314
|
-
throw new Error('Could not find query operation definition');
|
|
315
|
-
}
|
|
316
|
-
|
|
437
|
+
const operationDefinition = findQueryOperation(documentNode);
|
|
317
438
|
return findPaginatedListPath(operationDefinition.selectionSet, 'Query', currentPath);
|
|
318
439
|
}
|
|
319
440
|
|
|
@@ -365,11 +486,8 @@ function findPaginatedListPath(
|
|
|
365
486
|
* The mutation name is `createProduct`.
|
|
366
487
|
*/
|
|
367
488
|
export function getMutationName(documentNode: DocumentNode): string {
|
|
368
|
-
const operationDefinition = documentNode
|
|
369
|
-
|
|
370
|
-
def.kind === 'OperationDefinition' && def.operation === 'mutation',
|
|
371
|
-
);
|
|
372
|
-
const firstSelection = operationDefinition?.selectionSet.selections[0];
|
|
489
|
+
const operationDefinition = findMutationOperation(documentNode);
|
|
490
|
+
const firstSelection = operationDefinition.selectionSet.selections[0];
|
|
373
491
|
if (firstSelection?.kind === 'Field') {
|
|
374
492
|
return firstSelection.name.value;
|
|
375
493
|
} else {
|
|
@@ -27,6 +27,27 @@ export interface DashboardActionBarItem {
|
|
|
27
27
|
* A React component that will be rendered in the action bar.
|
|
28
28
|
*/
|
|
29
29
|
component: React.FunctionComponent<{ context: PageContextValue }>;
|
|
30
|
+
/**
|
|
31
|
+
* @description
|
|
32
|
+
* The type of action bar item to display. Defaults to `button`.
|
|
33
|
+
* The 'dropdown' type is used to display the action bar item as a dropdown menu item.
|
|
34
|
+
*
|
|
35
|
+
* When using the dropdown type, use a suitable [dropdown item](https://ui.shadcn.com/docs/components/dropdown-menu)
|
|
36
|
+
* component, such as:
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* import { DropdownMenuItem } from '\@vendure/dashboard';
|
|
40
|
+
*
|
|
41
|
+
* // ...
|
|
42
|
+
*
|
|
43
|
+
* {
|
|
44
|
+
* component: () => <DropdownMenuItem>My Item</DropdownMenuItem>
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @default 'button'
|
|
49
|
+
*/
|
|
50
|
+
type?: 'button' | 'dropdown';
|
|
30
51
|
/**
|
|
31
52
|
* @description
|
|
32
53
|
* Any permissions that are required to display this action bar item.
|
|
@@ -34,12 +55,6 @@ export interface DashboardActionBarItem {
|
|
|
34
55
|
requiresPermission?: string | string[];
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
export interface DashboardActionBarDropdownMenuItem {
|
|
38
|
-
locationId: string;
|
|
39
|
-
component: React.FunctionComponent<{ context: PageContextValue }>;
|
|
40
|
-
requiresPermission?: string | string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
58
|
export type PageBlockPosition = { blockId: string; order: 'before' | 'after' | 'replace' };
|
|
44
59
|
|
|
45
60
|
/**
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DashboardActionBarItem,
|
|
3
|
-
DashboardPageBlockDefinition,
|
|
4
|
-
} from '../extension-api/extension-api-types.js';
|
|
1
|
+
import { DashboardActionBarItem, DashboardPageBlockDefinition } from '../extension-api/types/layout.js';
|
|
5
2
|
import { globalRegistry } from '../registry/global-registry.js';
|
|
6
3
|
|
|
7
4
|
globalRegistry.register('dashboardActionBarItemRegistry', new Map<string, DashboardActionBarItem[]>());
|