@appsemble/utils 0.29.9 → 0.29.11

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.29.9/config/assets/logo.svg) Appsemble Utilities
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.29.11/config/assets/logo.svg) Appsemble Utilities
2
2
 
3
3
  > Internal utility functions used across multiple Appsemble projects.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/utils)](https://www.npmjs.com/package/@appsemble/utils)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.29.9/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.29.9)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.29.11/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.29.11)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
@@ -26,5 +26,5 @@ not guaranteed.
26
26
 
27
27
  ## License
28
28
 
29
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.29.9/LICENSE.md) ©
29
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.29.11/LICENSE.md) ©
30
30
  [Appsemble](https://appsemble.com)
@@ -96,6 +96,7 @@ The most basic resource has a \`schema\` property and defines the minimal securi
96
96
  { $ref: '#/components/schemas/TabsPageDefinition' },
97
97
  { $ref: '#/components/schemas/FlowPageDefinition' },
98
98
  { $ref: '#/components/schemas/LoopPageDefinition' },
99
+ { $ref: '#/components/schemas/ContainerPageDefinition' },
99
100
  ],
100
101
  },
101
102
  },
@@ -0,0 +1 @@
1
+ export declare const ContainerPageDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
@@ -0,0 +1,55 @@
1
+ import { BasePageDefinition } from './BasePageDefinition.js';
2
+ import { extendJSONSchema } from './utils.js';
3
+ export const ContainerPageDefinition = extendJSONSchema(BasePageDefinition, {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ description: `Use this page type to group pages in the menu, this doesn't actually group pages for now. Following is an example of how this can be used
7
+ \`\`\`yaml
8
+ pages:
9
+ - name: Page 1
10
+ type: container
11
+ pages:
12
+ - name: Contained page 1
13
+ blocks:
14
+ - type: action-button
15
+ version: 0.29.8
16
+ parameters:
17
+ icon: git-alt
18
+ actions:
19
+ onClick:
20
+ type: link
21
+ to: Contained page 2
22
+ - name: Contained page 2
23
+ blocks:
24
+ - type: action-button
25
+ version: 0.29.8
26
+ parameters:
27
+ icon: git-alt
28
+ actions:
29
+ onClick:
30
+ type: link
31
+ to: Contained page 1
32
+ \`\`\`
33
+ `,
34
+ required: ['type', 'pages'],
35
+ properties: {
36
+ type: {
37
+ enum: ['container'],
38
+ },
39
+ pages: {
40
+ type: 'array',
41
+ minItems: 1,
42
+ description: 'The pages of the app.',
43
+ items: {
44
+ anyOf: [
45
+ { $ref: '#/components/schemas/PageDefinition' },
46
+ { $ref: '#/components/schemas/TabsPageDefinition' },
47
+ { $ref: '#/components/schemas/FlowPageDefinition' },
48
+ { $ref: '#/components/schemas/LoopPageDefinition' },
49
+ { $ref: '#/components/schemas/ContainerPageDefinition' },
50
+ ],
51
+ },
52
+ },
53
+ },
54
+ });
55
+ //# sourceMappingURL=ContainerPageDefinition.js.map
@@ -20,6 +20,7 @@ export * from './BlockVersion.js';
20
20
  export * from './ConditionActionDefinition.js';
21
21
  export * from './CronDefinition.js';
22
22
  export * from './ContainerDefinition.js';
23
+ export * from './ContainerPageDefinition.js';
23
24
  export * from './CustomFontDefinition.js';
24
25
  export * from './DialogActionDefinition.js';
25
26
  export * from './DialogErrorActionDefinition.js';
@@ -20,6 +20,7 @@ export * from './BlockVersion.js';
20
20
  export * from './ConditionActionDefinition.js';
21
21
  export * from './CronDefinition.js';
22
22
  export * from './ContainerDefinition.js';
23
+ export * from './ContainerPageDefinition.js';
23
24
  export * from './CustomFontDefinition.js';
24
25
  export * from './DialogActionDefinition.js';
25
26
  export * from './DialogErrorActionDefinition.js';
@@ -0,0 +1,2 @@
1
+ import { type PageDefinition } from '@appsemble/types';
2
+ export declare function findPageByName(pages: PageDefinition[], name: string): PageDefinition | undefined;
@@ -0,0 +1,15 @@
1
+ export function findPageByName(pages, name) {
2
+ for (const page of pages) {
3
+ if (page.name === name) {
4
+ return page;
5
+ }
6
+ if (page.type === 'container' && page.pages) {
7
+ const found = findPageByName(page.pages, name);
8
+ if (found) {
9
+ return found;
10
+ }
11
+ }
12
+ }
13
+ return undefined;
14
+ }
15
+ //# sourceMappingURL=findPageByName.js.map
package/index.d.ts CHANGED
@@ -31,3 +31,4 @@ export * from './serializeResource.js';
31
31
  export * from './convertToCsv.js';
