@cccsaurora/howler-ui 2.19.0-dev.1260 → 2.19.0-dev.1266

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.
@@ -7,7 +7,17 @@ import { useTranslation } from 'react-i18next';
7
7
  import Throttler from '@cccsaurora/howler-ui/utils/Throttler';
8
8
  import { hashCode } from '@cccsaurora/howler-ui/utils/utils';
9
9
  import Markdown, {} from '../display/Markdown';
10
- import { useHelpers } from './handlebars/helpers';
10
+ import { HowlerHelperError, useHelpers } from './handlebars/helpers';
11
+ class HowlerHandlebarsRenderError extends Error {
12
+ helper;
13
+ hint;
14
+ constructor(message, helper, hint) {
15
+ super(Handlebars.escapeExpression(message));
16
+ this.name = 'HowlerHandlebarsRenderError';
17
+ this.helper = Handlebars.escapeExpression(helper);
18
+ this.hint = hint ? Handlebars.escapeExpression(hint) : undefined;
19
+ }
20
+ }
11
21
  const THROTTLER = new Throttler(500);
12
22
  const HandlebarsMarkdown = ({ md, object = {}, disableLinks = false }) => {
13
23
  const { t } = useTranslation();
@@ -48,7 +58,23 @@ const HandlebarsMarkdown = ({ md, object = {}, disableLinks = false }) => {
48
58
  }
49
59
  return new Handlebars.SafeString(`\`${id}\``);
50
60
  }
51
- return helper.callback(...args);
61
+ try {
62
+ const result = helper.callback(...args);
63
+ return result instanceof Promise
64
+ ? result.catch(err => {
65
+ if (err instanceof HowlerHelperError) {
66
+ throw new HowlerHandlebarsRenderError(err.message, helper.keyword, helper.hint);
67
+ }
68
+ throw err;
69
+ })
70
+ : result;
71
+ }
72
+ catch (err) {
73
+ if (err instanceof HowlerHelperError) {
74
+ throw new HowlerHandlebarsRenderError(err.message, helper.keyword, helper.hint);
75
+ }
76
+ throw err;
77
+ }
52
78
  });
53
79
  });
54
80
  }, [handlebars, helpers, mdComponents]);
@@ -65,6 +91,14 @@ const HandlebarsMarkdown = ({ md, object = {}, disableLinks = false }) => {
65
91
  setRendered(await compiled(object));
66
92
  return;
67
93
  }
94
+ if (err instanceof HowlerHandlebarsRenderError) {
95
+ setRendered(`
96
+ <h2 style="color: red">${t('markdown.error')}</h2>
97
+ <span style="color: red; font-weight: bold; font-family: monospace;">Invalid Usage [${err.helper}]: ${err.message}</span>
98
+ ${err.hint ? `<br/><span style="color: gray; font-family: monospace;">${err.hint}</span>` : ''}
99
+ `);
100
+ return;
101
+ }
68
102
  // eslint-disable-next-line no-console
69
103
  console.error(err);
