@cccsaurora/howler-ui 2.18.0-dev.676 → 2.18.0-dev.682

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 (230) hide show
  1. package/api/index.d.ts +2 -0
  2. package/api/index.js +4 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/index.d.ts +2 -1
  6. package/api/search/index.js +2 -1
  7. package/api/v2/case/index.d.ts +6 -0
  8. package/api/v2/case/index.js +18 -0
  9. package/api/v2/index.d.ts +4 -0
  10. package/api/v2/index.js +6 -0
  11. package/api/v2/search/facet.d.ts +3 -0
  12. package/api/v2/search/facet.js +12 -0
  13. package/api/v2/search/index.d.ts +5 -0
  14. package/api/v2/search/index.js +24 -0
  15. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  16. package/components/app/App.js +34 -7
  17. package/components/app/hooks/useMatchers.js +2 -2
  18. package/components/app/hooks/useMatchers.test.js +22 -22
  19. package/components/app/hooks/useTitle.js +3 -3
  20. package/components/app/providers/FavouritesProvider.js +2 -2
  21. package/components/app/providers/ParameterProvider.d.ts +9 -2
  22. package/components/app/providers/ParameterProvider.js +165 -240
  23. package/components/app/providers/ParameterProvider.test.js +307 -14
  24. package/components/app/providers/RecordProvider.d.ts +23 -0
  25. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  26. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  27. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  28. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  29. package/components/elements/ContextMenu.d.ts +56 -0
  30. package/components/elements/ContextMenu.js +109 -0
  31. package/components/elements/ContextMenu.test.js +215 -0
  32. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  33. package/components/elements/ObjectDetails.d.ts +6 -0
  34. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  35. package/components/elements/PluginTypography.d.ts +2 -1
  36. package/components/elements/PluginTypography.js +3 -2
  37. package/components/elements/UserList.d.ts +5 -2
  38. package/components/elements/UserList.js +14 -5
  39. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  40. package/components/elements/case/CaseCard.d.ts +8 -0
  41. package/components/elements/case/CaseCard.js +39 -0
  42. package/components/elements/case/CasePreview.d.ts +6 -0
  43. package/components/elements/case/CasePreview.js +17 -0
  44. package/components/elements/case/StatusIcon.d.ts +5 -0
  45. package/components/elements/case/StatusIcon.js +13 -0
  46. package/components/elements/display/ChipPopper.d.ts +1 -1
  47. package/components/elements/display/HowlerCard.js +1 -1
  48. package/components/elements/display/Modal.js +1 -0
  49. package/components/elements/hit/HitActions.js +4 -4
  50. package/components/elements/hit/HitBanner.js +28 -48
  51. package/components/elements/hit/HitCard.js +5 -5
  52. package/components/elements/hit/HitLabels.js +2 -2
  53. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  54. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  55. package/components/elements/hit/HitSummary.d.ts +2 -1
  56. package/components/elements/hit/HitSummary.js +6 -5
  57. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  58. package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
  59. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  60. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  61. package/components/elements/hit/related/RelatedRecords.js +63 -0
  62. package/components/elements/observable/ObservableCard.d.ts +6 -0
  63. package/components/elements/observable/ObservableCard.js +23 -0
  64. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  65. package/components/elements/observable/ObservablePreview.js +12 -0
  66. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  67. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  68. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  69. package/components/elements/record/RecordContextMenu.js +235 -0
  70. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  71. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +39 -39
  72. package/components/elements/record/RecordRelated.d.ts +7 -0
  73. package/components/elements/record/RecordRelated.js +34 -0
  74. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  75. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  76. package/components/elements/view/ViewTitle.js +1 -1
  77. package/components/hooks/useHitActions.d.ts +1 -1
  78. package/components/hooks/useHitActions.js +4 -4
  79. package/components/hooks/useMyPreferences.js +10 -1
  80. package/components/hooks/useMySearch.js +2 -2
  81. package/components/hooks/useMySitemap.js +4 -1
  82. package/components/hooks/useMyTheme.js +9 -2
  83. package/components/hooks/useParamState.test.js +3 -4
  84. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  85. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  86. package/components/hooks/useRelatedRecords.d.ts +13 -0
  87. package/components/hooks/useRelatedRecords.js +32 -0
  88. package/components/routes/action/edit/ActionEditor.js +2 -2
  89. package/components/routes/action/view/ActionSearch.js +1 -1
  90. package/components/routes/advanced/QueryBuilder.js +1 -1
  91. package/components/routes/advanced/QueryEditor.js +3 -3
  92. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  93. package/components/routes/analytics/AnalyticDetails.js +2 -2
  94. package/components/routes/analytics/AnalyticSearch.js +1 -1
  95. package/components/routes/cases/CaseViewer.d.ts +2 -0
  96. package/components/routes/cases/CaseViewer.js +22 -0
  97. package/components/routes/cases/Cases.d.ts +2 -0
  98. package/components/routes/cases/Cases.js +101 -0
  99. package/components/routes/cases/constants.d.ts +5 -0
  100. package/components/routes/cases/constants.js +5 -0
  101. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  102. package/components/routes/cases/detail/AlertPanel.js +33 -0
  103. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  104. package/components/routes/cases/detail/CaseAssets.js +101 -0
  105. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  106. package/components/routes/cases/detail/CaseAssets.test.js +163 -0
  107. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  108. package/components/routes/cases/detail/CaseDashboard.js +51 -0
  109. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  110. package/components/routes/cases/detail/CaseDetails.js +61 -0
  111. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  112. package/components/routes/cases/detail/CaseOverview.js +43 -0
  113. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  114. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  115. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  116. package/components/routes/cases/detail/CaseTask.js +57 -0
  117. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  118. package/components/routes/cases/detail/ItemPage.js +99 -0
  119. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  120. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  121. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  122. package/components/routes/cases/detail/TaskPanel.js +52 -0
  123. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  124. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  125. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  126. package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
  127. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  128. package/components/routes/cases/detail/assets/Asset.js +12 -0
  129. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  130. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  131. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +13 -0
  132. package/components/routes/cases/detail/sidebar/CaseFolder.js +131 -0
  133. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  134. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  135. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  136. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  137. package/components/routes/cases/hooks/useCase.js +38 -0
  138. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  139. package/components/routes/cases/modals/ResolveModal.js +59 -0
  140. package/components/routes/dossiers/DossierEditor.js +2 -2
  141. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  142. package/components/routes/help/ApiDocumentation.js +1 -1
  143. package/components/routes/help/HitBannerDocumentation.js +1 -0
  144. package/components/routes/help/HitDocumentation.js +1 -3
  145. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  146. package/components/routes/hits/search/InformationPane.js +47 -60
  147. package/components/routes/hits/search/LayoutSettings.js +3 -3
  148. package/components/routes/hits/search/QuerySettings.js +2 -1
  149. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  150. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  151. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  152. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  153. package/components/routes/hits/search/SearchPane.js +26 -49
  154. package/components/routes/hits/search/ViewLink.js +3 -3
  155. package/components/routes/hits/search/ViewLink.test.js +8 -8
  156. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  157. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  158. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  159. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  160. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  161. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  162. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  163. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  164. package/components/routes/hits/view/HitViewer.js +12 -13
  165. package/components/routes/home/ViewCard.js +4 -4
  166. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  167. package/components/routes/observables/ObservableViewer.js +27 -0
  168. package/components/routes/overviews/OverviewViewer.js +2 -2
  169. package/components/routes/views/ViewComposer.js +4 -4
  170. package/locales/en/translation.json +65 -3
  171. package/locales/fr/translation.json +63 -3
  172. package/models/WithMetadata.d.ts +2 -1
  173. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  174. package/models/entities/generated/Case.d.ts +28 -0
  175. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  176. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  177. package/models/entities/generated/EmailParent.d.ts +19 -0
  178. package/models/entities/generated/Enrichments.d.ts +7 -0
  179. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  180. package/models/entities/generated/Hit.d.ts +1 -0
  181. package/models/entities/generated/Howler.d.ts +0 -4
  182. package/models/entities/generated/HttpResponse.d.ts +11 -0
  183. package/models/entities/generated/Item.d.ts +9 -0
  184. package/models/entities/generated/Observable.d.ts +85 -0
  185. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  186. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  187. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  188. package/models/entities/generated/ObservableFile.d.ts +36 -0
  189. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  190. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  191. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  192. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  193. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  194. package/models/entities/generated/ObservableSource.d.ts +23 -0
  195. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  196. package/models/entities/generated/ObservableTls.d.ts +12 -0
  197. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  198. package/models/entities/generated/Rule.d.ts +2 -10
  199. package/models/entities/generated/Task.d.ts +10 -0
  200. package/models/entities/generated/Threat.d.ts +2 -2
  201. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  202. package/package.json +121 -104
  203. package/plugins/clue/components/ClueTypography.js +2 -2
  204. package/plugins/clue/utils.d.ts +2 -1
  205. package/tests/utils.d.ts +2 -0
  206. package/tests/utils.js +8 -0
  207. package/utils/constants.d.ts +3 -3
  208. package/utils/hitFunctions.d.ts +2 -1
  209. package/utils/hitFunctions.js +4 -4
  210. package/utils/typeUtils.d.ts +7 -0
  211. package/utils/typeUtils.js +27 -0
  212. package/components/app/providers/HitProvider.d.ts +0 -22
  213. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  214. package/components/elements/display/icons/BundleButton.js +0 -32
  215. package/components/elements/hit/HitRelated.d.ts +0 -6
  216. package/components/elements/hit/HitRelated.js +0 -7
  217. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  218. package/components/routes/help/BundleDocumentation.js +0 -12
  219. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  220. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  221. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  222. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  223. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  224. package/components/routes/hits/search/BundleScroller.js +0 -6
  225. package/components/routes/hits/search/HitContextMenu.js +0 -227
  226. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  227. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  228. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  229. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  230. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -10,12 +10,13 @@ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