32
32
  export * from './assets.js';
33
33
  export * from './validateAppMessages.js';
34
+ export * from './findPageByName.js';
package/index.js CHANGED
@@ -31,4 +31,5 @@ export * from './serializeResource.js';
31
31
  export * from './convertToCsv.js';
32
32
  export * from './assets.js';
33
33
  export * from './validateAppMessages.js';
34
+ export * from './findPageByName.js';
34
35
  //# sourceMappingURL=index.js.map
package/iterApp.js CHANGED
@@ -119,6 +119,13 @@ export function iterPage(page, callbacks, prefix = []) {
119
119
  return (result ||
120
120
  ['steps.first', 'steps', 'steps.last'].some((suffix) => iterBlockList(page.foreach.blocks, callbacks, [...prefix, suffix, 'blocks'])));
121
121
  }
122
+ if (page.type === 'container') {
123
+ let result = false;
124
+ for (const containerPage of page.pages) {
125
+ result = iterPage(containerPage, callbacks, prefix);
126
+ }
127
+ return result;
128
+ }
122
129
  return iterBlockList(page.blocks, callbacks, [...prefix, 'blocks']);
123
130
  }
124
131
  /**
package/iterApp.test.js CHANGED
@@ -235,6 +235,33 @@ describe('iterPage', () => {
235
235
  expect(onPage).toHaveBeenCalledWith(page, []);
236
236
  expect(result).toBe(false);
237
237
  });
238
+ it('should iterate over a container page', () => {
239
+ const onBlockList = vi.fn();
240
+ const onPage = vi.fn();
241
+ const page = {
242
+ name: 'Container Page',
243
+ type: 'container',
244
+ pages: [
245
+ {
246
+ name: 'Page 1',
247
+ blocks: [],
248
+ },
249
+ {
250
+ name: 'Page 2',
251
+ blocks: [],
252
+ },
253
+ ],
254
+ };
255
+ const result = iterPage(page, { onBlockList, onPage });
256
+ expect(onBlockList).toHaveBeenCalledWith(page.pages[0].blocks, [
257
+ 'blocks',
258
+ ]);
259
+ expect(onBlockList).toHaveBeenCalledWith(page.pages[1].blocks, [
260
+ 'blocks',
261
+ ]);
262
+ expect(onPage).toHaveBeenCalledWith(page, []);
263
+ expect(result).toBe(false);
264
+ });
238
265
  it('should abort if onPage returns true', () => {
239
266
  const onBlockList = vi.fn();
240
267
  const onPage = vi.fn().mockReturnValue(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/utils",
3
- "version": "0.29.9",
3
+ "version": "0.29.11",
4
4
  "description": "Utility functions used in Appsemble internally",
5
5
  "keywords": [
6
6
  "app",
@@ -37,7 +37,7 @@
37
37
  "test": "vitest"
38
38
  },
39
39
  "dependencies": {
40
- "@appsemble/types": "0.29.9",
40
+ "@appsemble/types": "0.29.11",
41
41
  "axios": "^1.0.0",
42
42
  "cron-parser": "^4.0.0",
43
43
  "date-fns": "^2.0.0",
@@ -46,7 +46,7 @@ export function validateMessages(messages, app) {
46
46
  }
47
47
  const coreMessages = (_a = messages.core) !== null && _a !== void 0 ? _a : {};
48
48
  for (const [key, value] of Object.entries(coreMessages)) {
49
- if (!value || typeof value !== 'string') {
49
+ if (typeof value !== 'string') {
50
50
  throw new AppMessageValidationError(`Invalid translation key: core.${key}`);
51
51
  }
52
52
  }
package/validation.js CHANGED
@@ -3,7 +3,7 @@ import { ValidationError, Validator } from 'jsonschema';
3
3
  import languageTags from 'language-tags';
4
4
  import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
5
5
  import { has } from './has.js';
6
- import { partialNormalized } from './index.js';
6
+ import { findPageByName, normalize, partialNormalized } from './index.js';
7
7
  import { iterApp } from './iterApp.js';
8
8
  import { serverActions } from './serverActions.js';
9
9
  /**
@@ -34,6 +34,37 @@ function validateJSONSchema(schema, prefix, report) {
34
34
  }
35
35
  }
36
36
  }
37
+ /**
38
+ * Validates the pages in the app definition to ensure there are no duplicate page names.
39
+ *
40
+ * @param definition The definition of the app
41
+ * @param report A function used to report a value.
42
+ */
43
+ function validateUniquePageNames(definition, report) {
44
+ if (!definition.pages) {
45
+ return;
46
+ }
47
+ const pageNames = new Map();
48
+ function checkPages(pages, parentPath = []) {
49
+ for (const page of pages) {
50
+ const pageName = page.name;
51
+ const normalizedPageName = normalize(page.name);
52
+ const pagePath = [...parentPath, pageName];
53
+ if (pageNames.has(normalizedPageName)) {
54
+ const paths = pageNames.get(normalizedPageName);
55
+ paths.push(pagePath);
56
+ report(pageName, 'is a duplicate page name', pagePath);
57
+ }
58
+ else {
59
+ pageNames.set(normalizedPageName, [pagePath]);
60
+ }
61
+ if (page.type === 'container') {
62
+ checkPages(page.pages, pagePath);
63
+ }
64
+ }
65
+ }
66
+ checkPages(definition.pages);
67
+ }
37
68
  function validateUsersSchema(definition, report) {
38
69
  var _a;
39
70
  if (!definition.users) {
@@ -462,7 +493,7 @@ function validateLanguage({ defaultLanguage }, report) {
462
493
  }
463
494
  }
464
495
  function validateDefaultPage({ defaultPage, pages }, report) {
465
- const page = pages === null || pages === void 0 ? void 0 : pages.find((p) => p.name === defaultPage);
496
+ const page = findPageByName(pages, defaultPage);
466
497
  if (!page) {
467
498
  report(defaultPage, 'does not refer to an existing page', ['defaultPage']);
468
499
  return;
@@ -606,7 +637,7 @@ function validateActions(definition, report) {
606
637
  return;
607
638
  }
608
639
  const [toBase, toSub] = [].concat(to);
609
- const toPage = definition.pages.find(({ name }) => name === toBase);
640
+ const toPage = findPageByName(definition.pages, toBase);
610
641
  if (!toPage) {
611
642
  report(to, 'refers to a page that doesn’t exist', [...path, 'to']);
612
643
  return;
@@ -833,6 +864,7 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
833
864
  validateBlocks(definition, blockVersionMap, report);
834
865
  validateActions(definition, report);
835
866
  validateEvents(definition, blockVersionMap, report);
867
+ validateUniquePageNames(definition, report);
836
868
  }
837
869
  catch (error) {
838
870
  report(null, `Unexpected error: ${error instanceof Error ? error.message : error}`, []);
@@ -43,6 +43,16 @@ function createTestApp() {
43
43
  { name: 'Step B', blocks: [] },
44
44
  ],
45
45
  },
46
+ {
47
+ name: 'Container Page 1',
48
+ type: 'container',
49
+ pages: [
50
+ {
51
+ name: 'Contained Page',
52
+ blocks: [],
53
+ },
54
+ ],
55
+ },
46
56
  ],
47
57
  };
48
58
  }
