@cccsaurora/howler-ui 2.17.0-dev.564 → 2.17.0-dev.617
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/api/index.d.ts +2 -0
- package/api/index.js +4 -2
- package/api/search/case.d.ts +4 -0
- package/api/search/case.js +8 -0
- package/api/search/index.d.ts +2 -1
- package/api/search/index.js +2 -1
- package/api/v2/case/index.d.ts +6 -0
- package/api/v2/case/index.js +18 -0
- package/api/v2/index.d.ts +4 -0
- package/api/v2/index.js +6 -0
- package/api/v2/search/facet.d.ts +3 -0
- package/api/v2/search/facet.js +12 -0
- package/api/v2/search/index.d.ts +5 -0
- package/api/v2/search/index.js +24 -0
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.js +14 -0
- package/components/app/providers/FavouritesProvider.js +2 -2
- package/components/app/providers/HitSearchProvider.d.ts +0 -1
- package/components/app/providers/HitSearchProvider.js +6 -11
- package/components/app/providers/HitSearchProvider.test.js +11 -32
- package/components/app/providers/ParameterProvider.d.ts +9 -2
- package/components/app/providers/ParameterProvider.js +165 -240
- package/components/app/providers/ParameterProvider.test.js +307 -14
- package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
- package/components/elements/ObjectDetails.d.ts +6 -0
- package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
- package/components/elements/PluginTypography.d.ts +2 -1
- package/components/elements/PluginTypography.js +3 -2
- package/components/elements/UserList.d.ts +5 -2
- package/components/elements/UserList.js +14 -5
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/case/CaseCard.d.ts +8 -0
- package/components/elements/case/CaseCard.js +39 -0
- package/components/elements/case/CasePreview.d.ts +6 -0
- package/components/elements/case/CasePreview.js +17 -0
- package/components/elements/case/StatusIcon.d.ts +5 -0
- package/components/elements/case/StatusIcon.js +13 -0
- package/components/elements/display/ChipPopper.d.ts +1 -0
- package/components/elements/display/ChipPopper.js +2 -2
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/Modal.js +1 -0
- package/components/elements/hit/HitBanner.js +28 -48
- package/components/elements/hit/HitCard.js +1 -1
- package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
- package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
- package/components/elements/hit/HitRelated.d.ts +1 -1
- package/components/elements/hit/HitRelated.js +30 -3
- package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
- package/components/elements/hit/elements/AnalyticLink.js +22 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/hit/related/RelatedRecords.js +63 -0
- package/components/elements/observable/ObservableCard.d.ts +5 -0
- package/components/elements/observable/ObservableCard.js +7 -0
- package/components/elements/observable/ObservablePreview.d.ts +6 -0
- package/components/elements/observable/ObservablePreview.js +12 -0
- package/components/elements/view/ViewTitle.js +1 -1
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +2 -2
- package/components/hooks/useHitSelection.js +3 -24
- package/components/hooks/useMyPreferences.js +10 -1
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +4 -1
- package/components/hooks/useMyTheme.js +9 -2
- package/components/hooks/useRelatedRecords.d.ts +13 -0
- package/components/hooks/useRelatedRecords.js +32 -0
- package/components/routes/action/view/ActionSearch.js +1 -1
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/analytics/AnalyticDetails.js +2 -2
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/cases/CaseViewer.d.ts +2 -0
- package/components/routes/cases/CaseViewer.js +24 -0
- package/components/routes/cases/Cases.d.ts +2 -0
- package/components/routes/cases/Cases.js +101 -0
- package/components/routes/cases/constants.d.ts +5 -0
- package/components/routes/cases/constants.js +5 -0
- package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
- package/components/routes/cases/detail/AlertPanel.js +32 -0
- package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
- package/components/routes/cases/detail/CaseDashboard.js +49 -0
- package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
- package/components/routes/cases/detail/CaseDetails.js +61 -0
- package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
- package/components/routes/cases/detail/CaseOverview.js +43 -0
- package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
- package/components/routes/cases/detail/CaseSidebar.js +36 -0
- package/components/routes/cases/detail/CaseTask.d.ts +11 -0
- package/components/routes/cases/detail/CaseTask.js +57 -0
- package/components/routes/cases/detail/ItemPage.d.ts +6 -0
- package/components/routes/cases/detail/ItemPage.js +93 -0
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
- package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
- package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
- package/components/routes/cases/detail/TaskPanel.js +52 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +13 -0
- package/components/routes/cases/detail/sidebar/CaseFolder.js +134 -0
- package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
- package/components/routes/cases/detail/sidebar/utils.js +25 -0
- package/components/routes/cases/hooks/useCase.d.ts +13 -0
- package/components/routes/cases/hooks/useCase.js +38 -0
- package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
- package/components/routes/cases/modals/ResolveModal.js +59 -0
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/HitDocumentation.js +1 -3
- package/components/routes/hits/search/HitContextMenu.js +3 -2
- package/components/routes/hits/search/InformationPane.d.ts +1 -0
- package/components/routes/hits/search/InformationPane.js +6 -28
- package/components/routes/hits/search/QuerySettings.js +2 -1
- package/components/routes/hits/search/QuerySettings.test.js +14 -9
- package/components/routes/hits/search/SearchPane.js +7 -32
- package/components/routes/hits/search/ViewLink.js +1 -1
- package/components/routes/hits/search/grid/EnhancedCell.js +1 -1
- package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
- package/components/routes/hits/search/shared/IndexPicker.js +20 -0
- package/components/routes/hits/view/HitViewer.js +3 -4
- package/components/routes/home/ViewCard.js +1 -1
- package/components/routes/observables/ObservableViewer.d.ts +7 -0
- package/components/routes/observables/ObservableViewer.js +27 -0
- package/components/routes/overviews/OverviewViewer.js +2 -2
- package/locales/en/translation.json +437 -398
- package/locales/fr/translation.json +442 -408
- package/models/WithMetadata.d.ts +2 -1
- package/models/entities/generated/AttachmentsFile.d.ts +12 -0
- package/models/entities/generated/Case.d.ts +28 -0
- package/models/entities/generated/DestinationOriginal.d.ts +19 -0
- package/models/entities/generated/EmailAttachment.d.ts +8 -0
- package/models/entities/generated/EmailParent.d.ts +19 -0
- package/models/entities/generated/Enrichments.d.ts +7 -0
- package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
- package/models/entities/generated/Howler.d.ts +0 -4
- package/models/entities/generated/HttpResponse.d.ts +11 -0
- package/models/entities/generated/Item.d.ts +9 -0
- package/models/entities/generated/Observable.d.ts +84 -0
- package/models/entities/generated/ObservableCloud.d.ts +20 -0
- package/models/entities/generated/ObservableDestination.d.ts +23 -0
- package/models/entities/generated/ObservableEmail.d.ts +30 -0
- package/models/entities/generated/ObservableFile.d.ts +36 -0
- package/models/entities/generated/ObservableHowler.d.ts +44 -0
- package/models/entities/generated/ObservableHttp.d.ts +11 -0
- package/models/entities/generated/ObservableObserver.d.ts +21 -0
- package/models/entities/generated/ObservableOrganization.d.ts +7 -0
- package/models/entities/generated/ObservableProcess.d.ts +34 -0
- package/models/entities/generated/ObservableSource.d.ts +23 -0
- package/models/entities/generated/ObservableThreat.d.ts +21 -0
- package/models/entities/generated/ObservableTls.d.ts +12 -0
- package/models/entities/generated/ObserverIngress.d.ts +9 -0
- package/models/entities/generated/Rule.d.ts +2 -10
- package/models/entities/generated/Task.d.ts +10 -0
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
- package/package.json +16 -1
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +2 -1
- package/utils/constants.d.ts +3 -3
- package/utils/typeUtils.d.ts +7 -0
- package/utils/typeUtils.js +18 -0
- package/components/elements/display/icons/BundleButton.d.ts +0 -6
- package/components/elements/display/icons/BundleButton.js +0 -32
- package/components/routes/help/BundleDocumentation.d.ts +0 -3
- package/components/routes/help/BundleDocumentation.js +0 -12
- package/components/routes/help/markdown/en/bundles.md.js +0 -1
- package/components/routes/help/markdown/fr/bundles.md.js +0 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
- package/components/routes/hits/search/BundleParentMenu.js +0 -32
- /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
- /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
|
@@ -272,17 +272,17 @@ describe('ParameterContext', () => {
|
|
|
272
272
|
});
|
|
273
273
|
});
|
|
274
274
|
});
|
|
275
|
-
describe('
|
|
275
|
+
describe('resetFilters', () => {
|
|
276
276
|
it('should clear all filters', async () => {
|
|
277
277
|
mockSearchParams = new URLSearchParams();
|
|
278
278
|
mockSearchParams.append('filter', 'filter1');
|
|
279
279
|
mockSearchParams.append('filter', 'filter2');
|
|
280
280
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
281
281
|
filters: ctx.filters,
|
|
282
|
-
|
|
282
|
+
resetFilters: ctx.resetFilters
|
|
283
283
|
})), { wrapper: Wrapper });
|
|
284
284
|
await act(async () => {
|
|
285
|
-
hook.result.current.
|
|
285
|
+
hook.result.current.resetFilters();
|
|
286
286
|
});
|
|
287
287
|
await waitFor(() => {
|
|
288
288
|
expect(hook.result.current.filters).toEqual([]);
|
|
@@ -291,10 +291,10 @@ describe('ParameterContext', () => {
|
|
|
291
291
|
it('should be no-op when already empty', async () => {
|
|
292
292
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
293
293
|
filters: ctx.filters,
|
|
294
|
-
|
|
294
|
+
resetFilters: ctx.resetFilters
|
|
295
295
|
})), { wrapper: Wrapper });
|
|
296
296
|
await act(async () => {
|
|
297
|
-
hook.result.current.
|
|
297
|
+
hook.result.current.resetFilters();
|
|
298
298
|
});
|
|
299
299
|
await waitFor(() => {
|
|
300
300
|
expect(hook.result.current.filters).toEqual([]);
|
|
@@ -441,10 +441,10 @@ describe('ParameterContext', () => {
|
|
|
441
441
|
mockSearchParams.append('filter', 'filter1');
|
|
442
442
|
mockSearchParams.append('filter', 'filter2');
|
|
443
443
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
444
|
-
|
|
444
|
+
resetFilters: ctx.resetFilters
|
|
445
445
|
})), { wrapper: Wrapper });
|
|
446
446
|
await act(async () => {
|
|
447
|
-
hook.result.current.
|
|
447
|
+
hook.result.current.resetFilters();
|
|
448
448
|
});
|
|
449
449
|
await waitFor(() => {
|
|
450
450
|
expect(mockSetParams).toHaveBeenCalled();
|
|
@@ -706,6 +706,299 @@ describe('ParameterContext', () => {
|
|
|
706
706
|
});
|
|
707
707
|
});
|
|
708
708
|
});
|
|
709
|
+
describe('indexes (multi-index support)', () => {
|
|
710
|
+
it('should initialize with default ["hit"] when no index params are present', async () => {
|
|
711
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
|
|
712
|
+
expect(hook.result.current).toEqual(['hit']);
|
|
713
|
+
});
|
|
714
|
+
it('should initialize with single index from URL', async () => {
|
|
715
|
+
mockSearchParams = new URLSearchParams({ index: 'observable' });
|
|
716
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
|
|
717
|
+
expect(hook.result.current).toEqual(['observable']);
|
|
718
|
+
});
|
|
719
|
+
it('should initialize with multiple indexes from URL', async () => {
|
|
720
|
+
mockSearchParams = new URLSearchParams();
|
|
721
|
+
mockSearchParams.append('index', 'hit');
|
|
722
|
+
mockSearchParams.append('index', 'observable');
|
|
723
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
|
|
724
|
+
expect(hook.result.current).toEqual(['hit', 'observable']);
|
|
725
|
+
});
|
|
726
|
+
it('should deduplicate repeated index values from URL', async () => {
|
|
727
|
+
mockSearchParams = new URLSearchParams();
|
|
728
|
+
mockSearchParams.append('index', 'hit');
|
|
729
|
+
mockSearchParams.append('index', 'hit');
|
|
730
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
|
|
731
|
+
expect(hook.result.current).toEqual(['hit']);
|
|
732
|
+
});
|
|
733
|
+
describe('addIndex', () => {
|
|
734
|
+
it('should add an index to the default array', async () => {
|
|
735
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
736
|
+
indexes: ctx.indexes,
|
|
737
|
+
addIndex: ctx.addIndex
|
|
738
|
+
})), { wrapper: Wrapper });
|
|
739
|
+
await act(async () => {
|
|
740
|
+
hook.result.current.addIndex('observable');
|
|
741
|
+
});
|
|
742
|
+
await waitFor(() => {
|
|
743
|
+
expect(hook.result.current.indexes).toEqual(['hit', 'observable']);
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
it('should not add a duplicate index', async () => {
|
|
747
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
748
|
+
indexes: ctx.indexes,
|
|
749
|
+
addIndex: ctx.addIndex
|
|
750
|
+
})), { wrapper: Wrapper });
|
|
751
|
+
await act(async () => {
|
|
752
|
+
hook.result.current.addIndex('hit');
|
|
753
|
+
});
|
|
754
|
+
await waitFor(() => {
|
|
755
|
+
expect(hook.result.current.indexes).toEqual(['hit']);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
describe('removeIndex', () => {
|
|
760
|
+
it('should remove an index from the list', async () => {
|
|
761
|
+
mockSearchParams = new URLSearchParams();
|
|
762
|
+
mockSearchParams.append('index', 'hit');
|
|
763
|
+
mockSearchParams.append('index', 'observable');
|
|
764
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
765
|
+
indexes: ctx.indexes,
|
|
766
|
+
removeIndex: ctx.removeIndex
|
|
767
|
+
})), { wrapper: Wrapper });
|
|
768
|
+
await act(async () => {
|
|
769
|
+
hook.result.current.removeIndex('hit');
|
|
770
|
+
});
|
|
771
|
+
await waitFor(() => {
|
|
772
|
+
expect(hook.result.current.indexes).toEqual(['observable']);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
it('should do nothing when removing a nonexistent index', async () => {
|
|
776
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
777
|
+
indexes: ctx.indexes,
|
|
778
|
+
removeIndex: ctx.removeIndex
|
|
779
|
+
})), { wrapper: Wrapper });
|
|
780
|
+
await act(async () => {
|
|
781
|
+
hook.result.current.removeIndex('observable');
|
|
782
|
+
});
|
|
783
|
+
await waitFor(() => {
|
|
784
|
+
expect(hook.result.current.indexes).toEqual(['hit']);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
it('should handle removing from empty array', async () => {
|
|
788
|
+
mockSearchParams = new URLSearchParams();
|
|
789
|
+
mockSearchParams.append('index', 'hit');
|
|
790
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
791
|
+
indexes: ctx.indexes,
|
|
792
|
+
removeIndex: ctx.removeIndex
|
|
793
|
+
})), { wrapper: Wrapper });
|
|
794
|
+
await act(async () => {
|
|
795
|
+
hook.result.current.removeIndex('hit');
|
|
796
|
+
});
|
|
797
|
+
await waitFor(() => {
|
|
798
|
+
expect(hook.result.current.indexes).toEqual([]);
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
describe('setIndex', () => {
|
|
803
|
+
it('should update the index at the specified position', async () => {
|
|
804
|
+
mockSearchParams = new URLSearchParams();
|
|
805
|
+
mockSearchParams.append('index', 'hit');
|
|
806
|
+
mockSearchParams.append('index', 'observable');
|
|
807
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
808
|
+
indexes: ctx.indexes,
|
|
809
|
+
setIndex: ctx.setIndex
|
|
810
|
+
})), { wrapper: Wrapper });
|
|
811
|
+
await act(async () => {
|
|
812
|
+
hook.result.current.setIndex(0, 'observable');
|
|
813
|
+
});
|
|
814
|
+
await waitFor(() => {
|
|
815
|
+
expect(hook.result.current.indexes).toEqual(['observable', 'observable']);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
it('should do nothing when index is out of bounds', async () => {
|
|
819
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
820
|
+
indexes: ctx.indexes,
|
|
821
|
+
setIndex: ctx.setIndex
|
|
822
|
+
})), { wrapper: Wrapper });
|
|
823
|
+
await act(async () => {
|
|
824
|
+
hook.result.current.setIndex(5, 'observable');
|
|
825
|
+
});
|
|
826
|
+
await waitFor(() => {
|
|
827
|
+
expect(hook.result.current.indexes).toEqual(['hit']);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
it('should do nothing when position is negative', async () => {
|
|
831
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
832
|
+
indexes: ctx.indexes,
|
|
833
|
+
setIndex: ctx.setIndex
|
|
834
|
+
})), { wrapper: Wrapper });
|
|
835
|
+
await act(async () => {
|
|
836
|
+
hook.result.current.setIndex(-1, 'observable');
|
|
837
|
+
});
|
|
838
|
+
await waitFor(() => {
|
|
839
|
+
expect(hook.result.current.indexes).toEqual(['hit']);
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
describe('setIndexes', () => {
|
|
844
|
+
it('should replace all indexes with the provided list', async () => {
|
|
845
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
846
|
+
indexes: ctx.indexes,
|
|
847
|
+
setIndexes: ctx.setIndexes
|
|
848
|
+
})), { wrapper: Wrapper });
|
|
849
|
+
await act(async () => {
|
|
850
|
+
hook.result.current.setIndexes(['observable']);
|
|
851
|
+
});
|
|
852
|
+
await waitFor(() => {
|
|
853
|
+
expect(hook.result.current.indexes).toEqual(['observable']);
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
it('should deduplicate values when setting all indexes', async () => {
|
|
857
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
858
|
+
indexes: ctx.indexes,
|
|
859
|
+
setIndexes: ctx.setIndexes
|
|
860
|
+
})), { wrapper: Wrapper });
|
|
861
|
+
await act(async () => {
|
|
862
|
+
hook.result.current.setIndexes(['hit', 'hit', 'observable']);
|
|
863
|
+
});
|
|
864
|
+
await waitFor(() => {
|
|
865
|
+
expect(hook.result.current.indexes).toEqual(['hit', 'observable']);
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
it('should set to empty array', async () => {
|
|
869
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
870
|
+
indexes: ctx.indexes,
|
|
871
|
+
setIndexes: ctx.setIndexes
|
|
872
|
+
})), { wrapper: Wrapper });
|
|
873
|
+
await act(async () => {
|
|
874
|
+
hook.result.current.setIndexes([]);
|
|
875
|
+
});
|
|
876
|
+
await waitFor(() => {
|
|
877
|
+
expect(hook.result.current.indexes).toEqual([]);
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
describe('resetIndexes', () => {
|
|
882
|
+
it('should reset indexes to default ["hit"]', async () => {
|
|
883
|
+
mockSearchParams = new URLSearchParams();
|
|
884
|
+
mockSearchParams.append('index', 'hit');
|
|
885
|
+
mockSearchParams.append('index', 'observable');
|
|
886
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
887
|
+
indexes: ctx.indexes,
|
|
888
|
+
resetIndexes: ctx.resetIndexes
|
|
889
|
+
})), { wrapper: Wrapper });
|
|
890
|
+
await act(async () => {
|
|
891
|
+
hook.result.current.resetIndexes();
|
|
892
|
+
});
|
|
893
|
+
await waitFor(() => {
|
|
894
|
+
expect(hook.result.current.indexes).toEqual(['hit']);
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
it('should reset to default even when called on empty array', async () => {
|
|
898
|
+
mockSearchParams = new URLSearchParams();
|
|
899
|
+
mockSearchParams.append('index', 'hit');
|
|
900
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
901
|
+
indexes: ctx.indexes,
|
|
902
|
+
removeIndex: ctx.removeIndex,
|
|
903
|
+
resetIndexes: ctx.resetIndexes
|
|
904
|
+
})), { wrapper: Wrapper });
|
|
905
|
+
// First empty it
|
|
906
|
+
await act(async () => {
|
|
907
|
+
hook.result.current.removeIndex('hit');
|
|
908
|
+
});
|
|
909
|
+
await waitFor(() => {
|
|
910
|
+
expect(hook.result.current.indexes).toEqual([]);
|
|
911
|
+
});
|
|
912
|
+
// Resetting always returns to default ['hit']
|
|
913
|
+
await act(async () => {
|
|
914
|
+
hook.result.current.resetIndexes();
|
|
915
|
+
});
|
|
916
|
+
await waitFor(() => {
|
|
917
|
+
expect(hook.result.current.indexes).toEqual(['hit']);
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
describe('URL synchronization', () => {
|
|
922
|
+
it('should not write the default ["hit"] index to the URL', async () => {
|
|
923
|
+
renderHook(() => useContextSelector(ParameterContext, ctx => ctx.indexes), { wrapper: Wrapper });
|
|
924
|
+
// Allow any effects to flush
|
|
925
|
+
await waitFor(() => {
|
|
926
|
+
// If setParams was called, the URL must not contain ?index=hit
|
|
927
|
+
if (mockSetParams.mock.calls.length > 0) {
|
|
928
|
+
const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
|
|
929
|
+
const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
|
|
930
|
+
expect(urlParams.getAll('index')).toEqual([]);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
// setParams not called at all is also fine
|
|
934
|
+
expect(true).toBe(true);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
it('should write a non-default index to the URL', async () => {
|
|
939
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
940
|
+
addIndex: ctx.addIndex,
|
|
941
|
+
resetIndexes: ctx.resetIndexes
|
|
942
|
+
})), { wrapper: Wrapper });
|
|
943
|
+
await act(async () => {
|
|
944
|
+
hook.result.current.resetIndexes();
|
|
945
|
+
hook.result.current.addIndex('observable');
|
|
946
|
+
});
|
|
947
|
+
await waitFor(() => {
|
|
948
|
+
expect(mockSetParams).toHaveBeenCalled();
|
|
949
|
+
const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
|
|
950
|
+
const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
|
|
951
|
+
expect(urlParams.getAll('index')).toEqual(['hit', 'observable']);
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
it('should sync multiple indexes to URL as multiple index params', async () => {
|
|
955
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
956
|
+
addIndex: ctx.addIndex
|
|
957
|
+
})), { wrapper: Wrapper });
|
|
958
|
+
await act(async () => {
|
|
959
|
+
hook.result.current.addIndex('observable');
|
|
960
|
+
});
|
|
961
|
+
await waitFor(() => {
|
|
962
|
+
expect(mockSetParams).toHaveBeenCalled();
|
|
963
|
+
const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
|
|
964
|
+
const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
|
|
965
|
+
expect(urlParams.getAll('index')).toEqual(['hit', 'observable']);
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
it('should remove all index params from URL when state resets to default', async () => {
|
|
969
|
+
mockSearchParams = new URLSearchParams();
|
|
970
|
+
mockSearchParams.append('index', 'hit');
|
|
971
|
+
mockSearchParams.append('index', 'observable');
|
|
972
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
973
|
+
resetIndexes: ctx.resetIndexes
|
|
974
|
+
})), { wrapper: Wrapper });
|
|
975
|
+
await act(async () => {
|
|
976
|
+
hook.result.current.resetIndexes();
|
|
977
|
+
});
|
|
978
|
+
await waitFor(() => {
|
|
979
|
+
expect(mockSetParams).toHaveBeenCalled();
|
|
980
|
+
const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
|
|
981
|
+
const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
|
|
982
|
+
expect(urlParams.getAll('index')).toEqual([]);
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
it('should remove index param from URL when state returns to default', async () => {
|
|
986
|
+
mockSearchParams = new URLSearchParams({ index: 'observable' });
|
|
987
|
+
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
988
|
+
setIndexes: ctx.setIndexes
|
|
989
|
+
})), { wrapper: Wrapper });
|
|
990
|
+
await act(async () => {
|
|
991
|
+
hook.result.current.setIndexes(['hit']);
|
|
992
|
+
});
|
|
993
|
+
await waitFor(() => {
|
|
994
|
+
expect(mockSetParams).toHaveBeenCalled();
|
|
995
|
+
const call = mockSetParams.mock.calls[mockSetParams.mock.calls.length - 1];
|
|
996
|
+
const urlParams = typeof call[0] === 'function' ? call[0](mockSearchParams) : call[0];
|
|
997
|
+
expect(urlParams.getAll('index')).toEqual([]);
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
709
1002
|
describe('views (multi-view support)', () => {
|
|
710
1003
|
it('should initialize with empty array when no view params present', async () => {
|
|
711
1004
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ctx.views), { wrapper: Wrapper });
|
|
@@ -839,17 +1132,17 @@ describe('ParameterContext', () => {
|
|
|
839
1132
|
});
|
|
840
1133
|
});
|
|
841
1134
|
});
|
|
842
|
-
describe('
|
|
1135
|
+
describe('resetViews', () => {
|
|
843
1136
|
it('should clear all views', async () => {
|
|
844
1137
|
mockSearchParams = new URLSearchParams();
|
|
845
1138
|
mockSearchParams.append('view', 'view_1');
|
|
846
1139
|
mockSearchParams.append('view', 'view_2');
|
|
847
1140
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
848
1141
|
views: ctx.views,
|
|
849
|
-
|
|
1142
|
+
resetViews: ctx.resetViews
|
|
850
1143
|
})), { wrapper: Wrapper });
|
|
851
1144
|
await act(async () => {
|
|
852
|
-
hook.result.current.
|
|
1145
|
+
hook.result.current.resetViews();
|
|
853
1146
|
});
|
|
854
1147
|
await waitFor(() => {
|
|
855
1148
|
expect(hook.result.current.views).toEqual([]);
|
|
@@ -858,10 +1151,10 @@ describe('ParameterContext', () => {
|
|
|
858
1151
|
it('should be no-op when already empty', async () => {
|
|
859
1152
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
860
1153
|
views: ctx.views,
|
|
861
|
-
|
|
1154
|
+
resetViews: ctx.resetViews
|
|
862
1155
|
})), { wrapper: Wrapper });
|
|
863
1156
|
await act(async () => {
|
|
864
|
-
hook.result.current.
|
|
1157
|
+
hook.result.current.resetViews();
|
|
865
1158
|
});
|
|
866
1159
|
await waitFor(() => {
|
|
867
1160
|
expect(hook.result.current.views).toEqual([]);
|
|
@@ -1008,10 +1301,10 @@ describe('ParameterContext', () => {
|
|
|
1008
1301
|
mockSearchParams.append('view', 'view_1');
|
|
1009
1302
|
mockSearchParams.append('view', 'view_2');
|
|
1010
1303
|
const hook = renderHook(() => useContextSelector(ParameterContext, ctx => ({
|
|
1011
|
-
|
|
1304
|
+
resetViews: ctx.resetViews
|
|
1012
1305
|
})), { wrapper: Wrapper });
|
|
1013
1306
|
await act(async () => {
|
|
1014
|
-
hook.result.current.
|
|
1307
|
+
hook.result.current.resetViews();
|
|
1015
1308
|
});
|
|
1016
1309
|
await waitFor(() => {
|
|
1017
1310
|
expect(mockSetParams).toHaveBeenCalled();
|
|
@@ -4,8 +4,8 @@ import { useTheme } from '@mui/material';
|
|
|
4
4
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
5
|
import ThemedEditor from '@cccsaurora/howler-ui/components/elements/ThemedEditor';
|
|
6
6
|
import { memo, useCallback, useContext, useEffect, useMemo } from 'react';
|
|
7
|
-
import { conf, language } from '
|
|
8
|
-
const
|
|
7
|
+
import { conf, language } from '../routes/overviews/markdownExtendedTokenProvider';
|
|
8
|
+
const MarkdownEditor = ({ content, setContent, onMount, fontSize = 16, height = '100%', width = '100%', editorOptions = {} }) => {
|
|
9
9
|
const theme = useTheme();
|
|
10
10
|
const monaco = useMonaco();
|
|
11
11
|
const { config } = useContext(ApiConfigContext);
|
|
@@ -53,4 +53,4 @@ const OverviewEditor = ({ content, setContent, onMount, fontSize = 16, height =
|
|
|
53
53
|
}), [fontSize, editorOptions]);
|
|
54
54
|
return (_jsx(ThemedEditor, { height: height, width: width, theme: theme.palette.mode === 'light' ? 'howler' : 'howler-dark', value: content, onChange: value => setContent(value), beforeMount: beforeEditorMount, onMount: onMount, options: options }));
|
|
55
55
|
};
|
|
56
|
-
export default memo(
|
|
56
|
+
export default memo(MarkdownEditor);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
2
|
+
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
|
+
declare const _default: import("react").NamedExoticComponent<{
|
|
4
|
+
obj: Hit | Observable;
|
|
5
|
+
}>;
|
|
6
|
+
export default _default;
|
|
@@ -5,12 +5,12 @@ import { ArrowDropDown, InfoOutlined } from '@mui/icons-material';
|
|
|
5
5
|
import { Accordion, AccordionDetails, AccordionSummary, Box, Divider, Grid, Stack, TextField, Tooltip, Typography, useTheme } from '@mui/material';
|
|
6
6
|
import { flatten } from 'flat';
|
|
7
7
|
import Fuse from 'fuse.js';
|
|
8
|
-
import { capitalize, groupBy, isArray, isEmpty, isNull, isObject, isPlainObject, isUndefined, max, sortBy, uniq } from 'lodash-es';
|
|
8
|
+
import { capitalize, groupBy, isArray, isBoolean, isEmpty, isNull, isNumber, isObject, isPlainObject, isUndefined, max, sortBy, uniq } from 'lodash-es';
|
|
9
9
|
import { memo, useEffect, useMemo, useState } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
|
11
11
|
import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
|
|
12
|
-
import PluginTypography from '
|
|
13
|
-
const ListRenderer = memo(({
|
|
12
|
+
import PluginTypography from './PluginTypography';
|
|
13
|
+
const ListRenderer = memo(({ obj, objKey: key, entries, maxKeyLength }) => {
|
|
14
14
|
const theme = useTheme();
|
|
15
15
|
const { t } = useTranslation();
|
|
16
16
|
const allPrimitives = useMemo(() => entries.every(entry => !isObject(entry)), [entries]);
|
|
@@ -36,15 +36,15 @@ const ListRenderer = memo(({ hit, objKey: key, entries, maxKeyLength }) => {
|
|
|
36
36
|
marginBottom: allPrimitives ? 0 : theme.spacing(1)
|
|
37
37
|
}, children: allPrimitives ? key.padStart(maxKeyLength ?? key.length) : key }) }), _jsxs(Grid, { container: true, spacing: allPrimitives ? 1 : 4, ml: allPrimitives ? -1 : -4, overflow: "hidden", maxWidth: "100%", children: [uniqueEntries.map((entry, index) => {
|
|
38
38
|
if (Array.isArray(entry)) {
|
|
39
|
-
return (_jsx(Grid, { item: true, xs: "auto", maxWidth: "100%", children: _jsx(ListRenderer, {
|
|
39
|
+
return (_jsx(Grid, { item: true, xs: "auto", maxWidth: "100%", children: _jsx(ListRenderer, { obj: obj, objKey: `${key}.${index}`, entries: entry }) }, index));
|
|
40
40
|
}
|
|
41
41
|
if (isPlainObject(entry)) {
|
|
42
42
|
return (_jsx(Grid, { item: true, xs: 'auto', maxWidth: "100%", minWidth: "350px", children: _jsx(ObjectRenderer, { parentKey: `${key}.${index}`, indent: true, data: entry }) }, index));
|
|
43
43
|
}
|
|
44
|
-
return (_jsxs(Grid, { item: true, maxWidth: "100%", className: `${key}_${index}`.replace(/\./g, '_'), component: "code", display: "flex", flexDirection: "row", children: [_jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: entry, field: key.replace(/\.[0-9]+/g, ''),
|
|
44
|
+
return (_jsxs(Grid, { item: true, maxWidth: "100%", className: `${key}_${index}`.replace(/\./g, '_'), component: "code", display: "flex", flexDirection: "row", children: [_jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: entry, field: key.replace(/\.[0-9]+/g, ''), obj: obj, children: entry }), allPrimitives && index < uniqueEntries.length - 1 && _jsx("span", { children: "," })] }, entry));
|
|
45
45
|
}), omittedDuplicates && (_jsx(Grid, { item: true, display: "flex", alignItems: "center", children: _jsx(Tooltip, { title: t('duplicates.omitted'), children: _jsx(InfoOutlined, { sx: { fontSize: '20px', ml: 1 }, color: "disabled" }) }) }))] })] }));
|
|
46
46
|
});
|
|
47
|
-
const ObjectRenderer = memo(({
|
|
47
|
+
const ObjectRenderer = memo(({ obj: obj, data, parentKey, indent = false }) => {
|
|
48
48
|
const theme = useTheme();
|
|
49
49
|
const entries = useMemo(() => {
|
|
50
50
|
const unsorted = Object.entries(flatten(data, { safe: true })).map(([key, val]) => [key, val]);
|
|
@@ -56,10 +56,10 @@ const ObjectRenderer = memo(({ hit, data, parentKey, indent = false }) => {
|
|
|
56
56
|
}, [data]);
|
|
57
57
|
const longestKey = useMemo(() => max(entries.map(([key]) => key.length)), [entries]);
|
|
58
58
|
return (_jsxs(Stack, { direction: "row", overflow: "hidden", maxWidth: "100%", children: [indent && _jsx(Divider, { orientation: "vertical", flexItem: true, sx: { borderColor: 'primary.main', borderWidth: '2px' } }), _jsx(Stack, { flex: 1, ml: 1, maxWidth: "100%", children: entries
|
|
59
|
-
.filter(([__, val]) => !isNull(val) && !isUndefined(val) && !isEmpty(val))
|
|
59
|
+
.filter(([__, val]) => !isNull(val) && !isUndefined(val) && !isEmpty(val) && !isBoolean(val) && !isNumber(val))
|
|
60
60
|
.map(([key, val]) => {
|
|
61
61
|
if (Array.isArray(val)) {
|
|
62
|
-
return _jsx(ListRenderer, {
|
|
62
|
+
return _jsx(ListRenderer, { obj: obj, maxKeyLength: longestKey, objKey: key, entries: val }, key);
|
|
63
63
|
}
|
|
64
64
|
return (_jsxs("code", { className: (parentKey ? `${parentKey}.${key}` : key).replace(/\./g, '_'), style: {
|
|
65
65
|
display: 'grid',
|
|
@@ -75,14 +75,14 @@ const ObjectRenderer = memo(({ hit, data, parentKey, indent = false }) => {
|
|
|
75
75
|
paddingRight: theme.spacing(1),
|
|
76
76
|
height: '100%',
|
|
77
77
|
wordWrap: 'break-word'
|
|
78
|
-
}, children: _jsx("code", { style: { maxWidth: '100%' }, children: key }) }), _jsx(Box, { display: "flex", alignItems: "start", children: _jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: val, field: (parentKey ? parentKey.concat('.', key) : key).replace(/\.[0-9]+/g, ''),
|
|
78
|
+
}, children: _jsx("code", { style: { maxWidth: '100%' }, children: key }) }), _jsx(Box, { display: "flex", alignItems: "start", children: _jsx(PluginTypography, { context: "details", component: "code", style: { maxWidth: '100%', font: 'inherit' }, value: val, field: (parentKey ? parentKey.concat('.', key) : key).replace(/\.[0-9]+/g, ''), obj: obj, children: val }) })] }, key));
|
|
79
79
|
}) })] }));
|
|
80
80
|
});
|
|
81
|
-
const Collapsible = memo(({
|
|
81
|
+
const Collapsible = memo(({ obj, title, data, query }) => {
|
|
82
82
|
const throttler = useMemo(() => new Throttler(400), []);
|
|
83
83
|
const [scores, setScores] = useState([]);
|
|
84
84
|
const [results, setResults] = useState({});
|
|
85
|
-
const options = useMemo(() => Object.entries(data).map(([key, value]) => ({ key, value })), [data]);
|
|
85
|
+
const options = useMemo(() => Object.entries(data).map(([key, value]) => ({ key, value: value.toString() })), [data]);
|
|
86
86
|
const keys = useMemo(() => options
|
|
87
87
|
.flatMap(option => (isArray(option.value) ? Object.keys(flatten(option.value)) : []))
|
|
88
88
|
.map(key => key.replace(/\d+/g, 'value'))
|
|
@@ -109,20 +109,20 @@ const Collapsible = memo(({ hit, title, data, query }) => {
|
|
|
109
109
|
if (isEmpty(results)) {
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
|
-
return (_jsxs(Accordion, { defaultExpanded: true, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ArrowDropDown, {}), children: _jsx(Typography, { children: title }) }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing:
|
|
112
|
+
return (_jsxs(Accordion, { defaultExpanded: true, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ArrowDropDown, {}), sx: { my: 0 }, children: _jsx(Typography, { children: title }) }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing: 0.5, justifyContent: "stretch", sx: styles, children: _jsx(ObjectRenderer, { obj: obj, showParentKey: true, data: results }) }) })] }));
|
|
113
113
|
});
|
|
114
|
-
const
|
|
114
|
+
const ObjectDetails = ({ obj }) => {
|
|
115
115
|
const { t } = useTranslation();
|
|
116
116
|
const [query, setQuery] = useState('');
|
|
117
|
-
const groups = useMemo(() => groupBy(Object.entries(flatten(
|
|
117
|
+
const groups = useMemo(() => groupBy(Object.entries(flatten(obj ?? {}, { safe: true })).filter(([key, value]) => !key.startsWith('__') &&
|
|
118
118
|
key.includes('.') &&
|
|
119
119
|
['howler', 'labels'].every(prefix => !key.startsWith(prefix)) &&
|
|
120
|
-
!isEmpty(value)), ([key]) => key.split('.')[0]), [
|
|
120
|
+
(!isEmpty(value) || isNumber(value) || isBoolean(value))), ([key]) => key.split('.')[0]), [obj]);
|
|
121
121
|
return (_jsxs(Stack, { spacing: 1, children: [_jsx(TextField, { value: query, onChange: event => setQuery(event.target.value), label: t('overview.search') }), Object.entries(groups).map(([section, entries]) => {
|
|
122
|
-
return (_jsx(Collapsible, {
|
|
122
|
+
return (_jsx(Collapsible, { obj: obj, query: query, title: section
|
|
123
123
|
.split('_')
|
|
124
124
|
.map(word => capitalize(word))
|
|
125
125
|
.join(' '), data: Object.fromEntries(entries) }, section));
|
|
126
126
|
})] }));
|
|
127
127
|
};
|
|
128
|
-
export default memo(
|
|
128
|
+
export default memo(ObjectDetails);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type TypographyProps } from '@mui/material';
|
|
2
2
|
import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
|
|
3
|
+
import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
|
|
3
4
|
export type PluginTypographyProps = TypographyProps & {
|
|
4
5
|
value: string;
|
|
5
6
|
context: string;
|
|
6
7
|
field?: string;
|
|
7
|
-
|
|
8
|
+
obj?: Hit | Observable;
|
|
8
9
|
};
|
|
9
10
|
declare const _default: import("react").NamedExoticComponent<PluginTypographyProps>;
|
|
10
11
|
export default _default;
|
|
@@ -3,7 +3,7 @@ import { Typography } from '@mui/material';
|
|
|
3
3
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
4
4
|
import { memo } from 'react';
|
|
5
5
|
import { usePluginStore } from 'react-pluggable';
|
|
6
|
-
const PluginTypography = ({ children, value, context, field,
|
|
6
|
+
const PluginTypography = ({ children, value, context, field, obj, ...props }) => {
|
|
7
7
|
const pluginStore = usePluginStore();
|
|
8
8
|
for (const plugin of howlerPluginStore.plugins) {
|
|
9
9
|
const component = pluginStore.executeFunction(`${plugin}.typography`, {
|
|
@@ -11,7 +11,8 @@ const PluginTypography = ({ children, value, context, field, hit, ...props }) =>
|
|
|
11
11
|
value,
|
|
12
12
|
context,
|
|
13
13
|
field,
|
|
14
|
-
hit,
|
|
14
|
+
hit: obj,
|
|
15
|
+
obj,
|
|
15
16
|
...props
|
|
16
17
|
});
|
|
17
18
|
if (component) {
|
|
@@ -2,8 +2,11 @@ import type { SxProps, Theme } from '@mui/material';
|
|
|
2
2
|
import type { FC } from 'react';
|
|
3
3
|
declare const UserList: FC<{
|
|
4
4
|
buttonSx?: SxProps<Theme>;
|
|
5
|
-
|
|
6
|
-
onChange: (
|
|
5
|
+
userIds: string[];
|
|
6
|
+
onChange: (userIds: string[]) => void;
|
|
7
7
|
i18nLabel: string;
|
|
8
|
+
avatarHeight?: number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
multiple?: boolean;
|
|
8
11
|
}>;
|
|
9
12
|
export default UserList;
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Add } from '@mui/icons-material';
|
|
3
|
+
import { Autocomplete, AvatarGroup, Box, IconButton, Popover, Stack, TextField, Typography } from '@mui/material';
|
|
3
4
|
import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
5
|
+
import { uniq } from 'lodash-es';
|
|
4
6
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
5
7
|
import { useTranslation } from 'react-i18next';
|
|
6
8
|
import HowlerAvatar from './display/HowlerAvatar';
|
|
7
|
-
const UserList = ({ buttonSx = {},
|
|
9
|
+
const UserList = ({ buttonSx = {}, userIds, onChange, i18nLabel, avatarHeight = 32, multiple = false, disabled = false }) => {
|
|
8
10
|
const { t } = useTranslation();
|
|
9
11
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
10
12
|
const { users, searchUsers } = useContext(UserListContext);
|
|
11
|
-
const
|
|
13
|
+
const allUserIds = useMemo(() => Object.keys(users), [users]);
|
|
12
14
|
useEffect(() => {
|
|
13
15
|
searchUsers('uname:*');
|
|
14
16
|
}, [searchUsers]);
|
|
15
|
-
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { sx: buttonSx, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(HowlerAvatar, { userId:
|
|
17
|
+
return (_jsxs(_Fragment, { children: [multiple ? (_jsxs(Stack, { direction: "row", spacing: 0.25, alignItems: "center", children: [_jsx(AvatarGroup, { children: uniq(userIds ?? [null]).map(userId => (_jsx(HowlerAvatar, { userId: userId, sx: { height: avatarHeight, width: avatarHeight } }, userId))) }), _jsx(IconButton, { size: "small", sx: buttonSx, disabled: disabled, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(Add, {}) })] })) : (_jsx(IconButton, { sx: buttonSx, disabled: disabled, onClick: e => setAnchorEl(e.currentTarget), children: _jsx(HowlerAvatar, { userId: userIds[0], sx: { height: avatarHeight, width: avatarHeight } }) })), _jsx(Popover, { open: !!anchorEl, onClose: () => setAnchorEl(null), anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, children: _jsx(Box, { sx: { p: 2 }, children: _jsx(Autocomplete, { disabled: disabled, multiple: multiple, sx: { minWidth: '300px' }, options: allUserIds, renderInput: params => _jsx(TextField, { ...params, label: t(i18nLabel), size: "small" }), renderOption: (props, _userId) => {
|
|
16
18
|
const user = users[_userId];
|
|
17
19
|
return (_jsx("li", { ...props, children: _jsxs(Box, { sx: {
|
|
18
20
|
display: 'grid',
|
|
@@ -21,6 +23,13 @@ const UserList = ({ buttonSx = {}, userId, onChange, i18nLabel }) => {
|
|
|
21
23
|
gridTemplateAreas: `"profile name"\n"profile email"`,
|
|
22
24
|
columnGap: 1.5
|
|
23
25
|
}, children: [_jsx(HowlerAvatar, { sx: { gridArea: 'profile', alignSelf: 'center', height: '32px', width: '32px' }, userId: user.username }), _jsx(Typography, { sx: { gridArea: 'name' }, variant: "body1", children: user.name }), _jsx(Typography, { sx: { gridArea: 'email' }, variant: "caption", children: user.email })] }) }));
|
|
24
|
-
}, value:
|
|
26
|
+
}, value: userIds, onChange: (__, options) => {
|
|
27
|
+
if (multiple) {
|
|
28
|
+
onChange(Array.isArray(options) ? options : [options]);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
onChange([Array.isArray(options) ? (options[0] ?? null) : options]);
|
|
32
|
+
}
|
|
33
|
+
} }) }) })] }));
|
|
25
34
|
};
|
|
26
35
|
export default UserList;
|
|
@@ -80,7 +80,7 @@ const Phrase = ({ value = '', variant = 'outlined', suggestions = [], lexer, sug
|
|
|
80
80
|
onSelectCapture: _onSelectCapture,
|
|
81
81
|
startAdornment: startAdornment && _jsx(InputAdornment, { position: "start", children: startAdornment }),
|
|
82
82
|
endAdornment: endAdornment && _jsx(InputAdornment, { position: "end", children: endAdornment })
|
|
83
|
-
} }), _jsx(Popper, { anchorEl: containerRef.current, style: { width: '100%', zIndex: 100 }, open: optionsOpen && (options.length > 0 || (debug && analysisRef.current?.tokens.length > 0)), disablePortal: true, children: _jsx(Paper, { elevation:
|
|
83
|
+
} }), _jsx(Popper, { anchorEl: containerRef.current, style: { width: '100%', zIndex: 100 }, open: optionsOpen && (options.length > 0 || (debug && analysisRef.current?.tokens.length > 0)), disablePortal: true, children: _jsx(Paper, { elevation: 1, sx: { maxHeight: 200, overflow: 'auto', borderTopRightRadius: 0, borderTopLeftRadius: 0 }, children: _jsx(MenuList, { ref: menuRef, onKeyDown: _onMenuKeyDown, sx: {
|
|
84
84
|
'&:focus': {
|
|
85
85
|
outline: 'none'
|
|
86
86
|
}
|