@cccsaurora/howler-ui 2.17.0-dev.420 → 2.17.0-dev.470

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/commons/components/app/hooks/useAppConfigs.d.ts +1 -1
  2. package/components/app/providers/HitSearchProvider.d.ts +0 -1
  3. package/components/app/providers/HitSearchProvider.js +4 -6
  4. package/components/app/providers/HitSearchProvider.test.js +1 -1
  5. package/components/app/providers/ParameterProvider.js +3 -3
  6. package/components/app/providers/ViewProvider.d.ts +1 -1
  7. package/components/app/providers/ViewProvider.js +3 -6
  8. package/components/app/providers/ViewProvider.test.js +1 -1
  9. package/components/elements/PluginChip.d.ts +2 -0
  10. package/components/elements/PluginChip.js +2 -1
  11. package/components/elements/PluginTypography.d.ts +4 -3
  12. package/components/elements/PluginTypography.js +4 -3
  13. package/components/elements/display/modals/RationaleModal.js +1 -1
  14. package/components/elements/display/modals/RationaleModal.test.js +1 -1
  15. package/components/elements/hit/HitBanner.js +2 -2
  16. package/components/elements/hit/HitDetails.js +9 -9
  17. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  18. package/components/routes/hits/search/ViewLink.js +1 -1
  19. package/components/routes/hits/search/ViewLink.test.js +3 -3
  20. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -0
  21. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  22. package/components/routes/hits/search/grid/HitRow.js +1 -1
  23. package/components/routes/views/ViewComposer.js +2 -2
  24. package/index.js +5 -0
  25. package/locales/en/translation.json +1 -0
  26. package/locales/fr/translation.json +1 -0
  27. package/models/entities/generated/Analytic.d.ts +2 -2
  28. package/models/entities/generated/ApiType.d.ts +2 -1
  29. package/models/entities/generated/Clue.d.ts +8 -0
  30. package/models/entities/generated/Hit.d.ts +2 -20
  31. package/models/entities/generated/Labels.d.ts +1 -0
  32. package/models/entities/generated/Type.d.ts +7 -0
  33. package/package.json +24 -15
  34. package/plugins/HowlerPlugin.js +1 -0
  35. package/plugins/clue/Provider.d.ts +3 -0
  36. package/plugins/clue/Provider.js +13 -0
  37. package/plugins/clue/components/ClueChip.d.ts +3 -0
  38. package/plugins/clue/components/ClueChip.js +29 -0
  39. package/plugins/clue/components/ClueLeadForm.d.ts +4 -0
  40. package/plugins/clue/components/ClueLeadForm.js +24 -0
  41. package/plugins/clue/components/CluePivot.d.ts +3 -0
  42. package/plugins/clue/components/CluePivot.js +145 -0
  43. package/plugins/clue/components/CluePivotForm.d.ts +21 -0
  44. package/plugins/clue/components/CluePivotForm.js +270 -0
  45. package/plugins/clue/components/ClueTypography.d.ts +3 -0
  46. package/plugins/clue/components/ClueTypography.js +53 -0
  47. package/plugins/clue/helpers.d.ts +3 -0
  48. package/plugins/clue/helpers.js +196 -0
  49. package/plugins/clue/index.d.ts +21 -0
  50. package/plugins/clue/index.js +66 -0
  51. package/plugins/clue/locales/clue.en.json +8 -0
  52. package/plugins/clue/locales/clue.fr.json +8 -0
  53. package/plugins/clue/setup.d.ts +2 -0
  54. package/plugins/clue/setup.js +46 -0
  55. package/plugins/clue/utils.d.ts +2 -0
  56. package/plugins/clue/utils.js +19 -0
  57. package/plugins/store.js +3 -0