@@ -85,6 +95,24 @@ describe('validateAppDefinition', () => {
85
95
  ]),
86
96
  ]);
87
97
  });
98
+ it('should report duplicate page names', async () => {
99
+ const app = createTestApp();
100
+ app.pages.push({
101
+ name: 'Container Page',
102
+ type: 'container',
103
+ pages: [{ name: 'Test Page', blocks: [] }],
104
+ });
105
+ const result = await validateAppDefinition(app, () => [
106
+ { name: '@appsemble/test', version: '0.0.0', files: [], languages: [] },
107
+ ]);
108
+ expect(result.valid).toBe(false);
109
+ expect(result.errors).toStrictEqual([
110
+ new ValidationError('is a duplicate page name', 'Test Page', undefined, [
111
+ 'Container Page',
112
+ 'Test Page',
113
+ ]),
114
+ ]);
115
+ });
88
116
  it('should validate block parameters', async () => {
89
117
  const app = createTestApp();
90
118
  app.pages[0].blocks.push({
@@ -1445,6 +1473,10 @@ describe('validateAppDefinition', () => {
1445
1473
  ]),
1446
1474
  ]);
1447
1475
  });
1476
+ it('should check if the default page exists inside contained page', async () => {
1477
+ const result = await validateAppDefinition({ ...createTestApp(), defaultPage: 'Contained Page' }, () => []);
1478
+ expect(result.valid).toBe(true);
1479
+ });
1448
1480
  it('should validate the default page doesn’t specify parameters', async () => {
1449
1481
  const result = await validateAppDefinition({ ...createTestApp(), defaultPage: 'Page with parameters' }, () => []);
1450
1482
  expect(result.valid).toBe(false);