10
10
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { useNavigate } from 'react-router-dom';
13
+ import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
13
14
  import { compareTimestamp, sortByTimestamp } from '@cccsaurora/howler-ui/utils/utils';
14
15
  import Comment from '../Comment';
15
16
  import HowlerAvatar from '../display/HowlerAvatar';
16
17
  import TypingIndicator from '../display/TypingIndicator';
17
18
  const MAX_LENGTH = 5000;
18
- const HitComments = ({ hit, users }) => {
19
+ const RecordComments = ({ record, users }) => {
19
20
  const { user } = useAppUser();
20
21
  const { t } = useTranslation();
21
22
  const navigate = useNavigate();
@@ -28,7 +29,7 @@ const HitComments = ({ hit, users }) => {
28
29
  const [length, setLength] = useState(0);
29
30
  const [analyticId, setAnalyticId] = useState();
30
31
  const [analyticComments, setAnalyticComments] = useState([]);
31
- const [comments, setComments] = useState(sortByTimestamp(hit?.howler?.comment));
32
+ const [comments, setComments] = useState(sortByTimestamp(record?.howler?.comment));
32
33
  const input = useRef();
33
34
  /**
34
35
  * Set the list of typers based on updates from the websocket
@@ -50,20 +51,20 @@ const HitComments = ({ hit, users }) => {
50
51
  // eslint-disable-next-line react-hooks/exhaustive-deps
51
52
  }, [handler]);
52
53
  useEffect(() => {
53
- if (hit?.howler?.analytic) {
54
- getMatchingAnalytic(hit).then(analytic => {
54
+ if (isHit(record) && record.howler.analytic) {
55
+ getMatchingAnalytic(record).then(analytic => {
55
56
  setAnalyticId(analytic?.analytic_id);
56
57
  setAnalyticComments(sortByTimestamp(analytic?.comment ?? []));
57
58
  });
58
59
  }
59
60
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
- }, [getMatchingAnalytic, hit?.howler?.analytic]);
61
+ }, [getMatchingAnalytic, record]);
61
62
  const onSubmit = useCallback(async () => {
62
- if (!input.current?.value || !hit || input.current.value.length > MAX_LENGTH)
63
+ if (!input.current?.value || !record || input.current.value.length > MAX_LENGTH)
63
64
  return;
64
65
  setLoading(true);
65
66
  try {
66
- const result = await dispatchApi(api.hit.comments.post(hit.howler.id, input.current.value), {
67
+ const result = await dispatchApi(api.hit.comments.post(record.howler.id, input.current.value), {
67
68
  showError: true,
68
69
  throwError: true,
69
70
  logError: false
@@ -75,23 +76,23 @@ const HitComments = ({ hit, users }) => {
75
76
  finally {
76
77
  setLoading(false);
77
78
  }
78
- }, [dispatchApi, hit]);
79
+ }, [dispatchApi, record]);
79
80
  /**
80
81
  * Emit a typing event when textbox is focused
81
82
  */
82
83
  const onFocus = useCallback(() => emit({
83
84
  broadcast: true,
84
85
  action: 'typing',
85
- id: hit?.howler?.id
86
- }), [emit, hit?.howler?.id]);
86
+ id: record?.howler?.id
87
+ }), [emit, record?.howler?.id]);
87
88
  /**
88
89
  * Emit a stop typing event when textbox is blurred
89
90
  */