package/package.json CHANGED
@@ -12,6 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "dependencies": {
15
+ "@cccsaurora/clue-ui": "1.1.0-dev.91",
15
16
  "@dnd-kit/core": "^6.3.1",
16
17
  "@dnd-kit/modifiers": "^7.0.0",
17
18
  "@dnd-kit/sortable": "^8.0.0",
@@ -20,46 +21,50 @@
20
21
  "@iconify/icons-logos": "^1.2.36",
21
22
  "@iconify/icons-simple-icons": "^1.2.74",
22
23
  "@iconify/react": "^4.1.1",
23
- "@microlink/react-json-view": "^1.26.2",
24
+ "@jsonforms/core": "^3.7.0",
25
+ "@jsonforms/material-renderers": "^3.7.0",
26
+ "@jsonforms/react": "^3.7.0",
27
+ "@microlink/react-json-view": "^1.27.1",
24
28
  "@monaco-editor/react": "^4.7.0",
25
29
  "ajv": "^8.17.1",
26
30
  "ajv-i18n": "^4.2.0",
27
- "axios": "^1.12.0",
31
+ "axios": "^1.13.4",
28
32
  "axios-retry": "^3.9.1",
29
- "chart.js": "^4.5.0",
33
+ "chart.js": "^4.5.1",
30
34
  "chartjs-adapter-dayjs-4": "^1.0.4",
31
35
  "chartjs-plugin-zoom": "^2.2.0",
32
- "dayjs": "^1.11.13",
33
- "dompurify": "^3.2.6",
36
+ "dayjs": "^1.11.19",
37
+ "dompurify": "^3.3.1",
34
38
  "flat": "^6.0.1",
35
39
  "fuse.js": "^7.1.0",
36
40
  "handlebars": "^4.7.8",
37
41
  "handlebars-async-helpers": "^1.0.6",
38
42
  "i18next": "^23.16.8",
39
43
  "i18next-browser-languagedetector": "^7.2.2",
44
+ "json-schema": "^0.4.0",
40
45
  "lodash-es": "^4.17.23",
41
46
  "md5": "^2.3.0",
42
- "mermaid": "^11.10.0",
47
+ "mermaid": "^11.12.2",
43
48
  "monaco-editor": "^0.49.0",
44
49
  "notistack": "^3.0.2",
45
50
  "react": "^18.3.1",
46
- "react-chartjs-2": "^5.3.0",
51
+ "react-chartjs-2": "^5.3.1",
47
52
  "react-device-detect": "^2.2.3",
48
53
  "react-dom": "^18.3.1",
49
54
  "react-i18next": "^14.1.3",
50
- "react-ipynb-renderer": "^2.2.4",
55
+ "react-ipynb-renderer": "^2.3.0",
51
56
  "react-markdown": "^10.1.0",
52
57
  "react-pluggable": "^0.4.3",
53
58
  "react-resize-detector": "^9.1.1",
54
- "react-router": "^7.13.0",
55
- "react-router-dom": "^6.30.1",
56
- "react-syntax-highlighter": "^15.6.1",
59
+ "react-router": "6.30.1",
60
+ "react-router-dom": "6.30.1",
61
+ "react-syntax-highlighter": "^15.6.6",
57
62
  "rehype-raw": "^7.0.0",
58
63
  "remark": "^15.0.1",
59
64
  "remark-gfm": "^4.0.1",
60
- "scheduler": "^0.23.2",
65
+ "scheduler": "^0.27.0",
61
66
  "unified": "^11.0.5",
62
- "unist-util-visit": "^5.0.0",
67
+ "unist-util-visit": "^5.1.0",
63
68
  "url-join": "^5.0.0",
64
69
  "use-context-selector": "^2.0.0",
65
70
  "uuid": "^9.0.1",
@@ -96,7 +101,7 @@
96
101
  "internal-slot": "1.0.7"
97
102
  },
98
103
  "type": "module",
99
- "version": "2.17.0-dev.420",
104
+ "version": "2.17.0-dev.470",
100
105
  "exports": {
101
106
  "./i18n": "./i18n.js",
102
107
  "./index.css": "./index.css",
@@ -251,6 +256,10 @@
251
256
  "./commons/components/app/providers/*": "./commons/components/app/providers/*.js",
252
257
  "./commons/components/display/hooks/*": "./commons/components/display/hooks/*.js",
253
258
  "./commons/components/notification/elements/*": "./commons/components/notification/elements/*.js",
254
- "./commons/components/notification/elements/item/*": "./commons/components/notification/elements/item/*.js"
259
+ "./commons/components/notification/elements/item/*": "./commons/components/notification/elements/item/*.js",
260
+ "./plugins/clue/*": "./plugins/clue/*.js",
261
+ "./plugins/clue": "./plugins/clue/index.js",
262
+ "./plugins/clue/locales/*": "./plugins/clue/locales/*.js",
263
+ "./plugins/clue/components/*": "./plugins/clue/components/*.js"
255
264
  }
256
265
  }
