@carto/ps-react-ui 4.5.0 → 4.6.0
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/{download-config-DemuQ3Jm.js → download-config-C3I0jWIL.js} +2 -2
- package/dist/{download-config-DemuQ3Jm.js.map → download-config-C3I0jWIL.js.map} +1 -1
- package/dist/{row-D4VOhcNI.js → row-DZSP99LW.js} +2 -2
- package/dist/{row-D4VOhcNI.js.map → row-DZSP99LW.js.map} +1 -1
- package/dist/{series-Bola3CmD.js → series-DLNHDWs0.js} +3 -3
- package/dist/{series-Bola3CmD.js.map → series-DLNHDWs0.js.map} +1 -1
- package/dist/types/hooks/index.d.ts +0 -1
- package/dist/types/widgets/actions/brush-toggle/brush-toggle.d.ts +3 -0
- package/dist/types/widgets/actions/index.d.ts +4 -4
- package/dist/types/widgets/actions/lock-selection/types.d.ts +2 -0
- package/dist/types/widgets/actions/relative-data/relative-data.d.ts +7 -2
- package/dist/types/widgets/actions/relative-data/types.d.ts +2 -0
- package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
- package/dist/types/widgets/category/index.d.ts +10 -2
- package/dist/types/widgets/category/style.d.ts +1 -0
- package/dist/types/widgets/no-data/no-data.d.ts +3 -2
- package/dist/types/widgets/no-data/types.d.ts +5 -1
- package/dist/types/widgets/stores/index.d.ts +1 -1
- package/dist/types/widgets/stores/types.d.ts +10 -10
- package/dist/types/widgets/stores/widget-store.d.ts +2 -3
- package/dist/types/widgets/table/index.d.ts +6 -2
- package/dist/{use-widget-ref-BFazQvJK.js → use-widget-ref-Ddr_SlJJ.js} +2 -2
- package/dist/{use-widget-ref-BFazQvJK.js.map → use-widget-ref-Ddr_SlJJ.js.map} +1 -1
- package/dist/{use-widget-selector-DqRmWQ1K.js → use-widget-selector-DFl2hW0R.js} +2 -2
- package/dist/{use-widget-selector-DqRmWQ1K.js.map → use-widget-selector-DFl2hW0R.js.map} +1 -1
- package/dist/{widget-store-CIrb9RKP.js → widget-store-Bw5zRUGg.js} +93 -95
- package/dist/widget-store-Bw5zRUGg.js.map +1 -0
- package/dist/widgets/actions.js +770 -755
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +2 -2
- package/dist/widgets/category.js +187 -183
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +2 -2
- package/dist/widgets/error.js +37 -2
- package/dist/widgets/error.js.map +1 -1
- package/dist/widgets/formula.js +5 -5
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/loader.js +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/no-data.js +58 -2
- package/dist/widgets/no-data.js.map +1 -1
- package/dist/widgets/note.js +121 -2
- package/dist/widgets/note.js.map +1 -1
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/range.js +3 -3
- package/dist/widgets/scatterplot.js +2 -2
- package/dist/widgets/skeleton-loader.js +1 -1
- package/dist/widgets/spread.js +5 -5
- package/dist/widgets/stores.js +2 -2
- package/dist/widgets/subheader.js +29 -29
- package/dist/widgets/subheader.js.map +1 -1
- package/dist/widgets/table.js +3 -3
- package/dist/widgets/timeseries.js +2 -2
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +2 -2
- package/package.json +1 -5
- package/src/hooks/index.ts +0 -1
- package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -22
- package/src/widgets/actions/change-column/change-column.test.tsx +1 -1
- package/src/widgets/actions/download/download.test.tsx +1 -1
- package/src/widgets/actions/index.ts +11 -2
- package/src/widgets/actions/lock-selection/lock-selection.test.tsx +14 -0
- package/src/widgets/actions/lock-selection/lock-selection.tsx +18 -11
- package/src/widgets/actions/lock-selection/types.ts +2 -0
- package/src/widgets/actions/relative-data/relative-data.test.tsx +211 -20
- package/src/widgets/actions/relative-data/relative-data.tsx +65 -34
- package/src/widgets/actions/relative-data/types.ts +2 -0
- package/src/widgets/actions/searcher/searcher.tsx +28 -30
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +11 -2
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +53 -45
- package/src/widgets/category/category-ui.tsx +7 -6
- package/src/widgets/category/index.ts +13 -14
- package/src/widgets/category/style.ts +1 -0
- package/src/widgets/no-data/no-data.test.tsx +90 -40
- package/src/widgets/no-data/no-data.tsx +7 -5
- package/src/widgets/no-data/types.ts +5 -1
- package/src/widgets/stores/index.ts +2 -0
- package/src/widgets/stores/types.ts +10 -18
- package/src/widgets/stores/widget-store.test.ts +132 -13
- package/src/widgets/stores/widget-store.ts +29 -35
- package/src/widgets/subheader/subheader.tsx +11 -3
- package/src/widgets/table/index.ts +6 -4
- package/dist/error-Cj8eUMrl.js +0 -40
- package/dist/error-Cj8eUMrl.js.map +0 -1
- package/dist/no-data-DkIt7Qt1.js +0 -61
- package/dist/no-data-DkIt7Qt1.js.map +0 -1
- package/dist/note-t51drNe0.js +0 -124
- package/dist/note-t51drNe0.js.map +0 -1
- package/dist/types/hooks/use-debounce.d.ts +0 -19
- package/dist/types/widgets/category/components/index.d.ts +0 -10
- package/dist/types/widgets/index.d.ts +0 -9
- package/dist/types/widgets/table/hooks/index.d.ts +0 -6
- package/dist/widget-store-CIrb9RKP.js.map +0 -1
- package/dist/widgets.js +0 -13
- package/dist/widgets.js.map +0 -1
- package/src/hooks/use-debounce.ts +0 -55
- package/src/widgets/category/components/index.ts +0 -14
- package/src/widgets/index.ts +0 -25
- package/src/widgets/table/hooks/index.ts +0 -7
|
@@ -14,6 +14,10 @@ export interface WidgetsStoreProps {
|
|
|
14
14
|
type: string
|
|
15
15
|
/** Widget data - flexible to accommodate different widget types */
|
|
16
16
|
data: unknown
|
|
17
|
+
/** Original pre-pipeline data. Used by NoData to distinguish
|
|
18
|
+
* "no data from API" from "pipeline tools filtered everything out".
|
|
19
|
+
* Set automatically by executeToolPipeline — not a component prop. */
|
|
20
|
+
sourceData?: unknown
|
|
17
21
|
/** Loading state */
|
|
18
22
|
isLoading: boolean
|
|
19
23
|
/** Fetching state (e.g., for async data) */
|
|
@@ -41,11 +45,7 @@ export interface WidgetsStoreProps {
|
|
|
41
45
|
* Tool transformation function type
|
|
42
46
|
* Can be synchronous or asynchronous to support remote operations
|
|
43
47
|
*/
|
|
44
|
-
export type ToolTransformFunction = (
|
|
45
|
-
data: unknown,
|
|
46
|
-
config?: Record<string, unknown>,
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
48
|
-
) => Promise<unknown> | unknown
|
|
48
|
+
export type ToolTransformFunction = (data: unknown) => unknown
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Tool registration for widget pipeline
|
|
@@ -88,8 +88,6 @@ export interface ToolRegistration {
|
|
|
88
88
|
enabled: boolean
|
|
89
89
|
/** 'data' (default) transforms data, 'config' transforms widget config/option */
|
|
90
90
|
type?: ToolType
|
|
91
|
-
/** Tool-specific configuration */
|
|
92
|
-
config?: Record<string, unknown>
|
|
93
91
|
/**
|
|
94
92
|
* Array of tool IDs to disable when this tool is active.
|
|
95
93
|
* During pipeline execution, if this tool is enabled, any tools listed
|
|
@@ -169,24 +167,18 @@ export interface WidgetStoreActions {
|
|
|
169
167
|
unregisterTool: (widgetId: string, toolId: string) => void
|
|
170
168
|
|
|
171
169
|
/**
|
|
172
|
-
*
|
|
170
|
+
* Set tool enabled state
|
|
173
171
|
* @param widgetId - Widget ID
|
|
174
172
|
* @param toolId - Tool ID
|
|
175
|
-
* @param
|
|
173
|
+
* @param enabled - Whether tool should be enabled
|
|
176
174
|
*/
|
|
177
|
-
|
|
178
|
-
widgetId: string,
|
|
179
|
-
toolId: string,
|
|
180
|
-
config: Record<string, unknown>,
|
|
181
|
-
) => void
|
|
175
|
+
setToolEnabled: (widgetId: string, toolId: string, enabled: boolean) => void
|
|
182
176
|
|
|
183
177
|
/**
|
|
184
|
-
*
|
|
178
|
+
* Trigger pipeline re-execution by bumping the registeredTools reference.
|
|
185
179
|
* @param widgetId - Widget ID
|
|
186
|
-
* @param toolId - Tool ID
|
|
187
|
-
* @param enabled - Whether tool should be enabled
|
|
188
180
|
*/
|
|
189
|
-
|
|
181
|
+
triggerToolPipeline: (widgetId: string) => void
|
|
190
182
|
|
|
191
183
|
/**
|
|
192
184
|
* Execute the tool transformation pipeline
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { useWidgetStore } from './widget-store'
|
|
3
|
+
import type { ToolRegistration } from './types'
|
|
3
4
|
|
|
4
5
|
describe('WidgetStore', () => {
|
|
5
6
|
beforeEach(() => {
|
|
@@ -465,6 +466,82 @@ describe('WidgetStore', () => {
|
|
|
465
466
|
})
|
|
466
467
|
})
|
|
467
468
|
|
|
469
|
+
describe('executeToolPipeline sourceData', () => {
|
|
470
|
+
const widgetId = 'test-widget-source'
|
|
471
|
+
|
|
472
|
+
beforeEach(() => {
|
|
473
|
+
useWidgetStore.getState().clearWidgets()
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('stores sourceData alongside data after pipeline execution', async () => {
|
|
477
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
478
|
+
type: 'bar',
|
|
479
|
+
isLoading: false,
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
const source = [{ value: 1 }, { value: 2 }]
|
|
483
|
+
await useWidgetStore.getState().executeToolPipeline(widgetId, source)
|
|
484
|
+
|
|
485
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
486
|
+
expect(widget?.sourceData).toBe(source)
|
|
487
|
+
expect(widget?.data).toBe(source) // No tools, passthrough
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('preserves sourceData when a tool transforms data to empty', async () => {
|
|
491
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
492
|
+
type: 'bar',
|
|
493
|
+
isLoading: false,
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
const emptyFilterTool: ToolRegistration = {
|
|
497
|
+
id: 'empty-filter',
|
|
498
|
+
order: 10,
|
|
499
|
+
enabled: true,
|
|
500
|
+
fn: () => [], // Transforms any data to empty array
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
useWidgetStore.getState().registerTool(widgetId, emptyFilterTool)
|
|
504
|
+
|
|
505
|
+
const source = [{ value: 1 }, { value: 2 }]
|
|
506
|
+
await useWidgetStore.getState().executeToolPipeline(widgetId, source)
|
|
507
|
+
|
|
508
|
+
const widget = useWidgetStore.getState().getWidget(widgetId)
|
|
509
|
+
expect(widget?.sourceData).toBe(source)
|
|
510
|
+
expect(widget?.data).toEqual([])
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('updates sourceData when input changes but pipeline output stays the same', async () => {
|
|
514
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
515
|
+
type: 'bar',
|
|
516
|
+
isLoading: false,
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
const constantTool: ToolRegistration = {
|
|
520
|
+
id: 'constant',
|
|
521
|
+
order: 10,
|
|
522
|
+
enabled: true,
|
|
523
|
+
fn: () => 'constant-output',
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
useWidgetStore.getState().registerTool(widgetId, constantTool)
|
|
527
|
+
|
|
528
|
+
const source1 = [{ value: 1 }]
|
|
529
|
+
await useWidgetStore.getState().executeToolPipeline(widgetId, source1)
|
|
530
|
+
|
|
531
|
+
expect(useWidgetStore.getState().getWidget(widgetId)?.sourceData).toBe(
|
|
532
|
+
source1,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
const source2 = [{ value: 2 }]
|
|
536
|
+
await useWidgetStore.getState().executeToolPipeline(widgetId, source2)
|
|
537
|
+
|
|
538
|
+
// sourceData should reflect the new input
|
|
539
|
+
expect(useWidgetStore.getState().getWidget(widgetId)?.sourceData).toBe(
|
|
540
|
+
source2,
|
|
541
|
+
)
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
468
545
|
describe('Config Tool Pipeline', () => {
|
|
469
546
|
const widgetId = 'test-widget-config'
|
|
470
547
|
|
|
@@ -478,7 +555,7 @@ describe('WidgetStore', () => {
|
|
|
478
555
|
isLoading: false,
|
|
479
556
|
})
|
|
480
557
|
|
|
481
|
-
const configTool:
|
|
558
|
+
const configTool: ToolRegistration = {
|
|
482
559
|
id: 'stack-tool',
|
|
483
560
|
type: 'config',
|
|
484
561
|
order: 10,
|
|
@@ -536,7 +613,7 @@ describe('WidgetStore', () => {
|
|
|
536
613
|
isLoading: false,
|
|
537
614
|
})
|
|
538
615
|
|
|
539
|
-
const dataTool:
|
|
616
|
+
const dataTool: ToolRegistration = {
|
|
540
617
|
id: 'data-tool',
|
|
541
618
|
type: 'data',
|
|
542
619
|
order: 10,
|
|
@@ -547,7 +624,7 @@ describe('WidgetStore', () => {
|
|
|
547
624
|
},
|
|
548
625
|
}
|
|
549
626
|
|
|
550
|
-
const configTool:
|
|
627
|
+
const configTool: ToolRegistration = {
|
|
551
628
|
id: 'config-tool',
|
|
552
629
|
type: 'config',
|
|
553
630
|
order: 10,
|
|
@@ -575,7 +652,7 @@ describe('WidgetStore', () => {
|
|
|
575
652
|
isLoading: false,
|
|
576
653
|
})
|
|
577
654
|
|
|
578
|
-
const configTool:
|
|
655
|
+
const configTool: ToolRegistration = {
|
|
579
656
|
id: 'config-tool',
|
|
580
657
|
type: 'config',
|
|
581
658
|
order: 10,
|
|
@@ -586,7 +663,7 @@ describe('WidgetStore', () => {
|
|
|
586
663
|
},
|
|
587
664
|
}
|
|
588
665
|
|
|
589
|
-
const disablerTool:
|
|
666
|
+
const disablerTool: ToolRegistration = {
|
|
590
667
|
id: 'disabler',
|
|
591
668
|
type: 'data',
|
|
592
669
|
order: 10,
|
|
@@ -687,6 +764,48 @@ describe('WidgetStore', () => {
|
|
|
687
764
|
})
|
|
688
765
|
})
|
|
689
766
|
|
|
767
|
+
describe('triggerToolPipeline', () => {
|
|
768
|
+
const widgetId = 'test-widget-trigger'
|
|
769
|
+
|
|
770
|
+
beforeEach(() => {
|
|
771
|
+
useWidgetStore.getState().clearWidgets()
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
it('creates new registeredTools reference', () => {
|
|
775
|
+
useWidgetStore.getState().setWidget(widgetId, {
|
|
776
|
+
type: 'bar',
|
|
777
|
+
isLoading: false,
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
useWidgetStore.getState().registerTool(widgetId, {
|
|
781
|
+
id: 'my-tool',
|
|
782
|
+
order: 10,
|
|
783
|
+
enabled: true,
|
|
784
|
+
fn: (data) => data,
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
const toolsBefore =
|
|
788
|
+
useWidgetStore.getState().widgets[widgetId]?.registeredTools
|
|
789
|
+
|
|
790
|
+
useWidgetStore.getState().triggerToolPipeline(widgetId)
|
|
791
|
+
|
|
792
|
+
const toolsAfter =
|
|
793
|
+
useWidgetStore.getState().widgets[widgetId]?.registeredTools
|
|
794
|
+
|
|
795
|
+
// New reference — triggers Effect 4 in WidgetLoader
|
|
796
|
+
expect(toolsBefore).not.toBe(toolsAfter)
|
|
797
|
+
|
|
798
|
+
// But same content
|
|
799
|
+
expect(toolsAfter?.length).toBe(toolsBefore?.length)
|
|
800
|
+
expect(toolsAfter?.[0]?.id).toBe(toolsBefore?.[0]?.id)
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('does nothing for non-existent widget', () => {
|
|
804
|
+
useWidgetStore.getState().triggerToolPipeline('non-existent')
|
|
805
|
+
// No error thrown
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
690
809
|
describe('Tool Dependency Management', () => {
|
|
691
810
|
const widgetId = 'test-widget-deps'
|
|
692
811
|
|
|
@@ -698,7 +817,7 @@ describe('WidgetStore', () => {
|
|
|
698
817
|
it('skips tools disabled by other enabled tools', async () => {
|
|
699
818
|
const executionOrder: string[] = []
|
|
700
819
|
|
|
701
|
-
const toolA:
|
|
820
|
+
const toolA: ToolRegistration = {
|
|
702
821
|
id: 'tool-a',
|
|
703
822
|
order: 10,
|
|
704
823
|
enabled: true,
|
|
@@ -708,7 +827,7 @@ describe('WidgetStore', () => {
|
|
|
708
827
|
},
|
|
709
828
|
}
|
|
710
829
|
|
|
711
|
-
const toolB:
|
|
830
|
+
const toolB: ToolRegistration = {
|
|
712
831
|
id: 'tool-b',
|
|
713
832
|
order: 20,
|
|
714
833
|
enabled: true,
|
|
@@ -718,7 +837,7 @@ describe('WidgetStore', () => {
|
|
|
718
837
|
},
|
|
719
838
|
}
|
|
720
839
|
|
|
721
|
-
const toolC:
|
|
840
|
+
const toolC: ToolRegistration = {
|
|
722
841
|
id: 'tool-c',
|
|
723
842
|
order: 30,
|
|
724
843
|
enabled: true,
|
|
@@ -743,7 +862,7 @@ describe('WidgetStore', () => {
|
|
|
743
862
|
it('includes tool when disabling tool is not enabled', async () => {
|
|
744
863
|
const executionOrder: string[] = []
|
|
745
864
|
|
|
746
|
-
const toolA:
|
|
865
|
+
const toolA: ToolRegistration = {
|
|
747
866
|
id: 'tool-a',
|
|
748
867
|
order: 10,
|
|
749
868
|
enabled: true,
|
|
@@ -753,7 +872,7 @@ describe('WidgetStore', () => {
|
|
|
753
872
|
},
|
|
754
873
|
}
|
|
755
874
|
|
|
756
|
-
const toolB:
|
|
875
|
+
const toolB: ToolRegistration = {
|
|
757
876
|
id: 'tool-b',
|
|
758
877
|
order: 20,
|
|
759
878
|
enabled: false, // Disabled
|
|
@@ -776,7 +895,7 @@ describe('WidgetStore', () => {
|
|
|
776
895
|
it('handles multiple tools disabling the same target', async () => {
|
|
777
896
|
const executionOrder: string[] = []
|
|
778
897
|
|
|
779
|
-
const toolA:
|
|
898
|
+
const toolA: ToolRegistration = {
|
|
780
899
|
id: 'tool-a',
|
|
781
900
|
order: 10,
|
|
782
901
|
enabled: true,
|
|
@@ -786,7 +905,7 @@ describe('WidgetStore', () => {
|
|
|
786
905
|
},
|
|
787
906
|
}
|
|
788
907
|
|
|
789
|
-
const toolB:
|
|
908
|
+
const toolB: ToolRegistration = {
|
|
790
909
|
id: 'tool-b',
|
|
791
910
|
order: 20,
|
|
792
911
|
enabled: true,
|
|
@@ -797,7 +916,7 @@ describe('WidgetStore', () => {
|
|
|
797
916
|
disables: ['tool-a'],
|
|
798
917
|
}
|
|
799
918
|
|
|
800
|
-
const toolC:
|
|
919
|
+
const toolC: ToolRegistration = {
|
|
801
920
|
id: 'tool-c',
|
|
802
921
|
order: 30,
|
|
803
922
|
enabled: true,
|
|
@@ -18,9 +18,8 @@ const activeConfigPipelines = new Map<string, number>()
|
|
|
18
18
|
*
|
|
19
19
|
* **Performance optimizations:**
|
|
20
20
|
* - `registerTool` skips the store update when structural properties (order, enabled,
|
|
21
|
-
* type, disables) haven't changed — only `fn`
|
|
22
|
-
*
|
|
23
|
-
* pipeline cascades.
|
|
21
|
+
* type, disables) haven't changed — only `fn` is updated via direct mutation,
|
|
22
|
+
* avoiding a new `registeredTools` array reference and WidgetLoader pipeline cascades.
|
|
24
23
|
* - `setToolEnabled` skips the store update when the enabled state is already the
|
|
25
24
|
* requested value.
|
|
26
25
|
* - `executeToolPipeline` / `executeConfigPipeline` skip the final `set()` when
|
|
@@ -95,9 +94,9 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
95
94
|
*
|
|
96
95
|
* **No-op optimization:** When a tool with the same `id` already exists and its
|
|
97
96
|
* structural properties (`order`, `enabled`, `type`, `disables`) are unchanged,
|
|
98
|
-
* only `fn`
|
|
99
|
-
*
|
|
100
|
-
*
|
|
97
|
+
* only `fn` is updated via direct mutation — no store update is triggered. This
|
|
98
|
+
* allows action components to include all reactive dependencies in their
|
|
99
|
+
* `useEffect` arrays without causing WidgetLoader pipeline cascades.
|
|
101
100
|
*/
|
|
102
101
|
registerTool: (widgetId: string, tool: ToolRegistration) => {
|
|
103
102
|
const current = get().widgets[widgetId]
|
|
@@ -105,8 +104,8 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
105
104
|
(t: ToolRegistration) => t.id === tool.id,
|
|
106
105
|
)
|
|
107
106
|
|
|
108
|
-
// No-op: structural properties unchanged — update fn
|
|
109
|
-
// Safe because fn
|
|
107
|
+
// No-op: structural properties unchanged — update fn via direct mutation.
|
|
108
|
+
// Safe because fn is only consumed imperatively during pipeline execution.
|
|
110
109
|
if (
|
|
111
110
|
existingTool?.order === tool.order &&
|
|
112
111
|
existingTool.enabled === tool.enabled &&
|
|
@@ -114,7 +113,6 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
114
113
|
existingTool.disables === tool.disables
|
|
115
114
|
) {
|
|
116
115
|
existingTool.fn = tool.fn
|
|
117
|
-
if (tool.config) existingTool.config = tool.config
|
|
118
116
|
return
|
|
119
117
|
}
|
|
120
118
|
|
|
@@ -161,31 +159,22 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
161
159
|
}
|
|
162
160
|
}),
|
|
163
161
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Triggers pipeline re-execution by creating a new `registeredTools` reference.
|
|
164
|
+
* This is a lightweight operation that only bumps the array reference without
|
|
165
|
+
* modifying any tool.
|
|
166
|
+
*/
|
|
167
|
+
triggerToolPipeline: (widgetId: string) =>
|
|
169
168
|
set((state) => {
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
|
|
173
|
-
const registeredTools = current.registeredTools ?? []
|
|
174
|
-
const updatedTools = registeredTools.map((tool: ToolRegistration) =>
|
|
175
|
-
tool.id === toolId
|
|
176
|
-
? {
|
|
177
|
-
...tool,
|
|
178
|
-
config: { ...tool.config, ...config },
|
|
179
|
-
}
|
|
180
|
-
: tool,
|
|
181
|
-
)
|
|
169
|
+
const widget = state.widgets[widgetId]
|
|
170
|
+
if (!widget) return state
|
|
182
171
|
|
|
183
172
|
return {
|
|
184
173
|
widgets: {
|
|
185
174
|
...state.widgets,
|
|
186
175
|
[widgetId]: {
|
|
187
|
-
...
|
|
188
|
-
registeredTools:
|
|
176
|
+
...widget,
|
|
177
|
+
registeredTools: [...(widget.registeredTools ?? [])],
|
|
189
178
|
},
|
|
190
179
|
},
|
|
191
180
|
}
|
|
@@ -277,7 +266,7 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
277
266
|
|
|
278
267
|
try {
|
|
279
268
|
// Call tool function (may return Promise or direct value)
|
|
280
|
-
transformedData = await tool.fn(transformedData
|
|
269
|
+
transformedData = await tool.fn(transformedData)
|
|
281
270
|
} catch (error) {
|
|
282
271
|
// eslint-disable-next-line no-console
|
|
283
272
|
console.error(`Tool ${tool.id} failed for widget ${widgetId}:`, error)
|
|
@@ -285,9 +274,13 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
285
274
|
}
|
|
286
275
|
}
|
|
287
276
|
|
|
288
|
-
// Skip store update if data
|
|
277
|
+
// Skip store update if neither data nor sourceData changed
|
|
289
278
|
const widgetAfter = get().widgets[widgetId]
|
|
290
|
-
if (
|
|
279
|
+
if (
|
|
280
|
+
widgetAfter &&
|
|
281
|
+
Object.is(widgetAfter.data, transformedData) &&
|
|
282
|
+
Object.is(widgetAfter.sourceData, sourceData)
|
|
283
|
+
) {
|
|
291
284
|
if (activePipelines.get(widgetId) === currentExecution) {
|
|
292
285
|
activePipelines.delete(widgetId)
|
|
293
286
|
}
|
|
@@ -304,6 +297,7 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
304
297
|
...state.widgets,
|
|
305
298
|
[widgetId]: {
|
|
306
299
|
...currentWidget,
|
|
300
|
+
sourceData,
|
|
307
301
|
data: transformedData,
|
|
308
302
|
},
|
|
309
303
|
},
|
|
@@ -362,7 +356,7 @@ export const useWidgetStore = create<WidgetStore>()((set, get) => ({
|
|
|
362
356
|
}
|
|
363
357
|
|
|
364
358
|
try {
|
|
365
|
-
transformedConfig = await tool.fn(transformedConfig
|
|
359
|
+
transformedConfig = await tool.fn(transformedConfig)
|
|
366
360
|
} catch (error) {
|
|
367
361
|
// eslint-disable-next-line no-console
|
|
368
362
|
console.error(
|
|
@@ -476,12 +470,12 @@ export const widgetStoreActions: WidgetStoreActions = {
|
|
|
476
470
|
get unregisterTool() {
|
|
477
471
|
return useWidgetStore.getState().unregisterTool
|
|
478
472
|
},
|
|
479
|
-
get updateToolConfig() {
|
|
480
|
-
return useWidgetStore.getState().updateToolConfig
|
|
481
|
-
},
|
|
482
473
|
get setToolEnabled() {
|
|
483
474
|
return useWidgetStore.getState().setToolEnabled
|
|
484
475
|
},
|
|
476
|
+
get triggerToolPipeline() {
|
|
477
|
+
return useWidgetStore.getState().triggerToolPipeline
|
|
478
|
+
},
|
|
485
479
|
get executeToolPipeline() {
|
|
486
480
|
return useWidgetStore.getState().executeToolPipeline
|
|
487
481
|
},
|
|
@@ -19,9 +19,17 @@ export function WidgetSubHeader({
|
|
|
19
19
|
sx,
|
|
20
20
|
}: WidgetSubHeaderProps) {
|
|
21
21
|
return (
|
|
22
|
-
<Box sx={{ ...styles.root, ...sx }}>
|
|
23
|
-
{slotLeft &&
|
|
24
|
-
|
|
22
|
+
<Box sx={{ ...styles.root, ...sx }} className='widget-subheader'>
|
|
23
|
+
{slotLeft && (
|
|
24
|
+
<Box sx={styles.slotLeft} className='widget-subheader-slot-left'>
|
|
25
|
+
{slotLeft}
|
|
26
|
+
</Box>
|
|
27
|
+
)}
|
|
28
|
+
{slotRight && (
|
|
29
|
+
<Box sx={styles.slotRight} className='widget-subheader-slot-right'>
|
|
30
|
+
{slotRight}
|
|
31
|
+
</Box>
|
|
32
|
+
)}
|
|
25
33
|
</Box>
|
|
26
34
|
)
|
|
27
35
|
}
|
|
@@ -8,7 +8,9 @@ export { PaginationActions } from './components/pagination-actions'
|
|
|
8
8
|
export { Table } from './table-ui'
|
|
9
9
|
|
|
10
10
|
// Hooks
|
|
11
|
-
export { usePagination
|
|
11
|
+
export { usePagination } from './hooks/use-pagination'
|
|
12
|
+
export { useSort } from './hooks/use-sort'
|
|
13
|
+
export { useSelection } from './hooks/use-selection'
|
|
12
14
|
|
|
13
15
|
// Config
|
|
14
16
|
export { tableConfig, tableDownloadConfig } from './config'
|
|
@@ -45,9 +47,9 @@ export type {
|
|
|
45
47
|
PaginationActionsProps,
|
|
46
48
|
} from './types'
|
|
47
49
|
|
|
50
|
+
export type { UsePaginationResult } from './hooks/use-pagination'
|
|
51
|
+
export type { UseSortResult } from './hooks/use-sort'
|
|
48
52
|
export type {
|
|
49
|
-
UsePaginationResult,
|
|
50
|
-
UseSortResult,
|
|
51
53
|
UseSelectionOptions,
|
|
52
54
|
UseSelectionResult,
|
|
53
|
-
} from './hooks'
|
|
55
|
+
} from './hooks/use-selection'
|
package/dist/error-Cj8eUMrl.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { jsx as m, jsxs as u } from "react/jsx-runtime";
|
|
2
|
-
import { c as h } from "react/compiler-runtime";
|
|
3
|
-
import { AlertTitle as p, Alert as x } from "@mui/material";
|
|
4
|
-
import { u as A } from "./use-widget-selector-DqRmWQ1K.js";
|
|
5
|
-
function E(t) {
|
|
6
|
-
const r = h(5), {
|
|
7
|
-
id: l,
|
|
8
|
-
children: c,
|
|
9
|
-
title: d,
|
|
10
|
-
description: g
|
|
11
|
-
} = t, {
|
|
12
|
-
isLoading: a,
|
|
13
|
-
isFetching: f,
|
|
14
|
-
error: o
|
|
15
|
-
} = A(l, F);
|
|
16
|
-
if (a || f)
|
|
17
|
-
return c;
|
|
18
|
-
if (o) {
|
|
19
|
-
const n = d ?? o.title ?? "Error", s = g ?? o.message ?? "An error occurred while loading the widget. Please try again.";
|
|
20
|
-
let e;
|
|
21
|
-
r[0] !== n ? (e = /* @__PURE__ */ m(p, { children: n }), r[0] = n, r[1] = e) : e = r[1];
|
|
22
|
-
let i;
|
|
23
|
-
return r[2] !== s || r[3] !== e ? (i = /* @__PURE__ */ u(x, { severity: "error", children: [
|
|
24
|
-
e,
|
|
25
|
-
s
|
|
26
|
-
] }), r[2] = s, r[3] = e, r[4] = i) : i = r[4], i;
|
|
27
|
-
}
|
|
28
|
-
return c;
|
|
29
|
-
}
|
|
30
|
-
function F(t) {
|
|
31
|
-
return {
|
|
32
|
-
isLoading: t?.isLoading,
|
|
33
|
-
isFetching: t?.isFetching,
|
|
34
|
-
error: t?.error
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
export {
|
|
38
|
-
E as W
|
|
39
|
-
};
|
|
40
|
-
//# sourceMappingURL=error-Cj8eUMrl.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"error-Cj8eUMrl.js","sources":["../src/widgets/error/error.tsx"],"sourcesContent":["import { Alert, AlertTitle } from '@mui/material'\nimport { useWidgetSelector } from '../stores/use-widget-selector'\nimport type { WidgetErrorProps } from './types'\n\n/**\n * Displays an error alert when a widget encounters an error during data loading. Reads error state from the widget store and hides errors during loading/fetching to prevent flashing.\n *\n * @example\n * ```tsx\n * <WidgetError id=\"my-widget\">\n * <WidgetContent />\n * </WidgetError>\n * ```\n */\nexport function WidgetError({\n id,\n children,\n title: titleProp,\n description,\n}: WidgetErrorProps) {\n // Single consolidated subscription instead of 3 separate ones.\n const { isLoading, isFetching, error } = useWidgetSelector(id, (w) => ({\n isLoading: w?.isLoading,\n isFetching: w?.isFetching,\n error: w?.error,\n }))\n\n // Don't show error during loading/fetching states\n if (isLoading || isFetching) {\n return children\n }\n\n // Show error UI if error exists\n if (error) {\n const errorTitle = titleProp ?? error.title ?? 'Error'\n const errorMessage =\n description ??\n error.message ??\n 'An error occurred while loading the widget. Please try again.'\n\n return (\n <Alert severity='error'>\n <AlertTitle>{errorTitle}</AlertTitle>\n {errorMessage}\n </Alert>\n )\n }\n\n // No error, render children\n return children\n}\n"],"names":["WidgetError","t0","$","_c","id","children","title","titleProp","description","isLoading","isFetching","error","useWidgetSelector","_temp","errorTitle","errorMessage","message","t1","AlertTitle","t2","jsxs","Alert","w"],"mappings":";;;;AAcO,SAAAA,EAAAC,GAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA,GAAqB;AAAA,IAAAC,IAAAA;AAAAA,IAAAC,UAAAA;AAAAA,IAAAC,OAAAC;AAAAA,IAAAC,aAAAA;AAAAA,EAAAA,IAAAP,GAO1B;AAAA,IAAAQ,WAAAA;AAAAA,IAAAC,YAAAA;AAAAA,IAAAC,OAAAA;AAAAA,EAAAA,IAAyCC,EAAkBR,GAAIS,CAI7D;AAGF,MAAIJ,KAAAC;AAAuB,WAClBL;AAIT,MAAIM,GAAK;AACP,UAAAG,IAAmBP,KAAaI,EAAKL,SAAlB,SACnBS,IACEP,KACAG,EAAKK,WADL;AAE+D,QAAAC;AAAA,IAAAf,SAAAY,KAI7DG,sBAACC,kBAAuB,GAAahB,OAAAY,GAAAZ,OAAAe,KAAAA,IAAAf,EAAA,CAAA;AAAA,QAAAiB;AAAA,WAAAjB,EAAA,CAAA,MAAAa,KAAAb,SAAAe,KADvCE,IAAA,gBAAAC,EAACC,GAAA,EAAe,UAAA,SACdJ,UAAAA;AAAAA,MAAAA;AAAAA,MACCF;AAAAA,IAAAA,GACH,GAAQb,OAAAa,GAAAb,OAAAe,GAAAf,OAAAiB,KAAAA,IAAAjB,EAAA,CAAA,GAHRiB;AAAAA,EAGQ;AAEX,SAGMd;AAAQ;AAnCV,SAAAQ,EAAAS,GAAA;AAAA,SAOkE;AAAA,IAAAb,WAC1Da,GAACb;AAAAA,IAAWC,YACXY,GAACZ;AAAAA,IAAYC,OAClBW,GAACX;AAAAA,EAAAA;AACT;"}
|
package/dist/no-data-DkIt7Qt1.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { jsx as f, jsxs as x } from "react/jsx-runtime";
|
|
2
|
-
import { c as v } from "react/compiler-runtime";
|
|
3
|
-
import { Typography as p, Box as b } from "@mui/material";
|
|
4
|
-
import { u as j } from "./use-widget-selector-DqRmWQ1K.js";
|
|
5
|
-
const A = {
|
|
6
|
-
root: {
|
|
7
|
-
display: "flex",
|
|
8
|
-
flexDirection: "column",
|
|
9
|
-
gap: 1,
|
|
10
|
-
// 8px
|
|
11
|
-
paddingTop: 1,
|
|
12
|
-
// 8px
|
|
13
|
-
paddingBottom: 2,
|
|
14
|
-
// 16px
|
|
15
|
-
paddingX: 2,
|
|
16
|
-
// 16px
|
|
17
|
-
width: "100%",
|
|
18
|
-
minHeight: "100%"
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
function B(e) {
|
|
22
|
-
const t = v(7), {
|
|
23
|
-
id: u,
|
|
24
|
-
children: a,
|
|
25
|
-
title: l,
|
|
26
|
-
description: c,
|
|
27
|
-
isEmpty: d
|
|
28
|
-
} = e, n = l === void 0 ? "No data available" : l, s = c === void 0 ? "There are no results for the combination of filters applied to your data. Try tweaking your filters, or zoom and pan the map to adjust filters" : c, y = d === void 0 ? E : d, {
|
|
29
|
-
isLoading: g,
|
|
30
|
-
isFetching: m,
|
|
31
|
-
data: h
|
|
32
|
-
} = j(u, T);
|
|
33
|
-
if (g || m)
|
|
34
|
-
return a;
|
|
35
|
-
if (y(h)) {
|
|
36
|
-
let r;
|
|
37
|
-
t[0] !== n ? (r = /* @__PURE__ */ f(p, { variant: "body2", color: "text.primary", children: n }), t[0] = n, t[1] = r) : r = t[1];
|
|
38
|
-
let i;
|
|
39
|
-
t[2] !== s ? (i = /* @__PURE__ */ f(p, { variant: "caption", color: "text.secondary", children: s }), t[2] = s, t[3] = i) : i = t[3];
|
|
40
|
-
let o;
|
|
41
|
-
return t[4] !== r || t[5] !== i ? (o = /* @__PURE__ */ x(b, { sx: A.root, children: [
|
|
42
|
-
r,
|
|
43
|
-
i
|
|
44
|
-
] }), t[4] = r, t[5] = i, t[6] = o) : o = t[6], o;
|
|
45
|
-
}
|
|
46
|
-
return a;
|
|
47
|
-
}
|
|
48
|
-
function T(e) {
|
|
49
|
-
return {
|
|
50
|
-
isLoading: e?.isLoading,
|
|
51
|
-
isFetching: e?.isFetching,
|
|
52
|
-
data: e?.data
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function E(e) {
|
|
56
|
-
return e == null ? !0 : Array.isArray(e) ? !!(e.length === 0 || e.every((t) => Array.isArray(t) && t.length === 0)) : typeof e == "object" ? Object.keys(e).length === 0 : !1;
|
|
57
|
-
}
|
|
58
|
-
export {
|
|
59
|
-
B as W
|
|
60
|
-
};
|
|
61
|
-
//# sourceMappingURL=no-data-DkIt7Qt1.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"no-data-DkIt7Qt1.js","sources":["../src/widgets/no-data/style.ts","../src/widgets/no-data/no-data.tsx"],"sourcesContent":["import type { SxProps, Theme } from '@mui/material'\n\n/**\n * Styles for NoData component matching Figma design specifications\n * Design reference: node 5781-11028\n */\nexport const styles: Record<string, SxProps<Theme>> = {\n root: {\n display: 'flex',\n flexDirection: 'column',\n gap: 1, // 8px\n paddingTop: 1, // 8px\n paddingBottom: 2, // 16px\n paddingX: 2, // 16px\n width: '100%',\n minHeight: '100%',\n },\n}\n","import { Box, Typography } from '@mui/material'\nimport { useWidgetSelector } from '../stores/use-widget-selector'\nimport type { WidgetNoDataProps } from './types'\nimport { styles } from './style'\n\n/**\n * NoData wrapper component that displays empty state UI when widget has no data\n *\n * Integrates with widget store to check loading/fetching state and data availability.\n * Works in conjunction with SkeletonLoader for complete loading/empty state handling.\n *\n * @example Basic usage\n * ```tsx\n * <NoData id=\"my-widget\">\n * <WidgetContent id=\"my-widget\" />\n * </NoData>\n * ```\n *\n * @example With SkeletonLoader\n * ```tsx\n * <SkeletonLoader id=\"my-widget\" Skeleton={MySkeleton}>\n * <NoData id=\"my-widget\">\n * <WidgetContent id=\"my-widget\" />\n * </NoData>\n * </SkeletonLoader>\n * ```\n *\n * @example With custom messages\n * ```tsx\n * <NoData\n * id=\"my-widget\"\n * title=\"No results found\"\n * description=\"Try adjusting your filters\"\n * >\n * <WidgetContent id=\"my-widget\" />\n * </NoData>\n * ```\n */\nexport function WidgetNoData({\n id,\n children,\n title = 'No data available',\n description = 'There are no results for the combination of filters applied to your data. Try tweaking your filters, or zoom and pan the map to adjust filters',\n isEmpty = defaultIsEmpty,\n}: WidgetNoDataProps) {\n // Single consolidated subscription instead of 3 separate ones.\n const { isLoading, isFetching, data } = useWidgetSelector(id, (w) => ({\n isLoading: w?.isLoading,\n isFetching: w?.isFetching,\n data: w?.data,\n }))\n\n // If loading or fetching, show children\n // SkeletonLoader handles loading state, this allows proper composition\n if (isLoading || isFetching) {\n return children\n }\n\n // Check if data is empty\n if (isEmpty(data)) {\n return (\n <Box sx={styles.root}>\n <Typography variant='body2' color='text.primary'>\n {title}\n </Typography>\n <Typography variant='caption' color='text.secondary'>\n {description}\n </Typography>\n </Box>\n )\n }\n\n // Data exists, render children\n return children\n}\n\n/**\n * Default function to determine if data is empty\n * Handles various data structures commonly used in widgets\n */\nfunction defaultIsEmpty(data: unknown): boolean {\n // Null or undefined\n if (data == null) {\n return true\n }\n\n // Arrays (most common case)\n if (Array.isArray(data)) {\n // Empty array\n if (data.length === 0) {\n return true\n }\n\n // Array of arrays (CategoryWidget pattern: [[],[]])\n // Check if all inner arrays are empty\n if (data.every((item) => Array.isArray(item) && item.length === 0)) {\n return true\n }\n\n return false\n }\n\n // Objects\n if (typeof data === 'object') {\n return Object.keys(data).length === 0\n }\n\n // Primitives (numbers, strings, booleans) are considered valid data\n return false\n}\n"],"names":["styles","root","display","flexDirection","gap","paddingTop","paddingBottom","paddingX","width","minHeight","WidgetNoData","t0","$","_c","id","children","title","t1","description","t2","isEmpty","t3","undefined","defaultIsEmpty","isLoading","isFetching","data","useWidgetSelector","_temp","t4","Typography","t5","t6","Box","w","Array","isArray","length","every","item","Object","keys"],"mappings":";;;;AAMO,MAAMA,IAAyC;AAAA,EACpDC,MAAM;AAAA,IACJC,SAAS;AAAA,IACTC,eAAe;AAAA,IACfC,KAAK;AAAA;AAAA,IACLC,YAAY;AAAA;AAAA,IACZC,eAAe;AAAA;AAAA,IACfC,UAAU;AAAA;AAAA,IACVC,OAAO;AAAA,IACPC,WAAW;AAAA,EAAA;AAEf;ACqBO,SAAAC,EAAAC,GAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA,GAAsB;AAAA,IAAAC,IAAAA;AAAAA,IAAAC,UAAAA;AAAAA,IAAAC,OAAAC;AAAAA,IAAAC,aAAAC;AAAAA,IAAAC,SAAAC;AAAAA,EAAAA,IAAAV,GAG3BK,IAAAC,MAAAK,SAAA,sBAAAL,GACAC,IAAAC,MAAAG,SAAA,mJAAAH,GACAC,IAAAC,MAAAC,SAAAC,IAAAF,GAGA;AAAA,IAAAG,WAAAA;AAAAA,IAAAC,YAAAA;AAAAA,IAAAC,MAAAA;AAAAA,EAAAA,IAAwCC,EAAkBb,GAAIc,CAI5D;AAIF,MAAIJ,KAAAC;AAAuB,WAClBV;AAIT,MAAIK,EAAQM,CAAI,GAAC;AAAA,QAAAG;AAAA,IAAAjB,SAAAI,KAGXa,sBAACC,GAAA,EAAmB,SAAA,SAAc,OAAA,gBAC/Bd,UAAAA,GACH,GAAaJ,OAAAI,GAAAJ,OAAAiB,KAAAA,IAAAjB,EAAA,CAAA;AAAA,QAAAmB;AAAA,IAAAnB,SAAAM,KACba,sBAACD,GAAA,EAAmB,SAAA,WAAgB,OAAA,kBACjCZ,UAAAA,GACH,GAAaN,OAAAM,GAAAN,OAAAmB,KAAAA,IAAAnB,EAAA,CAAA;AAAA,QAAAoB;AAAA,WAAApB,EAAA,CAAA,MAAAiB,KAAAjB,SAAAmB,KANfC,sBAACC,GAAA,EAAQ,IAAAjC,EAAMC,MACb4B,UAAAA;AAAAA,MAAAA;AAAAA,MAGAE;AAAAA,IAAAA,GAGF,GAAMnB,OAAAiB,GAAAjB,OAAAmB,GAAAnB,OAAAoB,KAAAA,IAAApB,EAAA,CAAA,GAPNoB;AAAAA,EAOM;AAET,SAGMjB;AAAQ;AAnCV,SAAAa,EAAAM,GAAA;AAAA,SAQiE;AAAA,IAAAV,WACzDU,GAACV;AAAAA,IAAWC,YACXS,GAACT;AAAAA,IAAYC,MACnBQ,GAACR;AAAAA,EAAAA;AACR;AA8BH,SAASH,EAAeG,GAAwB;AAE9C,SAAIA,KAAQ,OACH,KAILS,MAAMC,QAAQV,CAAI,IAEhBA,GAAAA,EAAKW,WAAW,KAMhBX,EAAKY,MAAOC,CAAAA,MAASJ,MAAMC,QAAQG,CAAI,KAAKA,EAAKF,WAAW,CAAC,KAQ/D,OAAOX,KAAS,WACXc,OAAOC,KAAKf,CAAI,EAAEW,WAAW,IAI/B;AACT;"}
|