@cccsaurora/howler-ui 3.1.0-dev.1610 → 3.1.0-dev.1617

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 (64) hide show
  1. package/api/dossier/groups.d.ts +2 -0
  2. package/api/dossier/groups.js +4 -0
  3. package/api/dossier/groups.test.d.ts +1 -0
  4. package/api/dossier/groups.test.js +26 -0
  5. package/api/{dossier.d.ts → dossier/index.d.ts} +2 -2
  6. package/api/{dossier.js → dossier/index.js} +2 -2
  7. package/components/elements/display/HowlerCard.d.ts +1 -1
  8. package/components/elements/display/HowlerCard.js +2 -2
  9. package/components/elements/hit/HitLinks.js +25 -21
  10. package/components/elements/hit/HitLinks.test.d.ts +1 -0
  11. package/components/elements/hit/HitLinks.test.js +118 -0
  12. package/components/elements/hit/related/RelatedIcon.js +2 -1
  13. package/components/elements/hit/related/RelatedIcon.test.d.ts +1 -0
  14. package/components/elements/hit/related/RelatedIcon.test.js +79 -0
  15. package/components/elements/hit/related/RelatedLink.d.ts +5 -0
  16. package/components/elements/hit/related/RelatedLink.js +23 -6
  17. package/components/elements/hit/related/RelatedLink.test.d.ts +1 -0
  18. package/components/elements/hit/related/RelatedLink.test.js +31 -0
  19. package/components/elements/hit/related/pivots/PivotFlyout.d.ts +25 -0
  20. package/components/elements/hit/related/pivots/PivotFlyout.js +26 -0
  21. package/components/elements/hit/related/pivots/PivotFlyoutContent.d.ts +4 -0
  22. package/components/elements/hit/related/pivots/PivotFlyoutContent.js +42 -0
  23. package/components/elements/hit/related/pivots/PivotFlyoutContent.test.d.ts +1 -0
  24. package/components/elements/hit/related/pivots/PivotFlyoutContent.test.js +41 -0
  25. package/components/elements/hit/related/pivots/PivotFolderMenu.d.ts +4 -0
  26. package/components/elements/hit/related/pivots/PivotFolderMenu.js +37 -0
  27. package/components/elements/hit/related/pivots/PivotFolderMenu.test.d.ts +1 -0
  28. package/components/elements/hit/related/pivots/PivotFolderMenu.test.js +55 -0
  29. package/components/elements/hit/related/{PivotLink.d.ts → pivots/PivotLink.d.ts} +9 -2
  30. package/components/elements/hit/related/pivots/PivotLink.js +53 -0
  31. package/components/elements/hit/related/pivots/PivotLink.test.d.ts +1 -0
  32. package/components/elements/hit/related/pivots/PivotLink.test.js +117 -0
  33. package/components/elements/hit/related/pivots/types.d.ts +29 -0
  34. package/components/elements/hit/related/pivots/utils.d.ts +12 -0
  35. package/components/elements/hit/related/pivots/utils.js +51 -0
  36. package/components/elements/hit/related/pivots/utils.test.d.ts +1 -0
  37. package/components/elements/hit/related/pivots/utils.test.js +30 -0
  38. package/components/hooks/useMyPreferences.d.ts +1 -1
  39. package/components/hooks/useMyPreferences.js +20 -2
  40. package/components/hooks/useMyPreferences.test.d.ts +1 -0
  41. package/components/hooks/useMyPreferences.test.js +40 -0
  42. package/components/routes/dossiers/DossierEditor.js +6 -0
  43. package/components/routes/dossiers/DossierEditor.test.js +30 -1
  44. package/components/routes/dossiers/PivotForm.js +41 -4
  45. package/components/routes/dossiers/PivotForm.test.d.ts +1 -0
  46. package/components/routes/dossiers/PivotForm.test.js +126 -0
  47. package/components/routes/dossiers/utils.d.ts +44 -0
  48. package/components/routes/dossiers/utils.js +135 -0
  49. package/components/routes/dossiers/utils.test.d.ts +1 -0
  50. package/components/routes/dossiers/utils.test.js +168 -0
  51. package/locales/en/translation.json +5 -0
  52. package/locales/fr/translation.json +5 -0
  53. package/models/entities/generated/Pivot.d.ts +1 -0
  54. package/package.json +4 -1
  55. package/plugins/HowlerPlugin.d.ts +1 -1
  56. package/plugins/clue/components/CluePivot.d.ts +1 -1
  57. package/plugins/clue/components/CluePivot.js +17 -1
  58. package/plugins/clue/components/CluePivot.test.d.ts +1 -0
  59. package/plugins/clue/components/CluePivot.test.js +94 -0
  60. package/utils/constants.d.ts +1 -0
  61. package/utils/constants.js +1 -0
  62. package/components/elements/hit/ResolvePivotUrl.d.ts +0 -4
  63. package/components/elements/hit/ResolvePivotUrl.js +0 -22
  64. package/components/elements/hit/related/PivotLink.js +0 -93