70
104
  setRendered(`
@@ -7,9 +7,13 @@ export interface HowlerHelper {
7
7
  fr: string;
8
8
  };
9
9
  async?: boolean;
10
+ hint?: string;
10
11
  callback?: Handlebars.HelperDelegate;
11
12
  componentCallback?: (...args: any[]) => ReactElement | Promise<ReactElement>;
12
13
  }
14
+ export declare class HowlerHelperError extends Error {
15
+ constructor(message: string);
16
+ }
13
17
  export declare const useHelpers: (opts?: {
14
18
  async: boolean;
15
19
  components: boolean;
@@ -6,12 +6,18 @@ import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
6
6
  import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
7
7
  import { flatten } from 'flat';
8
8
  import Handlebars from 'handlebars';
9
- import { capitalize, get, groupBy, isObject } from 'lodash-es';
9
+ import { capitalize, get, groupBy, isNil, isObject } from 'lodash-es';
10
10
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
11
11
  import { useMemo } from 'react';
12
12
  import { usePluginStore } from 'react-pluggable';
13
13
  import ActionButton from '../ActionButton';
14
14
  import JSONViewer from '../json/JSONViewer';
15
+ export class HowlerHelperError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'HowlerHelperError';
19
+ }
20
+ }
15
21
  const FETCH_RESULTS = {};
16
22
  export const useHelpers = (opts = { async: true, components: true }) => {
17
23
  const pluginStore = usePluginStore();
@@ -22,7 +28,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
22
28
  en: 'Checks the equality of the string representation of the two arguments.',
23
29
  fr: "Vérifie l'égalité de la représentation en chaîne de caractères des deux arguments."
24
30
  },
25
- callback: (arg1, arg2) => arg1?.toString() === arg2.toString()
31
+ callback: (...args) => {
32
+ args.pop(); // remove options
33
+ const [arg1, arg2] = args;
34
+ if (isNil(arg1) || isNil(arg2)) {
35
+ throw new HowlerHelperError('Both arguments must be provided.');
36
+ }
37
+ return arg1.toString() === arg2.toString();
38
+ },
39
+ hint: 'Usage: {{equals arg1 arg2}}'
26
40
  },
27
41
  {
28
42
  keyword: 'and',
@@ -30,7 +44,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
30
44
  en: 'Runs the comparison `arg1 && arg2`, and returns the result.',
31
45
  fr: 'Exécute la comparaison `arg1 && arg2`, et retourne le résultat.'
32
46
  },
33
- callback: (arg1, arg2) => arg1 && arg2
47
+ callback: (...args) => {
48
+ args.pop(); // remove options
49
+ const [arg1, arg2] = args;
50
+ return arg1 && arg2;
51
+ },
52
+ hint: 'Usage: {{and arg1 arg2}}'
34
53
  },
35
54
  {
36
55
  keyword: 'or',
@@ -38,7 +57,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
38
57
  en: 'Runs the comparison `arg1 || arg2`, and returns the result.',
39
58
  fr: 'Exécute la comparaison `arg1 || arg2`, et retourne le résultat.'
40
59
  },
41
- callback: (arg1, arg2) => arg1 || arg2
60
+ callback: (...args) => {
61
+ args.pop(); // remove options
62
+ const [arg1, arg2] = args;
63
+ return arg1 || arg2;
64
+ },
65
+ hint: 'Usage: {{or arg1 arg2}}'
42
66
  },
43
67
  {
44
68
  keyword: 'not',
@@ -46,7 +70,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
46
70
  en: 'Runs the comparison `!arg`, and returns the result.',
47
71
  fr: 'Exécute la comparaison `!arg`, et retourne le résultat.'
48
72
  },
49
- callback: arg => !arg
73
+ callback: (...args) => {
74
+ args.pop(); // remove options
75
+ const arg = args[0];
76
+ return !arg;
77
+ },
78
+ hint: 'Usage: {{not arg}}'
50
79
  },
51
80
  {
52
81
  keyword: 'curly',
@@ -54,7 +83,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
54
83
  en: 'Wraps the given argument in curly braces.',
55
84
  fr: "Entoure l'argument donné d'accolades."
56
85
  },
57
- callback: arg1 => new Handlebars.SafeString(`{{${arg1}}}`)
86
+ callback: (...args) => {
87
+ args.pop(); // remove options
88
+ const arg1 = args[0];
89
+ return new Handlebars.SafeString(`{{${arg1}}}`);
90
+ },
91
+ hint: 'Usage: {{curly arg}}'
58
92
  },
59
93
  {
60
94
  keyword: 'join',
@@ -62,7 +96,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
62
96
  en: 'Joins two string arguments with a given string `sep`, or the empty string as a default.',
63
97
  fr: 'Joint deux arguments de chaîne avec une chaîne donnée `sep`, ou la chaîne vide par défaut.'
64
98
  },
65
- callback: (arg1, arg2, context) => [arg1?.toString() ?? '', arg2?.toString() ?? ''].join(context.hash?.sep ?? '')
99
+ callback: (...args) => {
100
+ const context = args.pop();
101
+ const [arg1, arg2] = args;
102
+ return [arg1?.toString() ?? '', arg2?.toString() ?? ''].join(context.hash?.sep ?? '');
103
+ },
104
+ hint: 'Usage: {{join arg1 arg2 sep=string}}'
66
105
  },
67
106
  {
68
107
  keyword: 'upper',
@@ -70,7 +109,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
70
109
  en: 'Returns the uppercase representation of a string argument.',
71
110
  fr: "Retourne la représentation en majuscules d'un argument de chaîne."
72
111
  },
73
- callback: (val) => val.toLocaleUpperCase()
112
+ callback: (...args) => {
113
+ args.pop(); // remove options
114
+ const val = args[0];
115
+ if (isNil(val)) {
116
+ throw new HowlerHelperError('Upper expects a string argument');
117
+ }
118
+ return val.toString().toLocaleUpperCase();
119
+ },
120
+ hint: 'Usage: {{upper val}}'
74
121
  },
75
122
  {
76
123
  keyword: 'lower',
@@ -78,7 +125,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
78
125
  en: 'Returns the lowercase representation of a string argument.',
79
126
  fr: "Retourne la représentation en minuscules d'un argument de chaîne."
80
127
  },
81
- callback: (val) => val.toLocaleLowerCase()
128
+ callback: (...args) => {
129
+ args.pop(); // remove options
130
+ const val = args[0];
131
+ if (isNil(val)) {
132
+ throw new HowlerHelperError('Lower expects a string argument');
133
+ }
134
+ return val.toString().toLocaleLowerCase();
135
+ },
136
+ hint: 'Usage: {{lower val}}'
82
137
  },
83
138
  {
84
139
  keyword: 'fetch',
@@ -87,7 +142,9 @@ export const useHelpers = (opts = { async: true, components: true }) => {
87
142
  fr: "Récupère l'URL fournie et retourne la clé donnée (aplatie) de l'objet JSON retourné. Notez que le résultat doit être du JSON !"
88
143
  },
89
144
  async: true,
90
- callback: async (url, key) => {
145
+ callback: async (...args) => {
146
+ args.pop(); // remove options
147
+ const [url, key] = args;
91
148
  try {
92
149
  if (!FETCH_RESULTS[url]) {
93
150
  FETCH_RESULTS[url] = fetch(url).then(res => res.json());
@@ -98,7 +155,8 @@ export const useHelpers = (opts = { async: true, components: true }) => {
98
155
  catch (e) {
99
156
  return '';
100
157
  }
101
- }
158
+ },
159
+ hint: 'Usage: {{fetch url key}}'
102
160
  },
103
161
  {
104
162
  keyword: 'howler',
@@ -106,12 +164,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
106
164
  en: 'Given a howler hit ID, this helper renders a hit card for that ID.',
107
165
  fr: 'Étant donné un ID de résultat howler, cet assistant affiche une carte de résultat pour cet ID.'
108
166
  },
109
- componentCallback: id => {
167
+ componentCallback: (...args) => {
168
+ args.pop(); // remove options
169
+ const id = args[0];
110
170
  if (!id) {
111
171
  return _jsx(AppListEmpty, {});
112
172
  }
113
173
  return _jsx(HitCard, { id: id, layout: HitLayout.NORMAL });
114
- }
174
+ },
175
+ hint: 'Usage: {{howler hitId}}'
115
176
  },
116
177
  {
117
178
  keyword: 'entries',
@@ -119,12 +180,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
119
180
  en: 'Given a dict, return an array of {key, value} objects.',
120
181
  fr: "Étant donné un dictionnaire, retourne un tableau d'objets {key, value}."
121
182
  },
122
- callback: obj => {
183
+ callback: (...args) => {
184
+ args.pop(); // remove options
185
+ const obj = args[0];
123
186
  if (!isObject(obj)) {
124
187
  return new Handlebars.SafeString('Invalid Object.');
125
188
  }
126
189
  return Object.entries(obj).map(([key, value]) => ({ key, value }));
127
- }
190
+ },
191
+ hint: 'Usage: {{entries obj}}'
128
192
  },
129
193
  {
130
194
  keyword: 'render_json',
@@ -132,12 +196,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
132
196
  en: 'Given JSON data, this helper renders a JSON viewer component.',
133
197
  fr: 'Étant donné des données JSON, cet assistant affiche un composant de visualisation JSON.'
134
198
  },
135
- componentCallback: data => {
199
+ componentCallback: (...args) => {
200
+ args.pop(); // remove options
201
+ const data = args[0];
136
202
  if (!data) {
137
203
  return _jsx(AppListEmpty, {});
138
204
  }
139
205
  return _jsx(JSONViewer, { data: data });
140
- }
206
+ },
207
+ hint: 'Usage: {{render_json obj}}'
141
208
  },
142
209
  {
143
210
  keyword: 'to_json',
@@ -145,9 +212,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
145
212
  en: 'Convert any object into a JSON string.',
146
213
  fr: "Convertit n'importe quel objet en chaîne JSON."
147
214
  },
148
- callback: obj => {
215
+ callback: (...args) => {
216
+ args.pop(); // remove options
217
+ const obj = args[0];
149
218
  return new Handlebars.SafeString(JSON.stringify(obj));
150
- }
219
+ },
220
+ hint: 'Usage: {{to_json obj}}'
151
221
  },
152
222
  {
153
223
  keyword: 'parse_json',
@@ -155,9 +225,20 @@ export const useHelpers = (opts = { async: true, components: true }) => {
155
225
  en: 'Convert a JSON string into an object.',
156
226
  fr: 'Convertit une chaîne JSON en objet.'
157
227
  },
158
- callback: str => {
159
- return JSON.parse(str);
160
- }
228
+ callback: (...args) => {
229
+ args.pop(); // remove options
230
+ const str = args[0];
231
+ if (isNil(str)) {
232
+ throw new HowlerHelperError('Parse JSON expects a string argument');
233
+ }
234
+ try {
235
+ return JSON.parse(str);
236
+ }
237
+ catch (e) {
238
+ throw new HowlerHelperError('Invalid JSON string');
239
+ }
240
+ },
241
+ hint: 'Usage: {{parse_json str}}'
161
242
  },
162
243
  {
163
244
  keyword: 'get',
@@ -165,14 +246,17 @@ export const useHelpers = (opts = { async: true, components: true }) => {
165
246
  en: 'Returns the given (flattened) key from the provided object.',
166
247
  fr: "Retourne la clé donnée (aplatie) de l'objet fourni."
167
248
  },
168
- callback: (data, key) => {
249
+ callback: (...args) => {
250
+ args.pop(); // remove options
251
+ const [data, key] = args;
169
252
  try {
170
253
  return get(data, key);
171
254
  }
172
255
  catch (e) {
173
256
  return '';
174
257
  }
175
- }
258
+ },
259
+ hint: 'Usage: {{get obj key}}'
176
260
  },
177
261
  {
178
262
  keyword: 'includes',
@@ -180,9 +264,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
180
264
  en: 'Checks if field is in string',
181
265
  fr: 'Vérifie si le champ est dans la chaîne'
182
266
  },
183
- callback: (arg1, arg2) => {
267
+ callback: (...args) => {
268
+ args.pop(); // remove options
269
+ const [arg1, arg2] = args;
184
270
  return !!arg2 && !!arg1?.includes(arg2);
185
- }
271
+ },
272
+ hint: 'Usage: {{includes str substr}}'
186
273
  },
187
274
  {
188
275
  keyword: 'table',
@@ -190,7 +277,9 @@ export const useHelpers = (opts = { async: true, components: true }) => {
190
277
  en: 'Render a table in markdown given an array of cells',
191
278
  fr: "Affiche un tableau en markdown à partir d'un tableau de cellules"
192
279
  },
193
- componentCallback: (cells) => {
280
+ componentCallback: (...args) => {
281
+ args.pop(); // remove options
282
+ const cells = args[0];
194
283
  const columns = Object.keys(groupBy(cells, 'column'));
195
284
  const rows = groupBy(cells, 'row');
196
285
  return (_jsx(Paper, { sx: { width: '95%', overflowX: 'auto', m: 1 }, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsx(TableRow, { children: columns.map(col => (_jsx(TableCell, { sx: { maxWidth: '150px' }, children: col
@@ -202,7 +291,8 @@ export const useHelpers = (opts = { async: true, components: true }) => {
202
291
  return _jsx(TableCell, { children: cell?.value ?? 'N/A' }, col + cell?.value);
203
292
  }) }, rowId));
204
293
  }) })] }) }));
205
- }
294
+ },
295
+ hint: 'Usage: {{table cells}}'
206
296
  },
207
297
  {
208
298
  keyword: 'action',
@@ -210,9 +300,12 @@ export const useHelpers = (opts = { async: true, components: true }) => {
210
300
  en: 'Execute a howler action given a specific action ID (from the URL when viewing the action, i.e. yaIKVqiKhWpyCsWdqsE4D)',
211
301
  fr: "Exécute une action howler à partir d'un ID d'action spécifique (de l'URL lors de la visualisation de l'action, par ex. yaIKVqiKhWpyCsWdqsE4D)"
212
302
  },
213
- componentCallback: (actionId, hitId, context) => {
303
+ componentCallback: (...args) => {
304
+ const context = args.pop(); // remove options
305
+ const [actionId, hitId] = args;
214
306
  return _jsx(ActionButton, { actionId: actionId, hitId: hitId, ...(context.hash ?? {}) });
215
- }
307
+ },
308
+ hint: 'Usage: {{action actionId hitId}}'
216
309
  },
217
310
  {
218
311
  keyword: 'replace',
@@ -220,9 +313,15 @@ export const useHelpers = (opts = { async: true, components: true }) => {
220
313
  en: '',
221
314
  fr: ''
222
315
  },
223
- callback: (str, searchValue, replaceValue) => {
224
- return str.replaceAll(searchValue ?? '', replaceValue ?? '');
225
- }
316
+ callback: (...args) => {
317
+ args.pop(); // remove options
318
+ const [str, searchValue, replaceValue] = args;
319
+ if (isNil(str) || isNil(searchValue) || isNil(replaceValue)) {
320
+ throw new HowlerHelperError('Replace expects three arguments');
321
+ }
322
+ return str.toString().replaceAll(searchValue ?? '', replaceValue ?? '');
323
+ },
324
+ hint: 'Usage: {{replace str searchValue replaceValue}}'
226
325
  },
227
326
  ...howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.helpers`))