90
91
  const onBlur = useCallback(() => emit({
91
92
  broadcast: true,
92
93
  action: 'stop_typing',
93
- id: hit?.howler?.id
94
- }), [emit, hit?.howler?.id]);
94
+ id: record?.howler?.id
95
+ }), [emit, record?.howler?.id]);
95
96
  const onClear = useCallback(() => {
96
97
  input.current.value = '';
97
98
  setShowClear(false);
@@ -108,16 +109,16 @@ const HitComments = ({ hit, users }) => {
108
109
  }, [loading, onSubmit]);
109
110
  const checkLength = useCallback(() => setLength(input.current?.value.length), []);
110
111
  const handleDelete = useCallback(async (commentId) => {
111
- await dispatchApi(api.hit.comments.del(hit.howler.id, [commentId]), { throwError: true, showError: true });
112
+ await dispatchApi(api.hit.comments.del(record.howler.id, [commentId]), { throwError: true, showError: true });
112
113
  setComments(comments.filter(cmt => cmt.id !== commentId));
113
- }, [comments, dispatchApi, hit?.howler?.id]);
114
+ }, [comments, dispatchApi, record?.howler?.id]);
114
115
  const handleEdit = useCallback(async (commentId, editValue) => {
115
- await dispatchApi(api.hit.comments.put(hit.howler.id, commentId, editValue), {
116
+ await dispatchApi(api.hit.comments.put(record.howler.id, commentId, editValue), {
116
117
  throwError: true,
117
118
  showError: true
118
119
  });
119
120
  setComments(comments.map(cmt => (cmt.id !== commentId ? cmt : { ...cmt, value: editValue })));
120
- }, [comments, dispatchApi, hit?.howler?.id]);
121
+ }, [comments, dispatchApi, record?.howler?.id]);
121
122
  const handleQuote = useCallback((value) => {
122
123
  if (input.current) {
123
124
  // We use trimStart here so there isn't a bunch of newlines at the beginning of the comment
@@ -135,27 +136,27 @@ const HitComments = ({ hit, users }) => {
135
136
  }, []);
136
137
  const handleReact = useCallback(async (commentId, type) => {
137
138
  if (type) {
138
- await dispatchApi(api.hit.comments.react.put(hit.howler.id, commentId, type));
139
+ await dispatchApi(api.hit.comments.react.put(record.howler.id, commentId, type));
139
140
  setComments(comments.map(cmt => cmt.id !== commentId ? cmt : { ...cmt, reactions: { ...(cmt?.reactions ?? {}), [user.username]: type } }));
140
141
  }
141
142
  else {
142
- await dispatchApi(api.hit.comments.react.del(hit.howler.id, commentId));
143
+ await dispatchApi(api.hit.comments.react.del(record.howler.id, commentId));
143
144
  setComments(comments.map(cmt => cmt.id !== commentId
144
145
  ? cmt
145
146
  : { ...cmt, reactions: { ...(cmt?.reactions ?? {}), [user.username]: undefined } }));
146
147
  }
147
- }, [comments, dispatchApi, hit?.howler.id, user.username]);
148
+ }, [comments, dispatchApi, record?.howler.id, user.username]);
148
149
  /**
149
150
  * Handle loading the comments when the hit changes
150
151
  */