package/package.json CHANGED
@@ -97,7 +97,7 @@
97
97
  "url": "https://github.com/CybercentreCanada/howler"
98
98
  },
99
99
  "type": "module",
100
- "version": "3.1.0-dev.1610",
100
+ "version": "3.1.0-dev.1617",
101
101
  "exports": {
102
102
  "./i18n": "./i18n.js",
103
103
  "./index.css": "./index.css",
@@ -129,6 +129,8 @@
129
129
  "./api/template": "./api/template/index.js",
130
130
  "./api/socket/*": "./api/socket/*.js",
131
131
  "./api/socket": "./api/socket/index.js",
132
+ "./api/dossier/*": "./api/dossier/*.js",
133
+ "./api/dossier": "./api/dossier/index.js",
132
134
  "./api/utils/*": "./api/utils/*.js",
133
135
  "./api/v2/*": "./api/v2/*.js",
134
136
  "./api/v2": "./api/v2/index.js",
@@ -195,6 +197,7 @@
195
197
  "./components/elements/hit/related/*": "./components/elements/hit/related/*.js",
196
198
  "./components/elements/hit/aggregate/*": "./components/elements/hit/aggregate/*.js",
197
199
  "./components/elements/hit/actions/*": "./components/elements/hit/actions/*.js",
200
+ "./components/elements/hit/related/pivots/*": "./components/elements/hit/related/pivots/*.js",
198
201
  "./components/elements/display/features/*": "./components/elements/display/features/*.js",
199
202
  "./components/elements/display/icons/*": "./components/elements/display/icons/*.js",
200
203
  "./components/elements/display/json/*": "./components/elements/display/json/*.js",
@@ -1,7 +1,7 @@
1
1
  import type { HowlerHelper } from '@cccsaurora/howler-ui/components/elements/display/handlebars/helpers';
2
2
  import type { ActionButton } from '@cccsaurora/howler-ui/components/elements/hit/actions/SharedComponents';
3
3
  import type { StatusProps } from '@cccsaurora/howler-ui/components/elements/hit/HitBanner';
4
- import type { PivotLinkProps } from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
4
+ import type { PivotLinkProps } from '@cccsaurora/howler-ui/components/elements/hit/related/pivots/PivotLink';
5
5
  import type { PluginChipProps } from '@cccsaurora/howler-ui/components/elements/PluginChip';
6
6
  import type { PluginTypographyProps } from '@cccsaurora/howler-ui/components/elements/PluginTypography';
7
7
  import type { CustomActionProps } from '@cccsaurora/howler-ui/components/routes/action/edit/ActionEditor';
@@ -1,3 +1,3 @@
1
- import type { PivotLinkProps } from '@cccsaurora/howler-ui/components/elements/hit/related/PivotLink';
1
+ import type { PivotLinkProps } from '@cccsaurora/howler-ui/components/elements/hit/related/pivots/PivotLink';
2
2
  declare const _default: import("react").NamedExoticComponent<PivotLinkProps>;
3
3
  export default _default;
@@ -12,7 +12,7 @@ import isNil from 'lodash-es/isNil';
12
12
  import { memo, useCallback, useContext, useState } from 'react';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import { notNil } from '@cccsaurora/howler-ui/utils/utils';
15
- const CluePivot = ({ pivot, hit, compact }) => {
15
+ const CluePivot = ({ pivot, hit, compact, variant = 'card', onNavigate }) => {
16
16
  const guessType = useClueEnrichSelector(ctx => ctx?.guessType);
17
17
  const { showErrorMessage } = useMySnackbar();
18
18
  const { config } = useContext(ApiConfigContext);
@@ -124,6 +124,22 @@ const CluePivot = ({ pivot, hit, compact }) => {
124
124
  if (!actions[pivot.value]) {
125
125
  return null;
126
126
  }
127
+ if (variant === 'menu-item') {
128
+ return (_jsxs(Stack, { component: "button", type: "button", role: "menuitem", onClick: event => {
129
+ void onClueClick(event);
130
+ onNavigate?.();
131
+ }, direction: "row", spacing: 1, alignItems: "center", sx: theme => ({
132
+ width: '100%',
133
+ p: 1,
134
+ border: 0,
135
+ backgroundColor: 'transparent',
136
+ color: 'text.primary',
137
+ cursor: 'pointer',
138
+ font: 'inherit',
139
+ textAlign: 'left',
140
+ '&:hover': { backgroundColor: theme.palette.action.hover }
141
+ }), children: [_jsx(Icon, { fontSize: "1.5rem", icon: pivot.icon }), _jsx(Typography, { noWrap: true, children: pivot.label[i18n.language] })] }));
142
+ }
127
143
  return (_jsx(HowlerCard, { variant: compact ? 'outlined' : 'elevation', onClick: e => onClueClick(e), sx: [
128
144
  theme => ({
129
145
  backgroundColor: 'transparent',
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import CluePivot from './CluePivot';
7
+ const clueState = vi.hoisted(() => ({
8
+ actions: {},
9
+ executeAction: vi.fn(),
10
+ guessType: vi.fn()
11
+ }));
12
+ const showErrorMessage = vi.hoisted(() => vi.fn());
13
+ vi.mock('@cccsaurora/clue-ui', () => ({
14
+ useClueActionsSelector: (selector) => selector({ availableActions: clueState.actions, executeAction: clueState.executeAction }),
15
+ useClueEnrichSelector: (selector) => selector({ guessType: clueState.guessType })
16
+ }));
17
+ vi.mock('@iconify/react', () => ({
18
+ Icon: ({ icon }) => _jsx("span", { "data-testid": "pivot-icon", children: icon })
19
+ }));
20
+ vi.mock('components/elements/display/HowlerCard', () => ({
21
+ default: ({ children, onClick }) => (_jsx("div", { "data-testid": "howler-card", onClick: onClick, children: children }))
22
+ }));
23
+ vi.mock('components/hooks/useMySnackbar', () => ({
24
+ default: () => ({ showErrorMessage })
25
+ }));
26
+ vi.mock('react-i18next', () => ({
27
+ useTranslation: () => ({
28
+ i18n: { language: 'en' },
29
+ t: (key) => key
30
+ })
31
+ }));
32
+ const action = {
33
+ accept_multiple: false,
34
+ params: {
35
+ properties: {
36
+ selector: { type: 'string' },
37
+ note: { type: 'string' }
38
+ }
39
+ }
40
+ };
41
+ const pivot = (overrides = {}) => ({
42
+ value: 'enrich',
43
+ icon: 'material-symbols:bolt',
44
+ label: { en: 'Enrich', fr: 'Enrichir' },
45
+ mappings: [
46
+ { key: 'selector', field: 'howler.id' },
47
+ { key: 'note', field: 'custom', custom_value: 'from-pivot' }
48
+ ],
49
+ ...overrides
50
+ });
51
+ const hit = { howler: { id: 'hit-1' } };
52
+ const dossier = { dossier_id: 'dossier-1' };
53
+ const Wrapper = ({ children }) => (_jsx(ApiConfigContext.Provider, { value: {
54
+ config: {
55
+ indexes: { hit: { 'howler.id': {} } },
56
+ configuration: { mapping: {} }
57
+ },
58
+ setConfig: vi.fn(),
59
+ loaded: true
60
+ }, children: children }));
61
+ describe('CluePivot', () => {
62
+ beforeEach(() => {
63
+ clueState.actions = { enrich: action };
64
+ clueState.executeAction.mockReset();
65
+ clueState.executeAction.mockResolvedValue(undefined);
66
+ clueState.guessType.mockReset();
67
+ clueState.guessType.mockReturnValue('indicator');
68
+ showErrorMessage.mockReset();
69
+ });
70
+ it('renders a menu item and executes the mapped action without opening a window', async () => {
71
+ const user = userEvent.setup();
72
+ const onNavigate = vi.fn();
73
+ render(_jsx(Wrapper, { children: _jsx(CluePivot, { pivot: pivot(), hit: hit, dossier: dossier, variant: "menu-item", onNavigate: onNavigate }) }));
74
+ await user.click(screen.getByRole('menuitem', { name: /Enrich/ }));
75
+ expect(onNavigate).toHaveBeenCalledOnce();
76
+ await waitFor(() => {
77
+ expect(clueState.executeAction).toHaveBeenCalledWith('enrich', [{ type: 'indicator', value: 'hit-1' }], { note: 'from-pivot' }, { forceMenu: false });
78
+ });
79
+ });
80
+ it('passes forceMenu when the settings button is clicked on a card pivot', async () => {
81
+ const user = userEvent.setup();
82
+ render(_jsx(Wrapper, { children: _jsx(CluePivot, { pivot: pivot(), hit: hit, dossier: dossier, compact: true }) }));
83
+ await user.click(screen.getByRole('button', { name: '' }));
84
+ await waitFor(() => {
85
+ expect(clueState.executeAction).toHaveBeenCalledWith('enrich', [{ type: 'indicator', value: 'hit-1' }], { note: 'from-pivot' }, { forceMenu: true });
86
+ });
87
+ });
88
+ it('does not render a pivot when its action is unavailable', () => {
89
+ clueState.actions = {};
90
+ const { container } = render(_jsx(Wrapper, { children: _jsx(CluePivot, { pivot: pivot(), hit: hit, dossier: dossier }) }));
91
+ expect(container).toBeEmptyDOMElement();
92
+ expect(showErrorMessage).not.toHaveBeenCalled();
93
+ });
94
+ });
@@ -43,6 +43,7 @@ export declare enum StorageKey {
43
43
  GRID_COLUMN_WIDTHS = "grid_column_widths",
44
44
  QUERY_HISTORY = "query_history",
45
45
  LOGIN_NONCE = "login_nonce",
46
+ PIVOT_GROUP = "pivot_group",
46
47
  DISPLAY_TYPE = "display_type"
47
48
  }
48
49
  export declare const MOCK_SEARCH_QUERY_STORE = "howler.ui.mock_search_query_store";
@@ -48,6 +48,7 @@ export var StorageKey;
48
48
  StorageKey["GRID_COLUMN_WIDTHS"] = "grid_column_widths";
49
49
  StorageKey["QUERY_HISTORY"] = "query_history";
50
50
  StorageKey["LOGIN_NONCE"] = "login_nonce";
51
+ StorageKey["PIVOT_GROUP"] = "pivot_group";
51
52
  StorageKey["DISPLAY_TYPE"] = "display_type";
52
53
  })(StorageKey || (StorageKey = {}));
53
54
  export const MOCK_SEARCH_QUERY_STORE = `${MY_LOCAL_STORAGE_PREFIX}.${StorageKey.MOCK_SEARCH_QUERY_STORE}`;
@@ -1,4 +0,0 @@
1
- import type { Dossier } from '@cccsaurora/howler-ui/models/entities/generated/Dossier';
2
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
- declare const resolvePivotUrl: (pivot: NonNullable<Dossier['pivots']>[number], currentHit?: Hit) => string;
4
- export default resolvePivotUrl;
@@ -1,22 +0,0 @@
1
- import Handlebars from 'handlebars';
2
- import { flattenDeep } from '@cccsaurora/howler-ui/utils/utils';
3
- const resolvePivotUrl = (pivot, currentHit) => {
4
- const flatHit = flattenDeep(currentHit ?? {});
5
- const templateObject = Object.fromEntries((pivot.mappings ?? []).map(mapping => {
6
- const value = mapping.field === 'custom'
7
- ? mapping.custom_value
8
- : Array.isArray(flatHit[mapping.field])
9
- ? flatHit[mapping.field][0]
10
- : flatHit[mapping.field];
11
- return [mapping.key, value];
12
- }));
13
- try {
14
- return Handlebars.compile(pivot.value)(templateObject);
15
- }
16
- catch (e) {
17
- // eslint-disable-next-line no-console
18
- console.error(`Failed to compile pivot template for value "${pivot.value}":`, e);
19
- return pivot.value;
20
- }
21
- };
22
- export default resolvePivotUrl;
@@ -1,93 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ErrorOutline } from '@mui/icons-material';
3
- import { Tooltip } from '@mui/material';
4
- import { useHelpers } from '@cccsaurora/howler-ui/components/elements/display/handlebars/helpers';
5
- import HowlerCard from '@cccsaurora/howler-ui/components/elements/display/HowlerCard';
6
- import PivotTooltip from '@cccsaurora/howler-ui/components/elements/hit/PivotTooltip';
7
- import Handlebars from 'handlebars';
8
- import { isEmpty } from 'lodash-es';
9
- import React, { useMemo } from 'react';
10
- import { useTranslation } from 'react-i18next';
11
- import { usePluginStore } from 'react-pluggable';
12
- import { flattenDeep } from '@cccsaurora/howler-ui/utils/utils';
13
- import RelatedLink from './RelatedLink';
14
- const PivotLink = ({ pivot, hit, compact = false, dossier, resolvedUrl }) => {
15
- const { i18n } = useTranslation();
16
- const helpers = useHelpers({ async: false, components: false });
17
- const pluginStore = usePluginStore();
18
- const handlebars = useMemo(() => Handlebars.create(), []);
19
- const flatHit = useMemo(() => flattenDeep(hit ?? {}), [hit]);
20
- const href = useMemo(() => {
21
- if (!pivot || pivot.format !== 'link' || !flatHit || isEmpty(flatHit)) {
22
- return '';
23
- }
24
- const templateObject = Object.fromEntries((pivot.mappings ?? []).map(mapping => {
25
- const result = [mapping.key];
26
- if (mapping.field === 'custom') {
27
- result.push(mapping.custom_value);
28
- }
29
- else if (Array.isArray(flatHit[mapping.field])) {
30
- result.push(flatHit[mapping.field][0]);
31
- }
32
- else {
33
- result.push(flatHit[mapping.field]);
34
- }
35
- return result;
36
- }));
37
- helpers.forEach(helper => {
38
- if (handlebars.helpers[helper.keyword]) {
39
- return;
40
- }
41
- handlebars.registerHelper(helper.keyword, (...args) => {
42
- // eslint-disable-next-line no-console
43
- console.debug(`Running helper ${helper.keyword}`);
44
- return helper.callback?.(...args);
45
- });
46
- });
47
- try {
48
- return handlebars.compile(pivot.value)(templateObject);
49
- }
50
- catch (e) {
51
- // eslint-disable-next-line no-console
52
- console.error(`Failed to compile pivot template for value "${pivot.value}":`, e);
53
- return pivot.value;
54
- }
55
- }, [flatHit, pivot, handlebars, helpers]);
56
- if (href) {
57
- return (_jsx(RelatedLink, { title: pivot.label?.[i18n.language] ?? '', href: href, compact: compact, icon: pivot.icon, target: "_blank", rel: "noopener noreferrer", tooltip: _jsx(PivotTooltip, { dossier: dossier, resolvedUrl: resolvedUrl }) }));
58
- }
59
- // Hide a relatively useless console error, we'll show a UI component instead
60
- // eslint-disable-next-line no-console
61
- const oldError = console.error;
62
- let pluginPivot = null;
63
- try {
64
- // eslint-disable-next-line no-console
65
- console.error = () => { };
66
- pluginPivot = pluginStore.executeFunction(`pivot.${pivot.format}`, { pivot, hit, compact });
67
- }
68
- finally {
69
- // eslint-disable-next-line no-console
70
- console.error = oldError;
71
- }
72
- if (pluginPivot) {
73
- return pluginPivot;
74
- }
75
- return (_jsx(HowlerCard, { sx: [
76
- theme => ({
77
- p: 0.75,
78
- backgroundColor: 'transparent',
79
- transition: theme.transitions.create(['border-color']),
80
- '&:hover': { borderColor: 'error.main' }
81
- }),
82
- { border: 'thin solid', borderColor: 'transparent' }
83
- ], children: _jsx(Tooltip, { title: _jsxs(_Fragment, { children: [_jsx("span", { children: `Missing Pivot Implementation ${pivot.format}` }), _jsx("code", { children: _jsx("pre", { children: JSON.stringify(pivot, null, 4) }) })] }), slotProps: {
84
- popper: {
85
- sx: {
86
- '& > .MuiTooltip-tooltip': {
87
- maxWidth: '90vw !important'
88
- }
89
- }
90
- }
91
- }, children: _jsx(ErrorOutline, { color: "error" }) }) }));
92
- };
93
- export default PivotLink;