228
327
  ].filter((entry) => (opts.async || !entry.async) && (opts.components || !entry.componentCallback)), [opts.async, opts.components, pluginStore]);
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "dependencies": {
15
- "@cccsaurora/clue-ui": "1.2.8",
15
+ "@cccsaurora/clue-ui": "1.3.0",
16
16
  "@dnd-kit/core": "^6.3.1",
17
17
  "@dnd-kit/modifiers": "^7.0.0",
18
18
  "@dnd-kit/sortable": "^8.0.0",
@@ -34,9 +34,9 @@
34
34
  "chartjs-adapter-dayjs-4": "^1.0.4",
35
35
  "chartjs-plugin-zoom": "^2.2.0",
36
36
  "dayjs": "^1.11.21",
37
- "dompurify": "^3.4.11",
37
+ "dompurify": "^3.4.12",
38
38
  "flat": "^6.0.1",
39
- "fuse.js": "^7.4.2",
39
+ "fuse.js": "^7.5.0",
40
40
  "handlebars": "^4.7.9",
41
41
  "handlebars-async-helpers": "^1.0.6",
42
42
  "i18next": "^23.16.8",
@@ -93,7 +93,7 @@
93
93
  "url": "https://github.com/CybercentreCanada/howler"
94
94
  },
95
95
  "type": "module",
96
- "version": "2.19.0-dev.1260",
96
+ "version": "2.19.0-dev.1266",
97
97
  "exports": {
98
98
  "./i18n": "./i18n.js",
99
99
  "./index.css": "./index.css",