151
152
  useEffect(() => {
152
- if (hit?.howler?.comment) {
153
- setComments(hit?.howler?.comment.slice().sort((a, b) => compareTimestamp(b.timestamp, a.timestamp)));
153
+ if (record?.howler?.comment) {
154
+ setComments(record?.howler?.comment.slice().sort((a, b) => compareTimestamp(b.timestamp, a.timestamp)));
154
155
  }
155
- else if (!hit) {
156
+ else if (!record) {
156
157
  setComments([]);
157
158
  }
158
- }, [hit]);
159
+ }, [record]);
159
160
  /**
160
161
  * This is the comments for the analytic associated with the hit. We show this at the start of the comment
161
162
  * list, as if they've been pinned
@@ -164,13 +165,13 @@ const HitComments = ({ hit, users }) => {
164
165
  if (analyticComments.length < 1) {
165
166
  return null;
166
167
  }
167
- const displayedComments = analyticComments.filter(c => !c.detection || c.detection === hit?.howler.detection);
168
+ const displayedComments = analyticComments.filter(c => !c.detection || c.detection === record?.howler.detection);
168
169
  return (_jsxs(Accordion, { variant: "outlined", children: [_jsx(AccordionSummary, { expandIcon: _jsx(KeyboardArrowDown, { fontSize: "small" }), children: _jsx(Typography, { variant: "body1", children: t('comments.analytic', { count: displayedComments.length }) }) }), _jsx(AccordionDetails, { children: _jsx(Stack, { spacing: 1, children: displayedComments.map(c => (_jsx(Comment, { comment: c, extra: _jsx(Chip, { size: "small", variant: "outlined", onClick: () => navigate('/analytics' +
169
170
  (analyticId
170
171
  ? `/${analyticId}?tab=comments` + (c.detection ? `&filter=${c.detection}` : '')
171
- : '')), sx: theme => ({ marginLeft: '0 !important', mr: `${theme.spacing(2)} !important` }), label: `${hit?.howler?.analytic ?? 'Analytic'}${c.detection ? ' - ' + c.detection : ''}` }), users: users }, c.id))) }) })] }));
172
- }, [analyticComments, analyticId, hit?.howler.analytic, hit?.howler.detection, navigate, t, users]);
172
+ : '')), sx: theme => ({ marginLeft: '0 !important', mr: `${theme.spacing(2)} !important` }), label: `${record?.howler?.analytic ?? 'Analytic'}${c.detection ? ' - ' + c.detection : ''}` }), users: users }, c.id))) }) })] }));
173
+ }, [analyticComments, analyticId, record?.howler.analytic, record?.howler.detection, navigate, t, users]);
173
174
  const renderedComments = useMemo(() => comments.map(c => (_jsx(Comment, { comment: c, users: users, handleDelete: () => handleDelete(c.id), handleEdit: value => handleEdit(c.id, value), handleReact: type => handleReact(c.id, type), handleQuote: () => handleQuote(c.value) }, c.id))), [comments, handleDelete, handleEdit, handleQuote, handleReact, users]);
174
- return (_jsxs(Stack, { sx: { py: 1, pr: 1 }, spacing: 1, children: [hit && renderedAnalyticComments, _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(HowlerAvatar, { userId: user.username }), _jsx(TextField, { inputProps: { sx: theme => ({ fontSize: theme.typography.body2.fontSize }) }, InputLabelProps: { shrink: false }, placeholder: t('comments.add'), onKeyDown: checkForSubmit, onChangeCapture: checkLength, inputRef: input, onFocus: onFocus, onBlur: onBlur, error: length > MAX_LENGTH, fullWidth: true, multiline: true })] }), _jsxs(Stack, { direction: "row", alignItems: "center", children: [typers.length > 0 && (_jsxs(_Fragment, { children: [_jsx(AvatarGroup, { componentsProps: { additionalAvatar: { sx: { height: 24, width: 24, fontSize: '12px' } } }, children: typers.map(typer => (_jsx(HowlerAvatar, { userId: typer, sx: { height: 24, width: 24 } }, typer))) }), _jsx(TypingIndicator, {})] })), _jsx(FlexOne, {}), length > 0.9 * MAX_LENGTH && (_jsxs(Typography, { variant: "caption", sx: [{ opacity: 0.7, mr: 1 }, length > MAX_LENGTH && { color: 'error.main' }], children: [length, "/", MAX_LENGTH] })), showClear && (_jsx(IconButton, { size: "small", onClick: onClear, disabled: loading, children: _jsx(Clear, { fontSize: "small" }) })), _jsx(IconButton, { size: "small", onClick: onSubmit, disabled: loading || length > MAX_LENGTH, children: _jsx(Send, { fontSize: "small" }) })] }), hit ? (renderedComments) : (_jsxs(_Fragment, { children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Skeleton, { width: 40, height: 40, variant: "circular" }), _jsx(Skeleton, { width: "100%", height: 80, variant: "rounded" })] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Skeleton, { width: 40, height: 40, variant: "circular" }), _jsx(Skeleton, { width: "100%", height: 100, variant: "rounded" })] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Skeleton, { width: 40, height: 40, variant: "circular" }), _jsx(Skeleton, { width: "100%", height: 80, variant: "rounded" })] })] }))] }));
175
+ return (_jsxs(Stack, { sx: { py: 1, pr: 1 }, spacing: 1, children: [record && renderedAnalyticComments, _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(HowlerAvatar, { userId: user.username }), _jsx(TextField, { inputProps: { sx: theme => ({ fontSize: theme.typography.body2.fontSize }) }, InputLabelProps: { shrink: false }, placeholder: t('comments.add'), onKeyDown: checkForSubmit, onChangeCapture: checkLength, inputRef: input, onFocus: onFocus, onBlur: onBlur, error: length > MAX_LENGTH, fullWidth: true, multiline: true })] }), _jsxs(Stack, { direction: "row", alignItems: "center", children: [typers.length > 0 && (_jsxs(_Fragment, { children: [_jsx(AvatarGroup, { componentsProps: { additionalAvatar: { sx: { height: 24, width: 24, fontSize: '12px' } } }, children: typers.map(typer => (_jsx(HowlerAvatar, { userId: typer, sx: { height: 24, width: 24 } }, typer))) }), _jsx(TypingIndicator, {})] })), _jsx(FlexOne, {}), length > 0.9 * MAX_LENGTH && (_jsxs(Typography, { variant: "caption", sx: [{ opacity: 0.7, mr: 1 }, length > MAX_LENGTH && { color: 'error.main' }], children: [length, "/", MAX_LENGTH] })), showClear && (_jsx(IconButton, { size: "small", onClick: onClear, disabled: loading, children: _jsx(Clear, { fontSize: "small" }) })), _jsx(IconButton, { size: "small", onClick: onSubmit, disabled: loading || length > MAX_LENGTH, children: _jsx(Send, { fontSize: "small" }) })] }), record ? (renderedComments) : (_jsxs(_Fragment, { children: [_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Skeleton, { width: 40, height: 40, variant: "circular" }), _jsx(Skeleton, { width: "100%", height: 80, variant: "rounded" })] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Skeleton, { width: 40, height: 40, variant: "circular" }), _jsx(Skeleton, { width: "100%", height: 100, variant: "rounded" })] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Skeleton, { width: 40, height: 40, variant: "circular" }), _jsx(Skeleton, { width: "100%", height: 80, variant: "rounded" })] })] }))] }));
175
176
  };
176
- export default HitComments;
177
+ export default RecordComments;
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  /**
4
4
  * Props for the HitContextMenu component
5
5
  */
