@panneau/core 4.0.78 → 4.1.1
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.
- package/dist/contexts.d.ts +266 -168
- package/dist/contexts.js +46 -31
- package/dist/hooks.d.ts +78 -40
- package/dist/hooks.js +780 -338
- package/dist/index.d.ts +299 -201
- package/dist/index.js +23 -29
- package/dist/utils.d.ts +192 -15
- package/dist/utils.js +106 -31
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import isObject from 'lodash-es/isObject';
|
|
1
2
|
import { flattenComponents, getComponentFromName } from '@panneau/core/utils';
|
|
2
|
-
import { isObject } from 'lodash';
|
|
3
3
|
import isArray from 'lodash-es/isArray';
|
|
4
4
|
import uniqBy from 'lodash-es/uniqBy';
|
|
5
5
|
import EventEmitter from 'wolfy87-eventemitter';
|
|
6
|
-
import isObject$1 from 'lodash-es/isObject';
|
|
7
|
-
import uniq from 'lodash-es/uniq';
|
|
8
6
|
import { Tracking as Tracking$1 } from '@folklore/tracking';
|
|
9
7
|
|
|
10
8
|
class ComponentsManager {
|
|
9
|
+
// Nested components maps are flattened with their key as namespace
|
|
11
10
|
constructor(components = {}) {
|
|
12
11
|
this.components = flattenComponents(components);
|
|
13
12
|
}
|
|
@@ -175,7 +174,7 @@ class FontsParser {
|
|
|
175
174
|
theme = null,
|
|
176
175
|
components = []
|
|
177
176
|
} = story || {};
|
|
178
|
-
const fonts =
|
|
177
|
+
const fonts = uniqBy(components.reduce((currentFonts, screen) => {
|
|
179
178
|
const {
|
|
180
179
|
type
|
|
181
180
|
} = screen;
|
|
@@ -196,7 +195,7 @@ class FontsParser {
|
|
|
196
195
|
return fonts.length > 0 || themeFonts.length > 0 ? {
|
|
197
196
|
...story,
|
|
198
197
|
theme: newTheme,
|
|
199
|
-
fonts:
|
|
198
|
+
fonts: uniqBy([...themeFonts, ...fonts], 'name')
|
|
200
199
|
} : story;
|
|
201
200
|
}
|
|
202
201
|
return fonts.length > 0 ? {
|
|
@@ -244,9 +243,9 @@ class FontsParser {
|
|
|
244
243
|
const value = data[key];
|
|
245
244
|
let font = null;
|
|
246
245
|
let subFonts = null;
|
|
247
|
-
if (patternMatch && isObject
|
|
246
|
+
if (patternMatch && isObject(value) && FontsParser.valueIsFont(value)) {
|
|
248
247
|
font = value;
|
|
249
|
-
} else if (isObject
|
|
248
|
+
} else if (isObject(value) || isArray(value)) {
|
|
250
249
|
subFonts = FontsParser.extractFontsWithPaths(value, patterns, path);
|
|
251
250
|
}
|
|
252
251
|
return subFonts !== null || font !== null ? [...currentFonts, ...(subFonts || []), ...(font !== null ? [font] : [])] : currentFonts;
|
|
@@ -259,7 +258,7 @@ class FormsManager extends DefinitionsManager {}
|
|
|
259
258
|
class MediasParser {
|
|
260
259
|
constructor({
|
|
261
260
|
fieldsManager,
|
|
262
|
-
screensManager
|
|
261
|
+
screensManager = null
|
|
263
262
|
}) {
|
|
264
263
|
this.fieldsManager = fieldsManager;
|
|
265
264
|
this.screensManager = screensManager;
|
|
@@ -286,7 +285,7 @@ class MediasParser {
|
|
|
286
285
|
} = screen;
|
|
287
286
|
const {
|
|
288
287
|
fields = []
|
|
289
|
-
} = this.screensManager
|
|
288
|
+
} = this.screensManager?.getDefinition(type) || {};
|
|
290
289
|
const fieldsPattern = this.getMediaFieldsPattern(fields);
|
|
291
290
|
const {
|
|
292
291
|
data: newScreen,
|
|
@@ -346,7 +345,7 @@ class MediasParser {
|
|
|
346
345
|
} = screen;
|
|
347
346
|
const {
|
|
348
347
|
fields = []
|
|
349
|
-
} = this.screensManager
|
|
348
|
+
} = this.screensManager?.getDefinition(type) || {};
|
|
350
349
|
const fieldsPattern = this.getMediaFieldsPattern(fields);
|
|
351
350
|
return MediasParser.replacePathsWithMedias(screen, medias, fieldsPattern);
|
|
352
351
|
}) : components;
|
|
@@ -405,9 +404,9 @@ class MediasParser {
|
|
|
405
404
|
const value = data[key];
|
|
406
405
|
let newValue;
|
|
407
406
|
if (patternMatch) {
|
|
408
|
-
newValue = isObject
|
|
407
|
+
newValue = isObject(value) ? value : medias[value] || null;
|
|
409
408
|
} else {
|
|
410
|
-
newValue = isObject
|
|
409
|
+
newValue = isObject(value) || isArray(value) ? MediasParser.replacePathsWithMedias(value, medias, patterns, path) : value;
|
|
411
410
|
}
|
|
412
411
|
return dataIsArray ? [...newData, newValue] : {
|
|
413
412
|
...newData,
|
|
@@ -433,11 +432,11 @@ class MediasParser {
|
|
|
433
432
|
let newValue;
|
|
434
433
|
let media = null;
|
|
435
434
|
let subMedias = null;
|
|
436
|
-
if (patternMatch && isObject
|
|
435
|
+
if (patternMatch && isObject(value)) {
|
|
437
436
|
const mediaPath = MediasParser.getMediaPath(value);
|
|
438
437
|
newValue = mediaPath !== null ? mediaPath : value;
|
|
439
438
|
media = mediaPath !== null ? value : null;
|
|
440
|
-
} else if (isObject
|
|
439
|
+
} else if (isObject(value) || isArray(value)) {
|
|
441
440
|
const subReturn = MediasParser.replaceMediasWithPaths(value, patterns, medias, path);
|
|
442
441
|
newValue = subReturn.data;
|
|
443
442
|
subMedias = subReturn.medias;
|
|
@@ -459,7 +458,7 @@ class MediasParser {
|
|
|
459
458
|
}
|
|
460
459
|
};
|
|
461
460
|
}, {
|
|
462
|
-
data:
|
|
461
|
+
data: dataIsArray ? [] : {},
|
|
463
462
|
medias
|
|
464
463
|
});
|
|
465
464
|
}
|
|
@@ -467,16 +466,6 @@ class MediasParser {
|
|
|
467
466
|
|
|
468
467
|
/* eslint-disable no-console */
|
|
469
468
|
class Tracking extends Tracking$1 {
|
|
470
|
-
constructor(opts = {}) {
|
|
471
|
-
super(opts);
|
|
472
|
-
const {
|
|
473
|
-
variables = null
|
|
474
|
-
} = this.options;
|
|
475
|
-
this.variables = null;
|
|
476
|
-
if (variables !== null) {
|
|
477
|
-
this.setVariables(variables);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
469
|
setVariables(variables) {
|
|
481
470
|
this.variables = variables;
|
|
482
471
|
if (variables !== null) {
|
|
@@ -499,10 +488,15 @@ class Tracking extends Tracking$1 {
|
|
|
499
488
|
};
|
|
500
489
|
this.push(data);
|
|
501
490
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
491
|
+
|
|
492
|
+
// The options can also be a value, to stay compatible with the base trackEvent signature
|
|
493
|
+
trackEvent(category, action, label, options = {}) {
|
|
494
|
+
const {
|
|
495
|
+
value = null,
|
|
496
|
+
...opts
|
|
497
|
+
} = isObject(options) ? options : {
|
|
498
|
+
value: options
|
|
499
|
+
};
|
|
506
500
|
const data = {
|
|
507
501
|
...opts,
|
|
508
502
|
event: 'eventInteraction',
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { kebabCase as dashCase, pascalCase, snakeCase } from 'change-case';
|
|
2
|
-
import { ElementType, ReactNode, ReactElement, ForwardedRef } from 'react';
|
|
3
|
-
import { MessageDescriptor } from 'react-intl';
|
|
2
|
+
import { ElementType, ComponentProps, ReactNode, MouseEvent, ReactElement, ForwardedRef } from 'react';
|
|
3
|
+
import { FormattedMessage, MessageDescriptor } from 'react-intl';
|
|
4
4
|
|
|
5
5
|
interface CheckClickableOptions {
|
|
6
6
|
maxParentDistance?: number;
|
|
@@ -26,11 +26,6 @@ declare function createUseEvent(eventsManager: EventsManager | null): (event: st
|
|
|
26
26
|
/*! clipboard-copy. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
27
27
|
declare function copyToClipboard(text: string): Promise<void>;
|
|
28
28
|
|
|
29
|
-
type ComponentMap = Record<string, ElementType | Record<string, unknown>>;
|
|
30
|
-
declare function flattenComponents(components: ComponentMap | null, prefix?: string | null): Record<string, ElementType> | null;
|
|
31
|
-
|
|
32
|
-
declare function formatDuration(timeInSeconds: number | string): string;
|
|
33
|
-
|
|
34
29
|
/**
|
|
35
30
|
* Core UI Types
|
|
36
31
|
*/
|
|
@@ -39,10 +34,40 @@ declare function formatDuration(timeInSeconds: number | string): string;
|
|
|
39
34
|
* Internationalization message definition
|
|
40
35
|
*/
|
|
41
36
|
type Message = MessageDescriptor;
|
|
37
|
+
/**
|
|
38
|
+
* Values that can be interpolated in an internationalization message
|
|
39
|
+
* (the `values` prop of FormattedMessage)
|
|
40
|
+
*/
|
|
41
|
+
type MessageValues = NonNullable<ComponentProps<typeof FormattedMessage>['values']>;
|
|
42
|
+
type MessageValue = MessageValues[string];
|
|
42
43
|
/**
|
|
43
44
|
* Label can be either a message object or any React node
|
|
44
45
|
*/
|
|
45
46
|
type Label = Message | ReactNode;
|
|
47
|
+
/**
|
|
48
|
+
* Map of components, can be nested by namespace (ex: { fields: { text: TextField } })
|
|
49
|
+
*/
|
|
50
|
+
interface ComponentsMap {
|
|
51
|
+
[name: string]: ElementType | ComponentsMap;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Menu item definition
|
|
55
|
+
*/
|
|
56
|
+
interface MenuItem {
|
|
57
|
+
id?: number | string;
|
|
58
|
+
label?: Label;
|
|
59
|
+
url?: string;
|
|
60
|
+
href?: string | null;
|
|
61
|
+
external?: boolean;
|
|
62
|
+
active?: boolean;
|
|
63
|
+
type?: string;
|
|
64
|
+
component?: string;
|
|
65
|
+
className?: string | null;
|
|
66
|
+
linkClassName?: string | null;
|
|
67
|
+
onClick?: ((e: MouseEvent) => void) | null;
|
|
68
|
+
items?: MenuItem[] | null;
|
|
69
|
+
dropdown?: MenuItem[] | null;
|
|
70
|
+
}
|
|
46
71
|
/**
|
|
47
72
|
* Form control size
|
|
48
73
|
*/
|
|
@@ -95,6 +120,8 @@ interface Field {
|
|
|
95
120
|
size?: ControlSize;
|
|
96
121
|
[key: string]: unknown;
|
|
97
122
|
}
|
|
123
|
+
interface FieldDefinition$1 extends Omit<Field, 'component'>, Definition {
|
|
124
|
+
}
|
|
98
125
|
/**
|
|
99
126
|
* Form definition
|
|
100
127
|
*/
|
|
@@ -120,6 +147,7 @@ interface FormDefinition extends Form, Definition {
|
|
|
120
147
|
type TableColumn = string | Column;
|
|
121
148
|
interface Column extends Record<string, unknown> {
|
|
122
149
|
id?: string | null;
|
|
150
|
+
name?: string | null;
|
|
123
151
|
label?: unknown;
|
|
124
152
|
path?: string;
|
|
125
153
|
valueKey?: string;
|
|
@@ -145,7 +173,7 @@ type ActionDefinition = string | Action;
|
|
|
145
173
|
interface ResourceIntlValues {
|
|
146
174
|
a_singular?: string;
|
|
147
175
|
a_plural?: string;
|
|
148
|
-
[key: string]:
|
|
176
|
+
[key: string]: MessageValue;
|
|
149
177
|
}
|
|
150
178
|
/**
|
|
151
179
|
* Internationalization configuration
|
|
@@ -164,6 +192,11 @@ interface ResourceIndex {
|
|
|
164
192
|
fields?: Field[];
|
|
165
193
|
[key: string]: unknown;
|
|
166
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Overrides of the built-in resource pages, ex: `{ show: { component: 'my-page' } }`.
|
|
197
|
+
* Used on a resource (`resource.pages`) and on the panneau definition (`definition.pages`),
|
|
198
|
+
* the resource taking precedence.
|
|
199
|
+
*/
|
|
167
200
|
interface ResourcePages {
|
|
168
201
|
index?: PageDefinition;
|
|
169
202
|
show?: PageDefinition;
|
|
@@ -171,15 +204,49 @@ interface ResourcePages {
|
|
|
171
204
|
edit?: PageDefinition;
|
|
172
205
|
delete?: PageDefinition;
|
|
173
206
|
duplicate?: PageDefinition;
|
|
207
|
+
/** @deprecated Legacy resource keys, use `index`, `show`, `create`, etc. */
|
|
208
|
+
resourceIndexPage?: PageDefinition;
|
|
209
|
+
resourceShowPage?: PageDefinition;
|
|
210
|
+
resourceCreatePage?: PageDefinition;
|
|
211
|
+
resourceEditPage?: PageDefinition;
|
|
212
|
+
resourceDeletePage?: PageDefinition;
|
|
213
|
+
resourceDuplicatePage?: PageDefinition;
|
|
214
|
+
/** @deprecated Legacy panneau keys, use `index`, `show`, `create`, etc. */
|
|
215
|
+
indexPage?: PageDefinition;
|
|
216
|
+
showPage?: PageDefinition;
|
|
217
|
+
createPage?: PageDefinition;
|
|
218
|
+
editPage?: PageDefinition;
|
|
219
|
+
deletePage?: PageDefinition;
|
|
220
|
+
duplicatePage?: PageDefinition;
|
|
174
221
|
[key: string]: PageDefinition | undefined;
|
|
175
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Form definition of a resource (the id is optional, the form is identified by its key)
|
|
225
|
+
*/
|
|
226
|
+
interface ResourceForm extends Form {
|
|
227
|
+
id?: string;
|
|
228
|
+
component?: string | ElementType;
|
|
229
|
+
}
|
|
176
230
|
interface ResourceForms {
|
|
177
|
-
default
|
|
178
|
-
create
|
|
179
|
-
edit
|
|
180
|
-
delete
|
|
181
|
-
|
|
182
|
-
|
|
231
|
+
default?: ResourceForm | null;
|
|
232
|
+
create?: ResourceForm | null;
|
|
233
|
+
edit?: ResourceForm | null;
|
|
234
|
+
delete?: ResourceForm | null;
|
|
235
|
+
duplicate?: ResourceForm | null;
|
|
236
|
+
modal?: ResourceForm | null;
|
|
237
|
+
[key: string]: ResourceForm | null | undefined;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Resource type (sub-type of a resource, ex: page types)
|
|
241
|
+
*/
|
|
242
|
+
interface ResourceType {
|
|
243
|
+
id?: string;
|
|
244
|
+
name?: string;
|
|
245
|
+
fields?: Field[];
|
|
246
|
+
settings?: {
|
|
247
|
+
canCreate?: boolean;
|
|
248
|
+
[key: string]: unknown;
|
|
249
|
+
};
|
|
183
250
|
}
|
|
184
251
|
interface Resource {
|
|
185
252
|
id: string;
|
|
@@ -191,6 +258,7 @@ interface Resource {
|
|
|
191
258
|
settings?: Record<string, unknown>;
|
|
192
259
|
extraRoutes?: RouteDefinition[];
|
|
193
260
|
pages?: ResourcePages;
|
|
261
|
+
types?: ResourceType[];
|
|
194
262
|
}
|
|
195
263
|
/**
|
|
196
264
|
* Filter definition
|
|
@@ -201,6 +269,97 @@ interface Filter {
|
|
|
201
269
|
[key: string]: unknown;
|
|
202
270
|
}
|
|
203
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Panneau Definition Types
|
|
274
|
+
* TypeScript interfaces for main Panneau configuration and definitions
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Panneau route configuration
|
|
279
|
+
*/
|
|
280
|
+
interface Routes {
|
|
281
|
+
'resources.index'?: RouteDefinition | string;
|
|
282
|
+
'resources.create'?: RouteDefinition | string;
|
|
283
|
+
'resources.store'?: RouteDefinition | string;
|
|
284
|
+
'resources.show'?: RouteDefinition | string;
|
|
285
|
+
'resources.edit'?: RouteDefinition | string;
|
|
286
|
+
'resources.update'?: RouteDefinition | string;
|
|
287
|
+
'resources.delete'?: RouteDefinition | string;
|
|
288
|
+
'resources.destroy'?: RouteDefinition | string;
|
|
289
|
+
'resources.duplicate'?: RouteDefinition | string;
|
|
290
|
+
'resources.clone'?: RouteDefinition | string;
|
|
291
|
+
'resources.restore'?: RouteDefinition | string;
|
|
292
|
+
[key: string]: RouteDefinition | string;
|
|
293
|
+
}
|
|
294
|
+
interface PanneauTheme {
|
|
295
|
+
colorScheme?: string | null;
|
|
296
|
+
[key: string]: unknown;
|
|
297
|
+
}
|
|
298
|
+
interface PanneauComponents {
|
|
299
|
+
[key: string]: string | {
|
|
300
|
+
component: string;
|
|
301
|
+
[key: string]: unknown;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
interface PanneauIntlValues {
|
|
305
|
+
[key: string]: string;
|
|
306
|
+
}
|
|
307
|
+
interface PanneauIntl {
|
|
308
|
+
locale?: string;
|
|
309
|
+
locales?: string[];
|
|
310
|
+
messages?: Record<string, string>;
|
|
311
|
+
/** @deprecated Not used: set the values of each resource in `resources[].intl.values` */
|
|
312
|
+
values?: PanneauIntlValues;
|
|
313
|
+
}
|
|
314
|
+
interface PanneauPages extends ResourcePages {
|
|
315
|
+
home?: PageDefinition;
|
|
316
|
+
login?: PageDefinition;
|
|
317
|
+
account?: PageDefinition;
|
|
318
|
+
error?: PageDefinition;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Menu item in a Panneau menu: a menu item definition or a special item id
|
|
322
|
+
* (ex: 'resources', 'account', 'separator')
|
|
323
|
+
*/
|
|
324
|
+
type PanneauMenuItem = MenuItem | string;
|
|
325
|
+
interface PanneauMenus {
|
|
326
|
+
main?: PanneauMenuItem[] | null;
|
|
327
|
+
guest?: PanneauMenuItem[] | null;
|
|
328
|
+
[key: string]: PanneauMenuItem[] | null | undefined;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Links shown on the login page: `true` for the default url, or the url of the page
|
|
332
|
+
*/
|
|
333
|
+
interface PanneauAuth {
|
|
334
|
+
/** "Forgot your password?" link (default url: /forgot-password) */
|
|
335
|
+
forgotPassword?: boolean | string;
|
|
336
|
+
/** "Create account" link (default url: /register) */
|
|
337
|
+
register?: boolean | string;
|
|
338
|
+
[key: string]: unknown;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Panneau definition (main configuration)
|
|
342
|
+
*/
|
|
343
|
+
interface PanneauDefinition {
|
|
344
|
+
name?: string;
|
|
345
|
+
resources?: Resource[];
|
|
346
|
+
routes?: Routes;
|
|
347
|
+
pages?: PanneauPages;
|
|
348
|
+
intl?: PanneauIntl;
|
|
349
|
+
theme?: PanneauTheme;
|
|
350
|
+
/** @deprecated Not used: give the custom components to the `components` prop of the container */
|
|
351
|
+
components?: PanneauComponents;
|
|
352
|
+
settings?: Record<string, unknown>;
|
|
353
|
+
forms?: FormDefinition[];
|
|
354
|
+
fields?: FieldDefinition$1[];
|
|
355
|
+
menus?: PanneauMenus;
|
|
356
|
+
auth?: PanneauAuth;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
declare function flattenComponents(components: ComponentsMap | null, prefix?: string | null): Record<string, ElementType> | null;
|
|
360
|
+
|
|
361
|
+
declare function formatDuration(timeInSeconds: number | string): string;
|
|
362
|
+
|
|
204
363
|
declare function getColumnsWithFields(resource: Resource, columns: TableColumn[] | null): Column[];
|
|
205
364
|
|
|
206
365
|
interface ComponentResult {
|
|
@@ -216,6 +375,12 @@ declare function getComponentFromName(name?: string | ElementType | null, compon
|
|
|
216
375
|
|
|
217
376
|
declare function getDefinitionFromId<T extends Definition = Definition>(id?: string | null, items?: T[] | null, defaultObject?: T | null): T | null;
|
|
218
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Returns a message for each key of the definition that is not supported anymore,
|
|
380
|
+
* with the key to use instead. Without it, these keys would be silently ignored.
|
|
381
|
+
*/
|
|
382
|
+
declare function getDefinitionWarnings(definition: PanneauDefinition | null): string[];
|
|
383
|
+
|
|
219
384
|
interface ComponentWithName {
|
|
220
385
|
displayName?: string | null;
|
|
221
386
|
name?: string | null;
|
|
@@ -239,6 +404,8 @@ interface FieldWithSubFields extends Field {
|
|
|
239
404
|
type?: string | null;
|
|
240
405
|
fields?: Field[] | null;
|
|
241
406
|
field?: Field | null;
|
|
407
|
+
settings?: Field[] | null;
|
|
408
|
+
itemsField?: Field | null;
|
|
242
409
|
listItems?: boolean;
|
|
243
410
|
}
|
|
244
411
|
declare function getFieldFromPath(path: string | string[], fields: Field[], fieldManager: FieldManager): FieldWithSubFields | null;
|
|
@@ -259,6 +426,16 @@ declare function loadPackage<T = unknown>(name: string, loader: () => Promise<T>
|
|
|
259
426
|
|
|
260
427
|
declare function mergeRefs<T>(...refs: Array<ForwardedRef<T> | null | undefined>): (value: T | null) => void;
|
|
261
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Parse a date-only value as a local date, ignoring its timezone.
|
|
431
|
+
*
|
|
432
|
+
* A date stored at midnight represents a calendar day, not an instant: converting it to the
|
|
433
|
+
* browser timezone would show the previous day west of the server timezone (ex: a date saved as
|
|
434
|
+
* 2026-10-01 00:00 UTC is 2026-09-30 20:00 in Montreal). Returns null when the value has a time,
|
|
435
|
+
* so the caller can parse it as an instant.
|
|
436
|
+
*/
|
|
437
|
+
declare function parseDateOnly(value: string | null | undefined): Date | null;
|
|
438
|
+
|
|
262
439
|
declare function toggleSelectedItem(items: Item[] | null, item: Item, { multiple: multipleSelection }?: {
|
|
263
440
|
multiple?: boolean;
|
|
264
441
|
}): Item[] | null;
|
|
@@ -277,4 +454,4 @@ interface FieldWithValidation extends Field {
|
|
|
277
454
|
}
|
|
278
455
|
declare function validateFields(fields: FieldWithValidation[], value: Record<string, unknown> | null): boolean;
|
|
279
456
|
|
|
280
|
-
export { checkClickable, convertStyleToString, copyToClipboard, createNullableOnChange, createUseEvent, flattenComponents, formatDuration, getColumnsWithFields, getComponent, getComponentFromName, getDefinitionFromId, getDisplayName, getFieldByName, getFieldFromPath, getFileName, getPathValue, getSelectOptions, isMessage, loadPackage, mergeRefs, selectItems, setValue as setFieldValue, slug, toggleSelectedItem, unselectItems, validateFields };
|
|
457
|
+
export { checkClickable, convertStyleToString, copyToClipboard, createNullableOnChange, createUseEvent, flattenComponents, formatDuration, getColumnsWithFields, getComponent, getComponentFromName, getDefinitionFromId, getDefinitionWarnings, getDisplayName, getFieldByName, getFieldFromPath, getFileName, getPathValue, getSelectOptions, isMessage, loadPackage, mergeRefs, parseDateOnly, selectItems, setValue as setFieldValue, slug, toggleSelectedItem, unselectItems, validateFields };
|
package/dist/utils.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import isNumber from 'lodash-es/isNumber';
|
|
2
|
-
import { snakeCase } from 'snake-case';
|
|
3
2
|
import isObject from 'lodash-es/isObject';
|
|
4
3
|
import { useEffect, isValidElement } from 'react';
|
|
5
4
|
import { isValidElementType } from 'react-is';
|
|
6
5
|
import isString from 'lodash-es/isString';
|
|
7
|
-
import { pascalCase, kebabCase, snakeCase
|
|
6
|
+
import { pascalCase, kebabCase, snakeCase } from 'change-case';
|
|
8
7
|
export { kebabCase as dashCase, pascalCase, snakeCase } from 'change-case';
|
|
9
8
|
import isFunction from 'lodash-es/isFunction';
|
|
10
9
|
import isArray from 'lodash-es/isArray';
|
|
@@ -44,8 +43,17 @@ function checkClickable(el, options = {}, parentDistance = 1) {
|
|
|
44
43
|
return false;
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
// Convert a camelCase style property (ex: backgroundColor) to its CSS kebab-case
|
|
47
|
+
// name (ex: background-color). Custom properties (--foo) are kept as is.
|
|
48
|
+
function toCssProperty(key) {
|
|
49
|
+
if (key.startsWith('--')) {
|
|
50
|
+
return key;
|
|
51
|
+
}
|
|
52
|
+
const property = key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
|
53
|
+
return property.startsWith('ms-') ? `-${property}` : property;
|
|
54
|
+
}
|
|
47
55
|
function convertStyleToString(style) {
|
|
48
|
-
return style !== null ? Object.keys(style).map(key => `${
|
|
56
|
+
return style !== null ? Object.keys(style).map(key => `${toCssProperty(key)}:${isNumber(style[key]) ? `${style[key]}px` : style[key]};`).join('\n') : '';
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
function createNullableOnChange(onChange = null) {
|
|
@@ -142,12 +150,15 @@ function flattenComponents(components, prefix = null) {
|
|
|
142
150
|
if (components === null) {
|
|
143
151
|
return null;
|
|
144
152
|
}
|
|
145
|
-
return Object.keys(components).reduce((newMap, key) =>
|
|
146
|
-
|
|
147
|
-
[
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
return Object.keys(components).reduce((newMap, key) => {
|
|
154
|
+
const path = prefix !== null ? `${prefix}.${key}` : key;
|
|
155
|
+
return isValidElementType(components[key]) ? {
|
|
156
|
+
...newMap,
|
|
157
|
+
[path]: components[key]
|
|
158
|
+
} : {
|
|
159
|
+
...newMap,
|
|
160
|
+
...flattenComponents(components[key], path)
|
|
161
|
+
};
|
|
151
162
|
}, {});
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -251,6 +262,33 @@ function getDefinitionFromId(id = null, items = [], defaultObject = null) {
|
|
|
251
262
|
return items.find(i => i.id === pascalId) || items.find(i => i.id === id) || defaultObject;
|
|
252
263
|
}
|
|
253
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Returns a message for each key of the definition that is not supported anymore,
|
|
267
|
+
* with the key to use instead. Without it, these keys would be silently ignored.
|
|
268
|
+
*/
|
|
269
|
+
function getDefinitionWarnings(definition) {
|
|
270
|
+
if (!isObject(definition)) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const {
|
|
274
|
+
components = null,
|
|
275
|
+
intl = null,
|
|
276
|
+
theme = null,
|
|
277
|
+
resources = []
|
|
278
|
+
} = definition;
|
|
279
|
+
const {
|
|
280
|
+
values = null
|
|
281
|
+
} = intl || {};
|
|
282
|
+
return [...(components !== null ? ['`components` is not supported: give the custom components to the `components` prop of the container.'] : []), ...(values !== null ? ['`intl.values` is not supported: set the values of each resource in `resources[].intl.values`.'] : []), ...Object.keys(theme || {}).filter(key => key !== 'colorScheme').map(key => `\`theme.${key}\` is not supported: only \`theme.colorScheme\` is.`), ...(resources || []).reduce((warnings, resource) => {
|
|
283
|
+
const {
|
|
284
|
+
id,
|
|
285
|
+
components: resourceComponents = null,
|
|
286
|
+
columns = null
|
|
287
|
+
} = resource;
|
|
288
|
+
return [...warnings, ...(resourceComponents !== null ? [`\`resources[${id}].components\` is not supported: use \`index.component\`, \`forms.default.component\` and \`pages\`.`] : []), ...(columns !== null ? [`\`resources[${id}].columns\` is not supported: use \`index.columns\`.`] : [])];
|
|
289
|
+
}, [])];
|
|
290
|
+
}
|
|
291
|
+
|
|
254
292
|
function getDisplayName({
|
|
255
293
|
displayName = null,
|
|
256
294
|
name = null
|
|
@@ -275,6 +313,9 @@ function getFieldByName(fields, name) {
|
|
|
275
313
|
}
|
|
276
314
|
|
|
277
315
|
function getFieldFromPath(path, fields, fieldManager) {
|
|
316
|
+
const initialField = {
|
|
317
|
+
fields
|
|
318
|
+
};
|
|
278
319
|
return (isArray(path) ? path : [path]).reduce((foundField, key) => {
|
|
279
320
|
if (foundField === null) {
|
|
280
321
|
return null;
|
|
@@ -298,9 +339,7 @@ function getFieldFromPath(path, fields, fieldManager) {
|
|
|
298
339
|
};
|
|
299
340
|
}
|
|
300
341
|
return getFieldByName([...(fieldFields || []), ...(subFields || []), ...(settings || [])], key);
|
|
301
|
-
},
|
|
302
|
-
fields
|
|
303
|
-
});
|
|
342
|
+
}, initialField);
|
|
304
343
|
}
|
|
305
344
|
|
|
306
345
|
function getFileName(url = null) {
|
|
@@ -359,24 +398,24 @@ function loadPackage(name, loader) {
|
|
|
359
398
|
loaders[name].resource = response;
|
|
360
399
|
events.emit(`loaded:${name}`, response);
|
|
361
400
|
return response;
|
|
401
|
+
}, error => {
|
|
402
|
+
// Remove the failed loader so a later call can retry
|
|
403
|
+
delete loaders[name];
|
|
404
|
+
throw error;
|
|
362
405
|
}),
|
|
363
406
|
loaded: false,
|
|
364
407
|
resource: null
|
|
365
408
|
};
|
|
366
409
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
resolve(response);
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
});
|
|
410
|
+
const {
|
|
411
|
+
loaded = false,
|
|
412
|
+
resource = null,
|
|
413
|
+
loader: currentLoader = null
|
|
414
|
+
} = loaders[name];
|
|
415
|
+
if (loaded === true) {
|
|
416
|
+
return Promise.resolve(resource);
|
|
417
|
+
}
|
|
418
|
+
return currentLoader;
|
|
380
419
|
}
|
|
381
420
|
|
|
382
421
|
function mergeRefs(...refs) {
|
|
@@ -391,6 +430,30 @@ function mergeRefs(...refs) {
|
|
|
391
430
|
};
|
|
392
431
|
}
|
|
393
432
|
|
|
433
|
+
// A date without time (2026-10-01), or a date at midnight with or without a timezone
|
|
434
|
+
// (2026-10-01T00:00:00-04:00, 2026-10-01T00:00:00.000000Z, 2026-10-01 00:00:00)
|
|
435
|
+
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})(?:[T ]00:00(?::00(?:\.0+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Parse a date-only value as a local date, ignoring its timezone.
|
|
439
|
+
*
|
|
440
|
+
* A date stored at midnight represents a calendar day, not an instant: converting it to the
|
|
441
|
+
* browser timezone would show the previous day west of the server timezone (ex: a date saved as
|
|
442
|
+
* 2026-10-01 00:00 UTC is 2026-09-30 20:00 in Montreal). Returns null when the value has a time,
|
|
443
|
+
* so the caller can parse it as an instant.
|
|
444
|
+
*/
|
|
445
|
+
function parseDateOnly(value) {
|
|
446
|
+
if (typeof value !== 'string') {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
const match = DATE_ONLY_PATTERN.exec(value.trim());
|
|
450
|
+
if (match === null) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const [, year, month, day] = match;
|
|
454
|
+
return new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
|
|
455
|
+
}
|
|
456
|
+
|
|
394
457
|
function toggleSelectedItem(items, item, {
|
|
395
458
|
multiple: multipleSelection = false
|
|
396
459
|
} = {}) {
|
|
@@ -441,8 +504,18 @@ function setValue(value, keyParts, fieldValue) {
|
|
|
441
504
|
if (value !== null || fieldValue !== null) {
|
|
442
505
|
if (isArray) {
|
|
443
506
|
const index = parseInt(key, 10);
|
|
444
|
-
const arrayValue = value;
|
|
445
|
-
|
|
507
|
+
const arrayValue = Array.isArray(value) ? value : [];
|
|
508
|
+
let newArrayValue;
|
|
509
|
+
if (fieldValue !== null) {
|
|
510
|
+
newArrayValue = [...arrayValue];
|
|
511
|
+
// Pad with null so an out-of-range index lands at the right position
|
|
512
|
+
while (newArrayValue.length < index) {
|
|
513
|
+
newArrayValue.push(null);
|
|
514
|
+
}
|
|
515
|
+
newArrayValue[index] = keyParts.length > 0 ? setValue(arrayValue[index] || null, keyParts, fieldValue) : fieldValue;
|
|
516
|
+
} else {
|
|
517
|
+
newArrayValue = [...arrayValue.slice(0, index), ...arrayValue.slice(index + 1)];
|
|
518
|
+
}
|
|
446
519
|
return newArrayValue.length > 0 ? newArrayValue : null;
|
|
447
520
|
}
|
|
448
521
|
const objectValue = value;
|
|
@@ -459,7 +532,7 @@ function slug(str, separator = null) {
|
|
|
459
532
|
if (separator === '-') {
|
|
460
533
|
toSlug = kebabCase(str);
|
|
461
534
|
} else {
|
|
462
|
-
toSlug = snakeCase
|
|
535
|
+
toSlug = snakeCase(str);
|
|
463
536
|
}
|
|
464
537
|
return slugify(toSlug, {
|
|
465
538
|
lower: true
|
|
@@ -472,11 +545,13 @@ function validateFields(fields, value) {
|
|
|
472
545
|
if (field.type === 'fields' && field.fields) {
|
|
473
546
|
return validateFields(field.fields, value);
|
|
474
547
|
}
|
|
475
|
-
const val = value && field.name
|
|
476
|
-
|
|
548
|
+
const val = value && field.name ? value[field.name] : null;
|
|
549
|
+
// Only null, undefined and empty strings are considered missing (0 and false are valid)
|
|
550
|
+
const isMissing = val === null || typeof val === 'undefined' || val === '';
|
|
551
|
+
return !(field.required && isMissing);
|
|
477
552
|
}
|
|
478
553
|
return acc;
|
|
479
554
|
}, true);
|
|
480
555
|
}
|
|
481
556
|
|
|
482
|
-
export { checkClickable, convertStyleToString, copyToClipboard, createNullableOnChange, createUseEvent, flattenComponents, formatDuration, getColumnsWithFields, getComponent, getComponentFromName, getDefinitionFromId, getDisplayName, getFieldByName, getFieldFromPath, getFileName, getPathValue, getSelectOptions, isMessage, loadPackage, mergeRefs, selectItems, setValue as setFieldValue, slug, toggleSelectedItem, unselectItems, validateFields };
|
|
557
|
+
export { checkClickable, convertStyleToString, copyToClipboard, createNullableOnChange, createUseEvent, flattenComponents, formatDuration, getColumnsWithFields, getComponent, getComponentFromName, getDefinitionFromId, getDefinitionWarnings, getDisplayName, getFieldByName, getFieldFromPath, getFileName, getPathValue, getSelectOptions, isMessage, loadPackage, mergeRefs, parseDateOnly, selectItems, setValue as setFieldValue, slug, toggleSelectedItem, unselectItems, validateFields };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panneau/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"javascript"
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"debug": "^4.3.4",
|
|
76
76
|
"flat": "^6.0.1",
|
|
77
77
|
"hoist-non-react-statics": "^3.3.2",
|
|
78
|
-
"lodash": "^4.17.21",
|
|
78
|
+
"lodash-es": "^4.17.21",
|
|
79
79
|
"react-intl": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^10.0.0",
|
|
80
80
|
"react-is": "^19.2.5",
|
|
81
81
|
"slugify": "^1.5.0",
|
|
@@ -96,5 +96,5 @@
|
|
|
96
96
|
"publishConfig": {
|
|
97
97
|
"access": "public"
|
|
98
98
|
},
|
|
99
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "28b394278cffa0c927989873dd4b3f184635ceff"
|
|
100
100
|
}
|