@@ -133,6 +133,7 @@ class HowlerPlugin {
133
133
  if (isRoot) {
134
134
  if (breadcrumbs != null) {
135
135
  breadcrumbs = null;
136
+ // eslint-disable-next-line no-console
136
137
  console.warn(`Sitemap '${path}' with isRoot should not contain breadcrumbs and have been removed`);
137
138
  }
138
139
  }
@@ -0,0 +1,3 @@
1
+ import { type PropsWithChildren } from 'react';
2
+ declare const Provider: React.FC<PropsWithChildren<{}>>;
3
+ export default Provider;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ClueProvider } from '@cccsaurora/clue-ui/hooks/ClueProvider';
3
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
4
+ import { useContext } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
7
+ import { getStored } from '@cccsaurora/howler-ui/utils/localStorage';
8
+ const Provider = ({ children }) => {
9
+ const { config } = useContext(ApiConfigContext);
10
+ const features = config.configuration?.features ?? {};
11
+ return (_jsx(ClueProvider, { baseURL: location.origin + '/api/v1/clue', getToken: () => getStored(StorageKey.APP_TOKEN), enabled: features.clue, publicIconify: location.origin.includes('localhost'), customIconify: location.origin.replace(/howler(-stg)?/, 'icons'), defaultTimeout: 5, i18next: useTranslation('clue'), chunkSize: 50, children: children }));
12
+ };
13
+ export default Provider;
@@ -0,0 +1,3 @@
1
+ import type { PluginChipProps } from '@cccsaurora/howler-ui/components/elements/PluginChip';
2
+ declare const _default: import("react").NamedExoticComponent<PluginChipProps>;
3
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import EnrichedChip from '@cccsaurora/clue-ui/components/EnrichedChip';
3
+ import { Chip } from '@mui/material';
4
+ import { memo } from 'react';
5
+ import { useType } from '../utils';
6
+ const ClueChip = ({ children, value, context, field, hit, ...props }) => {
7
+ const type = useType(hit, field, value);
8
+ if (!type) {
9
+ return _jsx(Chip, { ...props, children: children });
10
+ }
11
+ let enrichedProps = {
12
+ ...props,
13
+ value
14
+ };
15
+ if (context === 'summary') {
16
+ enrichedProps = {
17
+ ...enrichedProps,
18
+ sx: [
19
+ ...(Array.isArray(enrichedProps.sx) ? enrichedProps.sx : [enrichedProps.sx]),
20
+ [{ height: '24px', '& .iconify': { fontSize: '1em' } }]
21
+ ]
22
+ };
23
+ }
24
+ else {
25
+ delete enrichedProps.label;
26
+ }
27
+ return _jsx(EnrichedChip, { ...enrichedProps, type: type, label: props.label });
28
+ };
29
+ export default memo(ClueChip);
@@ -0,0 +1,4 @@
1
+ import type { LeadFormProps } from '@cccsaurora/howler-ui/components/routes/dossiers/LeadEditor';
2
+ import { type FC } from 'react';
3
+ declare const ClueLeadForm: FC<LeadFormProps>;
4
+ export default ClueLeadForm;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useClueFetcherSelector } from '@cccsaurora/clue-ui/hooks/selectors';
3
+ import { Autocomplete, Divider, ListItemText, TextField, Typography } from '@mui/material';
4
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
+ import uniq from 'lodash-es/uniq';
6
+ import { useContext, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ const ClueLeadForm = ({ lead, metadata, update, updateMetadata }) => {
9
+ const { t } = useTranslation();
10
+ const { config } = useContext(ApiConfigContext);
11
+ const fetchers = useClueFetcherSelector(ctx => ctx.fetchers);
12
+ const [showCustom, setShowCustom] = useState(false);
13
+ return (_jsxs(_Fragment, { children: [_jsx(Divider, { orientation: "horizontal" }), _jsx(Autocomplete, { disabled: !lead, options: Object.keys(fetchers), renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.clue') }), value: Object.keys(fetchers).includes(lead?.content) ? lead.content : null, onChange: (_ev, content) => update({ content, metadata: '{}' }), renderOption: ({ key, ...props }, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: _jsx("code", { children: option }), secondary: fetchers[option].description }, key)) }), _jsx(Autocomplete, { options: fetchers[lead?.content]?.supported_types ??
14
+ uniq(Object.values(fetchers).flatMap(fetcher => fetcher.supported_types)), renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.clue.type') }), value: metadata?.type ?? null, onChange: (_ev, type) => updateMetadata({ type }) }), _jsx(Autocomplete, { options: ['custom', ...Object.keys(config.indexes.hit)], disabled: !metadata?.type, renderInput: params => _jsx(TextField, { ...params, size: "small", label: t('route.dossiers.manager.clue.value') }), getOptionLabel: opt => t(opt), value: metadata?.value ?? null, onChange: (_ev, value) => {
15
+ if (value === 'custom') {
16
+ setShowCustom(true);
17
+ }
18
+ else {
19
+ setShowCustom(false);
20
+ updateMetadata({ value });
21
+ }
22
+ } }), showCustom && (_jsxs(_Fragment, { children: [_jsx(TextField, { size: "small", label: t('route.dossiers.manager.clue.value.custom'), value: metadata?.value ?? '', disabled: !metadata?.type, fullWidth: true, onChange: ev => updateMetadata({ value: ev.target.value }) }), _jsx(Typography, { variant: "caption", color: "text.secondary", sx: { mt: '0 !important' }, children: t('route.dossiers.manager.clue.value.description') })] }))] }));
23
+ };
24
+ export default ClueLeadForm;
@@ -0,0 +1,3 @@
1
+ import type { PivotLinkProps } from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
2
+ declare const _default: import("react").NamedExoticComponent<PivotLinkProps>;
3
+ export default _default;
@@ -0,0 +1,145 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useClueActionsSelector, useClueEnrichSelector } from '@cccsaurora/clue-ui';
3
+ import { Icon } from '@iconify/react/dist/iconify.js';
4
+ import { Settings } from '@mui/icons-material';
5
+ import { Divider, IconButton, Stack, Typography } from '@mui/material';
6
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
7
+ import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
8
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
9
+ import get from 'lodash-es/get';
10
+ import isBoolean from 'lodash-es/isBoolean';
11
+ import isNil from 'lodash-es/isNil';
12
+ import { memo, useCallback, useContext, useState } from 'react';
13
+ import { useTranslation } from 'react-i18next';
14
+ const CluePivot = ({ pivot, hit, compact }) => {
15
+ const guessType = useClueEnrichSelector(ctx => ctx?.guessType);
16
+ const { showErrorMessage } = useMySnackbar();
17
+ const { config } = useContext(ApiConfigContext);
18
+ const { i18n, t } = useTranslation();
19
+ const actions = useClueActionsSelector(ctx => ctx?.availableActions ?? {});
20
+ const executeAction = useClueActionsSelector(ctx => ctx?.executeAction);
21
+ const [loading, setLoading] = useState(false);
22
+ const getValue = useCallback((actionId, mapping) => {
23
+ const parameterSchema = actions[actionId].params?.properties?.[mapping.key];
24
+ if (mapping.field === 'custom') {
25
+ if (parameterSchema?.type === 'number') {
26
+ return parseFloat(mapping.custom_value);
27
+ }
28
+ if (parameterSchema?.type === 'integer') {
29
+ return Math.floor(parseFloat(mapping.custom_value));
30
+ }
31
+ return mapping.custom_value;
32
+ }
33
+ if (!Object.keys(config.indexes.hit).includes(mapping.field)) {
34
+ return mapping.field;
35
+ }
36
+ const hitData = get(hit, mapping.field);
37
+ // No schema provided - just pass on the value
38
+ if (!parameterSchema) {
39
+ return hitData;
40
+ }
41
+ // JSON schema is a boolean - we just shove whatever they want in there if it's true, or skip if false
42
+ if (isBoolean(parameterSchema)) {
43
+ return parameterSchema ? hitData : null;
44
+ }
45
+ // It wants a list of values
46
+ if (parameterSchema.type === 'array') {
47
+ // We have a list of values
48
+ if (Array.isArray(hitData)) {
49
+ return hitData;
50
+ }
51
+ else {
52
+ // We don't have a list of values
53
+ return isNil(hitData) ? [] : [hitData];
54
+ }
55
+ }
56
+ // It wants a single object, but we have a list
57
+ if (Array.isArray(hitData)) {
58
+ // TODO: This is still a little blah.
59
+ return hitData[0];
60
+ }
61
+ // We have a single object and that's what they want
62
+ return hitData;
63
+ }, [actions, config.indexes.hit, hit]);
64
+ const onClueClick = useCallback(async (event, forceMenu = false) => {
65
+ event.preventDefault();
66
+ event.stopPropagation();
67
+ if (loading) {
68
+ return;
69
+ }
70
+ if (!actions[pivot.value]) {
71
+ showErrorMessage(t('pivot.clue.missing'));
72
+ return;
73
+ }
74
+ setLoading(true);
75
+ const data = Object.fromEntries(pivot.mappings.map(_mapping => {
76
+ const value = getValue(pivot.value, _mapping);
77
+ if (['selector', 'selectors'].includes(_mapping.key)) {
78
+ if (!value) {
79
+ return _mapping.key === 'selector' ? ['selector', null] : ['selectors', []];
80
+ }
81
+ if (Array.isArray(value)) {
82
+ return [
83
+ _mapping.key,
84
+ value
85
+ .filter(val => !isNil(val))
86
+ .map(val => ({
87
+ type: config.configuration?.mapping?.[_mapping.field] || guessType(val.toString()),
88
+ value: val
89
+ }))
90
+ ];
91
+ }
92
+ return [
93
+ _mapping.key,
94
+ {
95
+ type: config.configuration?.mapping?.[_mapping.field] || guessType(value.toString()),
96
+ value
97
+ }
98
+ ];
99
+ }
100
+ return [_mapping.key, value];
101
+ }));
102
+ const selectors = (actions[pivot.value].accept_multiple ? [data.selectors] : [data.selector])
103
+ .flat()
104
+ .filter(val => !isNil(val));
105
+ delete data.selector;
106
+ delete data.selectors;
107
+ try {
108
+ await executeAction(pivot.value, selectors, data, { forceMenu });
109
+ }
110
+ finally {
111
+ setLoading(false);
112
+ }
113
+ }, [
114
+ actions,
115
+ config.configuration?.mapping,
116
+ executeAction,
117
+ getValue,
118
+ guessType,
119
+ loading,
120
+ pivot.mappings,
121
+ pivot.value,
122
+ showErrorMessage,
123
+ t
124
+ ]);
125
+ if (!actions[pivot.value]) {
126
+ return;
127
+ }
128
+ return (_jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: e => onClueClick(e), sx: [
129
+ theme => ({
130
+ backgroundColor: 'transparent',
131
+ transition: theme.transitions.create(['border-color']),
132
+ '&:hover': { borderColor: 'primary.main' },
133
+ '& > div': {
134
+ height: '100%'
135
+ }
136
+ }),
137
+ loading
138
+ ? { opacity: 0.5, pointerEvents: 'none' }
139
+ : {
140
+ cursor: 'pointer'
141
+ },
142
+ !compact && { border: 'thin solid', borderColor: 'transparent' }
143
+ ], children: _jsxs(Stack, { direction: "row", p: compact ? 0.5 : 1, spacing: 1, alignItems: "center", children: [_jsx(Icon, { fontSize: "1.5rem", icon: pivot.icon }), _jsx(Typography, { children: pivot.label[i18n.language] }), _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(IconButton, { size: "small", onClick: e => onClueClick(e, true), children: _jsx(Settings, { fontSize: "small" }) })] }) }));
144
+ };
145
+ export default memo(CluePivot);
@@ -0,0 +1,21 @@
1
+ import type { PivotFormProps } from '@cccsaurora/howler-ui/components/routes/dossiers/PivotForm';
2
+ import { type FC } from 'react';
3
+ /**
4
+ * CluePivotForm Component
5
+ *
6
+ * A form component that allows users to configure pivot operations for Clue actions.
7
+ * This component provides a dynamic form interface for selecting and configuring
8
+ * Clue actions with their associated parameters and field mappings.
9
+ *
10
+ * Key Features:
11
+ * - Action selection via autocomplete dropdown
12
+ * - Dynamic form generation based on action schemas
13
+ * - Field mapping configuration for hit indexes
14
+ * - Custom value support for non-standard fields
15
+ * - Integration with JsonForms for complex parameter handling
16
+ *
17
+ * @param props - Component props containing pivot data and update callback
18
+ * @returns JSX element representing the pivot configuration form
19
+ */
20
+ declare const CluePivotForm: FC<PivotFormProps>;
21
+ export default CluePivotForm;
@@ -0,0 +1,270 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useClueActionsSelector, useClueEnrichSelector } from '@cccsaurora/clue-ui';
3
+ import { adaptSchema } from '@cccsaurora/clue-ui/components/actions/form/schemaAdapter';
4
+ import { materialCells, materialRenderers } from '@jsonforms/material-renderers';
5
+ import { JsonForms } from '@jsonforms/react';
6
+ import { Autocomplete, createTheme, Divider, Stack, TextField, ThemeProvider, Typography, useTheme } from '@mui/material';
7
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
8
+ import ErrorBoundary from '@cccsaurora/howler-ui/components/routes/ErrorBoundary';
9
+ import { flatten, unflatten } from 'flat';
10
+ import capitalize from 'lodash-es/capitalize';
11
+ import cloneDeep from 'lodash-es/cloneDeep';
12
+ import isBoolean from 'lodash-es/isBoolean';
13
+ import isEqual from 'lodash-es/isEqual';
14
+ import isPlainObject from 'lodash-es/isPlainObject';
15
+ import merge from 'lodash-es/merge';
16
+ import omitBy from 'lodash-es/omitBy';
17
+ import pick from 'lodash-es/pick';
18
+ import pickBy from 'lodash-es/pickBy';
19
+ import { useCallback, useContext, useMemo } from 'react';
20
+ import { useTranslation } from 'react-i18next';
21
+ /**
22
+ * Enhanced material renderers wrapped in ErrorBoundary components to prevent
23
+ * form rendering errors from crashing the entire application
24
+ */
25
+ const WRAPPED_RENDERERS = materialRenderers.map(value => ({
26
+ ...value,
27
+ renderer: ({ ...props }) => (_jsx(ErrorBoundary, { children: _jsx(value.renderer, { ...props }) }))
28
+ }));
29
+ /**
30
+ * CluePivotForm Component
31
+ *
32
+ * A form component that allows users to configure pivot operations for Clue actions.
33
+ * This component provides a dynamic form interface for selecting and configuring
34
+ * Clue actions with their associated parameters and field mappings.
35
+ *
36
+ * Key Features:
37
+ * - Action selection via autocomplete dropdown
38
+ * - Dynamic form generation based on action schemas
39
+ * - Field mapping configuration for hit indexes
40
+ * - Custom value support for non-standard fields
41
+ * - Integration with JsonForms for complex parameter handling
42
+ *
43
+ * @param props - Component props containing pivot data and update callback
44
+ * @returns JSX element representing the pivot configuration form
45
+ */
46
+ const CluePivotForm = ({ pivot, update }) => {
47
+ const theme = useTheme();
48
+ const { t } = useTranslation();
49
+ const { config } = useContext(ApiConfigContext);
50
+ // Get available Clue actions from the global state
51
+ const actions = useClueActionsSelector(ctx => ctx?.availableActions);
52
+ const ready = useClueEnrichSelector(ctx => ctx.ready);
53
+ /**
54
+ * Generates a dynamic JSON schema for the selected action's parameters.
55
+ * This schema is used to render the form fields for action configuration.
56
+ *
57
+ * The function:
58
+ * 1. Retrieves the action definition based on the pivot value
59
+ * 2. Adapts the action's parameter schema using Clue's schema adapter
60
+ * 3. Enhances string properties with hit index enums for autocomplete
61
+ * 4. Applies any extra schema definitions from the action
62
+ */
63
+ const formSchema = useMemo(() => {
64
+ const value = pivot?.value;
65
+ // Return null if no action is selected or action doesn't exist
66
+ if (!value || !actions[value]) {
67
+ return null;
68
+ }
69
+ // Clone and adapt the action's parameter schema
70
+ const clueAdaptedSchema = cloneDeep({
71
+ ...adaptSchema(actions[value].params),
72
+ ...(actions[value].extra_schema ?? {})
73
+ });
74
+ // Enhance string properties with hit index options for better UX
75
+ Object.entries(clueAdaptedSchema.properties).forEach(([key, prop]) => {
76
+ // Handle boolean properties by converting to enum with hit indexes
77
+ if (typeof prop === 'boolean') {
78
+ clueAdaptedSchema.properties[key] = { enum: Object.keys(config.indexes.hit) };
79
+ return;
80
+ }
81
+ if (prop.type === 'array') {
82
+ if (isBoolean(prop.items)) {
83
+ clueAdaptedSchema.properties[key] = { enum: Object.keys(config.indexes.hit) };
84
+ return;
85
+ }
86
+ else if (!Array.isArray(prop.items)) {
87
+ prop.type = prop.items.type;
88
+ delete prop.items;
89
+ }
90
+ }
91
+ // Skip non-string properties
92
+ if (prop.type !== 'string' && (!Array.isArray(prop.type) || !prop.type.includes('string'))) {
93
+ return;
94
+ }
95
+ // Strip date-time format since we're picking from a list of fields
96
+ if (prop.format === 'date-time') {
97
+ delete prop.format;
98
+ }
99
+ // Add hit index options to string properties without existing enums
100
+ if (!prop.enum && !prop.oneOf) {
101
+ prop.enum = Object.keys(config.indexes.hit);
102
+ }
103
+ });
104
+ return clueAdaptedSchema;
105
+ }, [actions, config.indexes.hit, pivot?.value]);
106
+ /**
107
+ * Generates the UI schema for JsonForms based on the form schema.
108
+ * This defines the layout and rendering options for each form field.
109
+ *
110
+ * The UI schema:
111
+ * 1. Creates a vertical layout for all form elements
112
+ * 2. Sorts fields by custom order property or required status
113
+ * 3. Configures autocomplete for enum fields
114
+ * 4. Applies custom options and rules from the schema
115
+ */
116
+ const uiSchema = useMemo(() => ({
117
+ type: 'VerticalLayout',
118
+ elements: Object.entries(formSchema?.properties ?? {})
119
+ // Sort fields: first by custom order, then by required status
120
+ .sort(([a_key, a_ent], [b_key, b_ent]) => {
121
+ if (!!a_ent.order || !!b_ent.order) {
122
+ return a_ent.order - b_ent.order;
123
+ }
124
+ else {
125
+ // Required fields appear first
126
+ return +formSchema?.required.includes(a_key) - +formSchema?.required.includes(b_key);
127
+ }
128
+ })
129
+ .map(([key, value]) => ({
130
+ type: 'Control',
131
+ scope: `#/properties/${key}`,
132
+ options: {
133
+ // Enable autocomplete for fields with enum values
134
+ autocomplete: !!value.enum || !!value.oneOf,
135
+ showUnfocusedDescription: true,
136
+ // Apply any custom options from the schema
137
+ ...value.options
138
+ },
139
+ // Apply conditional rendering rules if present
140
+ rule: value.rule
141
+ }))
142
+ }), [formSchema?.properties, formSchema?.required]);
143
+ /**
144
+ * Handles updates to the selected action value.
145
+ * When an action is selected, this function:
146
+ * 1. Sets up default field mappings based on action requirements
147
+ * 2. Configures selector mappings for actions that don't accept empty inputs
148
+ * 3. Updates the pivot configuration
149
+ *
150
+ * @param value - The selected action identifier
151
+ */
152
+ const onUpdate = useCallback((value) => {
153
+ const mappings = [];
154
+ // TODO: Fix type hints and remove the any cast
155
+ // For actions that don't accept empty inputs, add selector mapping
156
+ if (!actions[value].accept_empty) {
157
+ // Use 'selectors' for multiple selection, 'selector' for single selection
158
+ mappings.push({
159
+ key: actions[value].accept_multiple ? 'selectors' : 'selector',
160
+ field: 'howler.id'
161
+ });
162
+ }
163
+ update({
164
+ value,
165
+ mappings
166
+ });
167
+ }, [actions, update]);
168
+ /**
169
+ * Transforms pivot mappings into form data format for JsonForms.
170
+ * Converts the mappings array into a key-value object where:
171
+ * - Key is the mapping key (e.g., 'selector', 'selectors')
172
+ * - Value is either the custom_value (if set) or the field name
173
+ */
174
+ const formData = useMemo(() => {
175
+ const rawData = Object.fromEntries((pivot?.mappings ?? []).map(mapping => {
176
+ const parameterSchema = actions[pivot.value]?.params?.properties?.[mapping.key];
177
+ const value = mapping.custom_value ?? mapping.field;
178
+ if (mapping.field === 'custom') {
179
+ if (parameterSchema?.type === 'number') {
180
+ return [mapping.key, parseFloat(mapping.custom_value)];
181
+ }
182
+ if (parameterSchema?.type === 'integer') {
183
+ return [mapping.key, Math.floor(parseFloat(mapping.custom_value))];
184
+ }
185
+ return [mapping.key, mapping.custom_value];
186
+ }
187
+ return [mapping.key, value];
188
+ }));
189
+ // JSONForms will inadvertently nest dicts
190
+ // (e.g. "abc1.field" key will result in)
191
+ // {
192
+ // "abc1": {
193
+ // "field"
194
+ // }
195
+ // }
196
+ // Validation doesn't account for this, so we need to merge the flattened and unflattened dicts.
197
+ // This leads to awful side effects in the onChange function, detailed there.
198
+ return merge({}, rawData, unflatten(rawData));
199
+ }, [actions, pivot?.mappings, pivot.value]);
200
+ // Find the main selector mapping for special handling in the UI
201
+ const selectorMapping = pivot?.mappings?.find(_mapping => ['selector', 'selectors'].includes(_mapping.key));
202
+ return (_jsxs(ErrorBoundary, { children: [_jsx(Autocomplete, { fullWidth: true, disabled: !pivot || !ready, options: Object.entries(actions)
203
+ .filter(([_key, definition]) => !!definition && definition.format === 'pivot')
204
+ .map(([key]) => key), renderOption: ({ key, ...optionProps }, actionId) => {
205
+ const definition = actions[actionId];
206
+ return (_jsxs(Stack, { component: "li", ...optionProps, spacing: 1, children: [_jsxs(Stack, { direction: "row", spacing: 1, alignSelf: "start", alignItems: "center", children: [_jsx(Typography, { children: definition.name }), _jsx("pre", { style: {
207
+ fontSize: '0.85rem',
208
+ border: `thin solid ${theme.palette.divider}`,
209
+ padding: theme.spacing(0.5),
210
+ borderRadius: theme.shape.borderRadius
211
+ }, children: actionId })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", alignSelf: "start", children: definition.summary })] }, key));
212
+ }, getOptionLabel: opt => actions[opt]?.name ?? '', renderInput: params => (_jsx(TextField, { ...params, size: "small", fullWidth: true, label: t('route.dossiers.manager.pivot.value') })), value: pivot?.value ?? '', onChange: (_ev, value) => onUpdate(value) }), _jsx(Divider, { flexItem: true }), _jsx(Typography, { children: t('route.dossiers.manager.pivot.mappings') }), selectorMapping && (_jsxs(_Fragment, { children: [_jsx(Autocomplete, { fullWidth: true, options: ['custom', ...Object.keys(config.indexes.hit)], renderInput: params => (_jsx(TextField, { ...params, size: "small", fullWidth: true, label: capitalize(selectorMapping.key), sx: { minWidth: '150px' } })), value: selectorMapping.field, onChange: (_ev, field) => update({
213
+ mappings: pivot.mappings.map(_mapping => _mapping.key === selectorMapping.key ? { ..._mapping, field } : _mapping)
214
+ }) }), selectorMapping.field === 'custom' && (_jsx(TextField, { size: "small", label: t('route.dossiers.manager.pivot.mapping.custom'), disabled: !pivot, value: selectorMapping?.custom_value ?? null, onChange: ev => update({
215
+ mappings: pivot.mappings.map(_mapping => _mapping.key === selectorMapping.key ? { ..._mapping, custom_value: ev.target.value } : _mapping)
216
+ }) }))] })), formSchema && (_jsx(ThemeProvider, { theme: _theme => createTheme({
217
+ ..._theme,
218
+ components: {
219
+ MuiTextField: {
220
+ defaultProps: { size: 'small' },
221
+ styleOverrides: { root: { marginTop: theme.spacing(1) } }
222
+ },
223
+ MuiInputBase: {
224
+ defaultProps: { size: 'small' }
225
+ },
226
+ MuiFormControl: {
227
+ defaultProps: { size: 'small' },
228
+ styleOverrides: {
229
+ root: { marginTop: theme.spacing(1) }
230
+ }
231
+ }
232
+ }
233
+ }), children: _jsx(ErrorBoundary, { children: _jsx(JsonForms, { schema: formSchema, uischema: uiSchema, renderers: WRAPPED_RENDERERS, cells: materialCells, data: formData, onChange: ({ data }) => {
234
+ // We now need to remove the unflattened object we provided for validation to JSON forms
235
+ // EXCEPT, new keys are added via nesting. So we need to flatten the nested data and overwrite it
236
+ // with the flat data.
237
+ const flatData = omitBy(data, isPlainObject);
238
+ const nestedData = flatten(pickBy(data, isPlainObject));
239
+ // Merge existing selector mappings with new form data. This is the crazy complexity I mentioned above.
240
+ //
241
+ // The full flow:
242
+ // 1. A field mapping is chosen, e.g. 'key.1': 'howler.id'
243
+ // 2. The data object has the structure {key: {1: 'howler.id'}}
244
+ // 3. This is added to fullData via nestedData
245
+ // 4. Another field mapping is chosen, e.g. 'key.2': 'howler.id'
246
+ // 5. The data object now looks like: {key.1: 'howler.id', key: {1: 'howler.id', 2: 'howler.id'}}
247
+ // 6. We integrate this again, but this time the existing `key.1` key overwrites
248
+ // the flattened `key: {1}` object.
249
+ // 7. When 'key.1' is changed, the ROOT (flattened) key changes, but the outdated variant is still there.
250
+ // that is: {key.1: 'howler.status', key.2: 'howler.id', key: {1: 'howler.id', 2: 'howler.id'}}
251
+ // 8. This is why the flat data is AFTER the nested data. So that the final, resolved object is:
252
+ // {key.1: 'howler.status', key.2: 'howler.id'}
253
+ //
254
+ // This is very confusing - ask Matt R if you need a better explanation.
255
+ const fullData = { ...nestedData, ...flatData, ...pick(pivot.mappings, ['selector', 'selectors']) };
256
+ // Convert form data back to mappings format
257
+ const newMappings = Object.entries(fullData).map(([key, val]) => ({
258
+ key,
259
+ // Use 'custom' field type if value is not a standard hit index
260
+ field: val in config.indexes.hit ? val : 'custom',
261
+ // Store custom values separately from field names
262
+ custom_value: val in config.indexes.hit ? null : val
263
+ }));
264
+ // Only update if mappings have actually changed (performance optimization)
265
+ if (!isEqual(newMappings, pivot.mappings)) {
266
+ update({ mappings: newMappings });
267
+ }
268
+ }, config: {} }) }) }))] }));
269
+ };
270
+ export default CluePivotForm;
@@ -0,0 +1,3 @@
1
+ import type { PluginTypographyProps } from '@cccsaurora/howler-ui/components/elements/PluginTypography';
2
+ declare const _default: import("react").NamedExoticComponent<PluginTypographyProps>;
3
+ export default _default;