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

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 +135 -126
  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
@@ -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;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import EnrichedTypography from '@cccsaurora/clue-ui/components/EnrichedTypography';
3
+ import { Typography } from '@mui/material';
4
+ import { memo } from 'react';
5
+ import { useType } from '../utils';
6
+ const ClueTypography = ({ children, value, context, field, hit, ...props }) => {
7
+ const type = useType(hit, field, value);
8
+ if (!type) {
9
+ return _jsx(Typography, { ...props, children: children ?? value });
10
+ }
11
+ let enrichedProps = {
12
+ ...props,
13
+ value
14
+ };
15
+ if (context === 'banner') {
16
+ enrichedProps = {
17
+ ...enrichedProps,
18
+ slotProps: { stack: { component: 'span' } }
19
+ };
20
+ }
21
+ else if (context === 'outline') {
22
+ enrichedProps = {
23
+ ...enrichedProps,
24
+ hideLoading: true,
25
+ slotProps: {
26
+ stack: {
27
+ sx: { mr: 'auto' },
28
+ onClick: e => {
29
+ e.preventDefault();
30
+ e.stopPropagation();
31
+ }
32
+ },
33
+ popover: {
34
+ onClick: e => {
35
+ e.preventDefault();
36
+ e.stopPropagation();
37
+ }
38
+ }
39
+ }
40
+ };
41
+ }
42
+ else if (context === 'table') {
43
+ enrichedProps = {
44
+ ...enrichedProps,
45
+ hideLoading: true,
46
+ slotProps: {
47
+ stack: { sx: { width: '100%', '& > p': { textOverflow: 'ellipsis', overflow: 'hidden' } } }
48
+ }
49
+ };
50
+ }
51
+ return _jsx(EnrichedTypography, { ...enrichedProps, type: type });
52
+ };
53
+ export default memo(ClueTypography);
@@ -0,0 +1,3 @@
1
+ import type { HowlerHelper } from '@cccsaurora/howler-ui/components/elements/display/handlebars/helpers';
2
+ declare const HELPERS: HowlerHelper[];
3
+ export default HELPERS;
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /* eslint-disable no-console */
3
+ import EnrichedTypography, {} from '@cccsaurora/clue-ui/components/EnrichedTypography';
4
+ import Fetcher from '@cccsaurora/clue-ui/components/fetchers/Fetcher';
5
+ import Entry from '@cccsaurora/clue-ui/components/group/Entry';
6
+ import Group from '@cccsaurora/clue-ui/components/group/Group';
7
+ import { useClueEnrichSelector } from '@cccsaurora/clue-ui/hooks/selectors';
8
+ import { Checkbox, Paper, Stack, Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material';
9
+ import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
10
+ import i18nInstance from '@cccsaurora/howler-ui/i18n';
11
+ import capitalize from 'lodash-es/capitalize';
12
+ import groupBy from 'lodash-es/groupBy';
13
+ import uniq from 'lodash-es/uniq';
14
+ import { useState } from 'react';
15
+ import { useTranslation } from 'react-i18next';
16
+ const MarkdownTypography = ({ type, value, ...props }) => {
17
+ const { t } = useTranslation();
18
+ try {
19
+ const guessType = useClueEnrichSelector(ctx => ctx.guessType);
20
+ if (!type || type?.toString().toLowerCase() === 'guess') {
21
+ type = guessType(value.toString());
22
+ }
23
+ if (!type) {
24
+ return _jsx("span", { children: value });
25
+ }
26
+ return _jsx(EnrichedTypography, { ...props, type: type, value: value });
27
+ }
28
+ catch (err) {
29
+ return (_jsxs(Stack, { children: [_jsx("strong", { style: { color: 'red' }, children: t('markdown.error') }), _jsx("strong", { children: err.toString() }), _jsx("code", { style: { fontSize: '0.8rem' }, children: _jsx("pre", { children: err.stack }) })] }));
30
+ }
31
+ };
32
+ const ClueGroup = props => {
33
+ if (!props.enabled) {
34
+ return _jsx(_Fragment, { children: props.children });
35
+ }
36
+ if (!props.type) {
37
+ console.error('Missing required props for group helper');
38
+ return (_jsxs(Stack, { spacing: 1, children: [_jsx("strong", { style: { color: 'red' }, children: i18nInstance.t('markdown.error') }), _jsxs("code", { style: { fontSize: '0.8rem' }, children: [i18nInstance.t('markdown.props.missing'), ": type"] })] }));
39
+ }
40
+ return _jsx(Group, { type: props.type, children: props.children });
41
+ };
42
+ const ClueEntry = ({ value }) => {
43
+ const [checked, setChecked] = useState(false);
44
+ return (_jsx(Entry, { entry: value, selected: checked, children: _jsx(Paper, { sx: { p: 1 }, children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Checkbox, { checked: checked, onChange: (_event, _checked) => setChecked(_checked) }), _jsx(MarkdownTypography, { value: value }), _jsx(FlexOne, {})] }) }) }));
45
+ };
46
+ const ClueCheckbox = ({ value }) => {
47
+ const [checked, setChecked] = useState(false);
48
+ return (_jsx(Entry, { entry: value, selected: checked, children: _jsx(Checkbox, { checked: checked, onChange: (_event, _checked) => setChecked(_checked) }) }));
49
+ };
50
+ const HELPERS = [
51
+ {
52
+ keyword: 'clue',
53
+ documentation: {
54
+ en: 'Given a selector, this helper enriches the selector through clue.',
55
+ fr: 'Étant donné un sélecteur, cet assistant enrichit le sélecteur via clue.'
56
+ },
57
+ componentCallback: (type, value) => {
58
+ if (typeof type !== 'string' || typeof value !== 'string') {
59
+ return (_jsxs(Stack, { spacing: 1, children: [_jsx("strong", { style: { color: 'red' }, children: i18nInstance.t('markdown.error') }), _jsx("code", { style: { fontSize: '0.8rem' }, children: i18nInstance.t('markdown.helpers.clue.arguments') })] }));
60
+ }
61
+ return (_jsx(MarkdownTypography, { slotProps: { stack: { component: 'span', sx: { width: 'fit-content' } } }, component: "span", type: type, value: typeof value === 'string' ? value : null }));
62
+ }
63
+ },
64
+ {
65
+ keyword: 'fetcher',
66
+ documentation: {
67
+ en: 'Given a selector, this helper fetches data for the selector through clue.',
68
+ fr: 'Étant donné un sélecteur, cet assistant récupère les données pour le sélecteur via clue.'
69
+ },
70
+ componentCallback: (...args) => {
71
+ const options = args.pop();
72
+ const props = options?.hash ?? {};
73
+ if (!props.type || !props.value || !props.fetcherId) {
74
+ console.error('Missing required props for fetcher helper');
75
+ return (_jsxs(Stack, { spacing: 1, children: [_jsx("strong", { style: { color: 'red' }, children: i18nInstance.t('markdown.error') }), _jsxs("code", { style: { fontSize: '0.8rem' }, children: [i18nInstance.t('markdown.props.missing'), ":", ' ', ['type', 'value', 'fetcherId'].filter(key => !props[key]).join(', ')] })] }));
76
+ }
77
+ if (props.fetcherId.includes('eml')) {
78
+ props.fetcherId = props.fetcherId.replace('eml', 'email');
79
+ }
80
+ console.debug(`Rendering fetcher (${props.fetcherId}) for selector ${props.type}:${props.value}`);
81
+ return (_jsx(Fetcher, { slotProps: { stack: { component: 'span', sx: { width: 'fit-content' } } }, component: "span", ...props }));
82
+ }
83
+ },
84
+ {
85
+ keyword: 'clue_group',
86
+ documentation: {
87
+ en: 'Initializes a clue group',
88
+ fr: 'Initialise un groupe clue'
89
+ },
90
+ componentCallback: (values, ...args) => {
91
+ const options = args.pop();
92
+ const props = options?.hash ?? {};
93
+ const missing = [];
94
+ if (!Array.isArray(values)) {
95
+ missing.push('values');
96
+ }
97
+ if (!props.type) {
98
+ missing.push('type');
99
+ }
100
+ if (missing.length > 0) {
101
+ return (_jsxs(Stack, { spacing: 1, children: [_jsx("strong", { style: { color: 'red' }, children: i18nInstance.t('markdown.error') }), _jsxs("code", { style: { fontSize: '0.8rem' }, children: [i18nInstance.t('markdown.props.missing'), ": ", missing.join(', ')] })] }));
102
+ }
103
+ return (_jsx(ClueGroup, { type: props.type, enabled: true, children: _jsx(Stack, { spacing: 1, mt: 1, children: uniq(values).map(value => (_jsx(ClueEntry, { value: value }, value))) }) }));
104
+ }
105
+ },
106
+ {
107
+ keyword: 'clue_table',
108
+ documentation: {
109
+ en: `Render a table with optional Clue enrichments, fetchers and actions.
110
+
111
+ Clue enrichments are performed for cells with a clue_type and no clue_fetcher.
112
+
113
+ Clue fetchers are used for cells with a clue_type and a clue_fetcher.
114
+
115
+ Clue actions are enabled by specifying a clue action type using the optional clue_action_type parameter. If enabled, cells with clue_entity==true will be selectable for use with Clue enrichments and actions, with a value of action_value if present, and otherwise value.
116
+
117
+ Example:
118
+ \`\`\`markdown
119
+ {{curly 'clue_table clue_table_cells clue_action_type="ip"'}}
120
+ \`\`\`
121
+ where clue_table_cells is an array with properties:
122
+
123
+ \`\`\`
124
+ column: string;
125
+ row: string;
126
+ value: string;
127
+ clue_type (optional): string;
128
+ clue_fetcher (optional): string;
129
+ fetcher_width (optional): string;
130
+ clue_entity (optional): boolean;
131
+ action_value (optional): string;
132
+ \`\`\`
133
+ `,
134
+ fr: `Affiche un tableau avec des enrichissements, des extractions et des actions Clue optionnelles.
135
+
136
+ Les enrichissements Clue sont effectués pour les cellules avec un clue_type et sans un clue_fetcher.
137
+
138
+ Clue récupère les données pour le sélecteur pour les cellules avec un clue_type et un clue_fetcher.
139
+
140
+ Les actions Clue sont activées en spécifiant un type d'action clue en utilisant le paramètre optionnel clue_action_type. Si activé, les cellules avec clue_entity==true seront sélectionnables pour utilisation avec les enrichissements et actions Clue, avec une valeur de action_value si présente, sinon la valeur.
141
+
142
+ Exemple :
143
+ \`\`\`markdown
144
+ {{curly 'clue_table clue_table_cells clue_action_type="ip"'}}
145
+ \`\`\`
146
+ où clue_table_cells est un tableau avec les propriétés :
147
+
148
+ \`\`\`
149
+ column: string;
150
+ row: string;
151
+ value: string;
152
+ clue_type (optionnel): string;
153
+ clue_fetcher (optionnel): string;
154
+ fetcher_width (optionnel): string;
155
+ clue_entity (optionnel): boolean;
156
+ action_value (optionnel): string;
157
+ \`\`\`
158
+ `
159
+ },
160
+ componentCallback: (cells, ...args) => {
161
+ const options = args.pop();
162
+ const props = options?.hash ?? {};
163
+ const columns = Object.keys(groupBy(cells, 'column'));
164
+ const rows = groupBy(cells, 'row');
165
+ const clueActionType = props.clue_action_type;
166
+ const enableClueActions = !!clueActionType;
167
+ return (_jsx(Paper, { sx: { width: '95%', overflowX: 'auto', m: 1 }, children: _jsx(ClueGroup, { type: clueActionType, enabled: enableClueActions, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsx(TableRow, { children: columns.map(col => (_jsx(TableCell, { sx: { maxWidth: '150px' }, children: col
168
+ .split(/[_-]/)
169
+ .map(word => capitalize(word))
170
+ .join(' ') }, col))) }) }), _jsx(TableBody, { sx: { '& td': { wordBreak: 'break-word' } }, children: Object.entries(rows).map(([rowId, _cells]) => {
171
+ return (_jsx(TableRow, { children: columns.map(col => {
172
+ const cell = _cells.find(row => row.column === col);
173
+ if (!cell) {
174
+ return _jsx(TableCell, {}, col);
175
+ }
176
+ return (_jsxs(TableCell, { children: [enableClueActions && cell.clue_entity && (_jsx(ClueCheckbox, { value: cell.action_value ?? cell.value })), !!cell.clue_fetcher && !!cell.clue_type ? (_jsx(Fetcher, { slotProps: {
177
+ image: { width: !!cell.fetcher_width ? cell.fetcher_width : 'fit-content' },
178
+ stack: {
179
+ component: 'span',
180
+ sx: !!cell.fetcher_width
181
+ ? { width: cell.fetcher_width, display: 'block' }
182
+ : { width: 'fit-content' }
183
+ }
184
+ }, fetcherId: cell.clue_fetcher, value: cell.value, type: cell.clue_type })) : !!cell.clue_type ? (_jsx(MarkdownTypography, { slotProps: {
185
+ stack: {
186
+ component: 'span',
187
+ sx: { width: 'fit-content' },
188
+ display: 'inline-flex'
189
+ }
190
+ }, component: "span", type: cell.clue_type, value: cell.value })) : ((cell.value ?? 'N/A'))] }, col + cell.value));
191
+ }) }, rowId));
192
+ }) })] }) }) }));
193
+ }
194
+ }
195
+ ];
196
+ export default HELPERS;
@@ -0,0 +1,21 @@
1
+ import type { HowlerHelper } from '@cccsaurora/howler-ui/components/elements/display/handlebars/helpers';
2
+ import type { PluginChipProps } from '@cccsaurora/howler-ui/components/elements/PluginChip';
3
+ import type { PluginTypographyProps } from '@cccsaurora/howler-ui/components/elements/PluginTypography';
4
+ import { type i18n as I18N } from 'i18next';
5
+ import HowlerPlugin from '@cccsaurora/howler-ui/plugins/HowlerPlugin';
6
+ declare class CluePlugin extends HowlerPlugin {
7
+ name: string;
8
+ version: string;
9
+ author: string;
10
+ description: string;
11
+ activate(): void;
12
+ provider(): import("react").FC<{
13
+ children?: import("react").ReactNode | undefined;
14
+ }>;
15
+ setup(): () => void;
16
+ localization(i18nInstance: I18N): void;
17
+ helpers(): HowlerHelper[];
18
+ typography(_props: PluginTypographyProps): import("react/jsx-runtime").JSX.Element;
19
+ chip(_props: PluginChipProps): import("react/jsx-runtime").JSX.Element;
20
+ }
21
+ export default CluePlugin;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Fetcher from '@cccsaurora/clue-ui/components/fetchers/Fetcher';
3
+ import clueEN from '@cccsaurora/clue-ui/en/translation.json';
4
+ import clueFR from '@cccsaurora/clue-ui/fr/translation.json';
5
+ import { Box } from '@mui/material';
6
+ import {} from 'i18next';
7
+ import get from 'lodash-es/get';
8
+ import HowlerPlugin from '@cccsaurora/howler-ui/plugins/HowlerPlugin';
9
+ import ClueChip from './components/ClueChip';
10
+ import ClueLeadForm from './components/ClueLeadForm';
11
+ import CluePivot from './components/CluePivot';
12
+ import CluePivotForm from './components/CluePivotForm';
13
+ import ClueTypography from './components/ClueTypography';
14
+ import HELPERS from './helpers';
15
+ import howlerClueEN from './locales/clue.en.json';
16
+ import howlerClueFR from './locales/clue.fr.json';
17
+ import Provider from './Provider';
18
+ import useSetup from './setup';
19
+ class CluePlugin extends HowlerPlugin {
20
+ name = 'CluePlugin';
21
+ version = '0.0.1';
22
+ author = 'Matthew Rafuse <matthew.rafuse@cyber.gc.ca>';
23
+ description = 'This plugin enables clue enrichment in Howler.';
24
+ activate() {
25
+ super.activate();
26
+ const leadForm = props => _jsx(ClueLeadForm, { ...props });
27
+ const leadRenderer = (content, metadata, hit) => {
28
+ const parsedProps = JSON.parse(metadata);
29
+ const value = get(hit, parsedProps.value);
30
+ if (Array.isArray(value)) {
31
+ // TODO: Revisit handling for array values
32
+ parsedProps.value = value[0];
33
+ }
34
+ else {
35
+ parsedProps.value = value;
36
+ }
37
+ return (_jsx(Box, { p: 1, flex: 1, display: "flex", alignItems: "stretch", children: _jsx(Fetcher, { fetcherId: content, ...parsedProps }) }));
38
+ };
39
+ super.addLead('clue', leadForm, leadRenderer);
40
+ const pivotForm = (props) => _jsx(CluePivotForm, { ...props });
41
+ const pivotRenderer = (props) => _jsx(CluePivot, { ...props });
42
+ super.addPivot('clue', pivotForm, pivotRenderer);
43
+ }
44
+ provider() {
45
+ return Provider;
46
+ }
47
+ setup() {
48
+ return useSetup;
49
+ }
50
+ localization(i18nInstance) {
51
+ i18nInstance.addResourceBundle('en', 'clue', clueEN, true, true);
52
+ i18nInstance.addResourceBundle('fr', 'clue', clueFR, true, true);
53
+ i18nInstance.addResourceBundle('en', 'translation', howlerClueEN, true, false);
54
+ i18nInstance.addResourceBundle('fr', 'translation', howlerClueFR, true, false);
55
+ }
56
+ helpers() {
57
+ return HELPERS;
58
+ }
59
+ typography(_props) {
60
+ return _jsx(ClueTypography, { ..._props });
61
+ }
62
+ chip(_props) {
63
+ return _jsx(ClueChip, { ..._props });
64
+ }
65
+ }
66
+ export default CluePlugin;
@@ -0,0 +1,8 @@
1
+ {
2
+ "markdown.helpers.clue.arguments": "You must provide at least two arguments: type and value.",
3
+ "route.dossiers.manager.clue": "Clue Fetcher ID",
4
+ "route.dossiers.manager.clue.type": "Selector Type",
5
+ "route.dossiers.manager.clue.value": "Selector Value",
6
+ "route.dossiers.manager.clue.value.custom": "Custom Selector Value",
7
+ "route.dossiers.manager.clue.value.description": "You can use handlebars notation to insert fields from the corresponding alert (i.e. {{howler.id}})."
8
+ }