6
- interface HitContextMenuProps {
6
+ interface RecordContextMenuProps {
7
7
  /**
8
8
  * Function to extract the hit ID from a mouse event
9
9
  */
@@ -18,5 +18,5 @@ interface HitContextMenuProps {
18
18
  * Provides quick access to common hit actions including assessment, voting,
19
19
  * transitions, and exclusion filters based on template fields.
20
20
  */
21
- declare const HitContextMenu: FC<PropsWithChildren<HitContextMenuProps>>;
22
- export default HitContextMenu;
21
+ declare const RecordContextMenu: FC<PropsWithChildren<RecordContextMenuProps>>;
22
+ export default RecordContextMenu;
@@ -0,0 +1,235 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { AddCircleOutline, Assignment, Edit, HowToVote, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
3
+ import api from '@cccsaurora/howler-ui/api';
4
+ import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
+ import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
+ import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
7
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
8
+ import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
9
+ import { TOP_ROW, VOTE_OPTIONS } from '@cccsaurora/howler-ui/components/elements/hit/actions/SharedComponents';
10
+ import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
11
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
12
+ import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
13
+ import { capitalize, get, groupBy, isEmpty, toString } from 'lodash-es';
14
+ import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
15
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
16
+ import { useTranslation } from 'react-i18next';
17
+ import { usePluginStore } from 'react-pluggable';
18
+ import { useContextSelector } from 'use-context-selector';
19
+ import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
20
+ import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
21
+ import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
22
+ /**
23
+ * Order in which action types should be displayed in the context menu
24
+ */
25
+ const ORDER = ['assessment', 'vote', 'action'];
26
+ /**
27
+ * Icon mapping for different action types
28
+ */
29
+ const ICON_MAP = {
30
+ assessment: _jsx(Assignment, {}),
31
+ vote: _jsx(HowToVote, {}),
32
+ action: _jsx(Edit, {})
33
+ };
34
+ /**
35
+ * Context menu component for hit operations.
36
+ * Provides quick access to common hit actions including assessment, voting,
37
+ * transitions, and exclusion filters based on template fields.
38
+ */
39
+ const RecordContextMenu = ({ children, getSelectedId, Component }) => {
40
+ const { t } = useTranslation();
41
+ const { dispatchApi } = useMyApi();
42
+ const { executeAction } = useMyActionFunctions();
43
+ const { config } = useContext(ApiConfigContext);
44
+ const pluginStore = usePluginStore();
45
+ const { getMatchingAnalytic, getMatchingTemplate } = useMatchers();
46
+ const query = useContextSelector(ParameterContext, ctx => ctx?.query);
47
+ const setQuery = useContextSelector(ParameterContext, ctx => ctx?.setQuery);
48
+ const [id, setId] = useState(null);
49
+ const record = useContextSelector(RecordContext, ctx => ctx.records[id]);
50
+ const selectedRecords = useContextSelector(RecordContext, ctx => ctx.selectedRecords);
51
+ const [analytic, setAnalytic] = useState(null);
52
+ const [template, setTemplate] = useState(null);
53
+ const [actions, setActions] = useState([]);
54
+ const records = useMemo(() => selectedRecords.some(_record => _record.howler.id === record?.howler.id)
55
+ ? selectedRecords
56
+ : record
57
+ ? [record]
58
+ : [], [record, selectedRecords]);
59
+ const hits = useMemo(() => records.filter(isHit), [records]);
60
+ const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
61
+ /**
62
+ * Called by ContextMenu after the menu is positioned and opened.
63
+ * Identifies the clicked record and fetches available actions.
64
+ */
65
+ const onOpen = useCallback(async (event) => {
66
+ const _id = getSelectedId(event);
67
+ setId(_id);
68
+ const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*' }), { throwError: false }))
69
+ ?.items;
70
+ if (_actions) {
71
+ setActions(_actions);
72
+ }
73
+ }, [dispatchApi, getSelectedId]);
74
+ const rowStatus = useMemo(() => ({
75
+ assessment: canAssess,
76
+ vote: canVote
77
+ }), [canAssess, canVote]);
78
+ const pluginActions = howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.actions`, records));
79
+ /**
80
+ * Generates grouped action entries for the context menu.
81
+ * Combines transitions, plugin actions, votes, and assessments based on permissions.
82
+ */
83
+ const entries = useMemo(() => {
84
+ let _actions = [...availableTransitions, ...pluginActions];
85
+ if (canVote) {
86
+ _actions = [
87
+ ..._actions,
88
+ ...VOTE_OPTIONS.map(option => ({ ...option, actionFunction: () => vote(option.name.toLowerCase()) }))
89
+ ];
90
+ }
91
+ if (config.lookups?.['howler.assessment'] && canAssess) {
92
+ _actions = [
93
+ ..._actions,
94
+ ...config.lookups['howler.assessment']
95
+ .filter(_assessment => analytic?.triage_settings?.valid_assessments
96
+ ? analytic.triage_settings?.valid_assessments.includes(_assessment)
97
+ : true)
98
+ .sort((a, b) => +TOP_ROW.includes(b) - +TOP_ROW.includes(a))
99
+ .map(assessment => ({
100
+ type: 'assessment',
101
+ name: assessment,
102
+ actionFunction: async () => {
103
+ await assess(assessment, analytic?.triage_settings?.skip_rationale);
104
+ }
105
+ }))
106
+ ];
107
+ }
108
+ return Object.entries(groupBy(_actions, 'type')).sort(([a], [b]) => ORDER.indexOf(a) - ORDER.indexOf(b));
109
+ }, [analytic, assess, availableTransitions, canAssess, canVote, config.lookups, vote, pluginActions]);
110
+ // Load analytic and template data when a hit is selected
111
+ useEffect(() => {
112
+ if (!record?.howler.analytic) {
113
+ return;
114
+ }
115
+ getMatchingAnalytic(record).then(setAnalytic);
116
+ getMatchingTemplate(record).then(setTemplate);
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [record]);
119
+ /**
120
+ * Builds the declarative items structure for the ContextMenu component.
121
+ */
122
+ const items = useMemo(() => {
123
+ const result = [
124
+ {
125
+ kind: 'item',
126
+ id: 'open-record',
127
+ icon: _jsx(OpenInNew, {}),
128
+ label: t(`${record?.__index ?? 'hit'}.open`),
129
+ disabled: !record,
130
+ to: `/${record?.__index}s/${record?.howler.id}`
131
+ }
132
+ ];
133
+ if (isHit(record)) {
134
+ result.push({
135
+ kind: 'item',
136
+ id: 'open-analytic',
137
+ icon: _jsx(QueryStats, {}),
138
+ label: t('analytic.open'),
139
+ disabled: !analytic,
140
+ to: `/analytics/${analytic?.analytic_id}`
141
+ });
142
+ result.push({ kind: 'divider', id: 'actions-divider' });
143
+ for (const [type, typeItems] of entries) {
144
+ result.push({
145
+ kind: 'submenu',
146
+ id: type,
147
+ icon: ICON_MAP[type] ?? _jsx(Terminal, {}),
148
+ label: t(`hit.details.actions.${type}`),
149
+ disabled: rowStatus[type] === false,
150
+ items: typeItems.map(a => ({
151
+ key: a.name,
152
+ label: a.i18nKey ? t(a.i18nKey) : capitalize(a.name),
153
+ onClick: a.actionFunction
154
+ }))
155
+ });
156
+ }
157
+ result.push({
158
+ kind: 'submenu',
159
+ id: 'actions',
160
+ icon: _jsx(SettingsSuggest, {}),
161
+ label: t('route.actions.change'),
162
+ disabled: actions.length < 1,
163
+ items: actions.map(action => ({
164
+ key: action.action_id,
165
+ label: action.name,
166
+ onClick: () => executeAction(action.action_id, `howler.id:${record?.howler.id}`)
167
+ }))
168
+ });
169
+ if (!isEmpty(template?.keys ?? []) && setQuery) {
170
+ result.push({ kind: 'divider', id: 'filter-divider' });
171
+ result.push({
172
+ kind: 'submenu',
173
+ id: 'excludes',
174
+ icon: _jsx(RemoveCircleOutline, {}),
175
+ label: t('hit.panel.exclude'),
176
+ items: (template?.keys ?? []).flatMap(key => {
177
+ let newQuery = '';
178
+ if (query !== DEFAULT_QUERY) {
179
+ newQuery = `(${query}) AND `;
180
+ }
181
+ const value = get(record, key);
182
+ if (!value) {
183
+ return [];
184
+ }
185
+ else if (Array.isArray(value)) {
186
+ const sanitizedValues = value
187
+ .map(toString)
188
+ .filter(val => !!val)
189
+ .map(val => `"${sanitizeLuceneQuery(val)}"`);
190
+ if (sanitizedValues.length < 1) {
191
+ return [];
192
+ }
193
+ newQuery += `-${key}:(${sanitizedValues.join(' OR ')})`;
194
+ }
195
+ else {
196
+ newQuery += `-${key}:"${sanitizeLuceneQuery(value.toString())}"`;
197
+ }
198
+ return [{ key, label: key, onClick: () => setQuery(newQuery) }];
199
+ })
200
+ });
201
+ result.push({
202
+ kind: 'submenu',
203
+ id: 'includes',
204
+ icon: _jsx(AddCircleOutline, {}),
205
+ label: t('hit.panel.include'),
206
+ items: (template?.keys ?? []).flatMap(key => {
207
+ let newQuery = `(${query}) AND `;
208
+ const value = get(record, key);
209
+ if (!value) {
210
+ return [];
211
+ }
212
+ else if (Array.isArray(value)) {
213
+ const sanitizedValues = value
214
+ .map(toString)
215
+ .filter(val => !!val)
216
+ .map(val => `"${sanitizeLuceneQuery(val)}"`);
217
+ if (sanitizedValues.length < 1) {
218
+ return [];
219
+ }
220
+ newQuery += `${key}:(${sanitizedValues.join(' OR ')})`;
221
+ }
222
+ else {
223
+ newQuery += `${key}:"${sanitizeLuceneQuery(value.toString())}"`;
224
+ }
225
+ return [{ key, label: key, onClick: () => setQuery(newQuery) }];
226
+ })
227
+ });
228
+ }
229
+ }
230
+ return result;
231
+ // eslint-disable-next-line react-hooks/exhaustive-deps
232
+ }, [record, analytic, template, entries, rowStatus, actions, query, t, setQuery, executeAction]);
233
+ return (_jsx(ContextMenu, { id: "contextMenu", Component: Component, onOpen: onOpen, onClose: () => setAnalytic(null), items: items, children: children }));
234
+ };
235
+ export default RecordContextMenu;