@neovici/cosmoz-form 4.0.0 → 4.1.0

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.
@@ -5,10 +5,21 @@ import { UseValidatedForm } from '../use-validated-form-core';
5
5
  import { Progress } from './types';
6
6
  export declare const nothing$: () => symbol;
7
7
  export declare const then$: <P, R>(p$?: PromiseLike<P>, fn?: () => R) => Promise<R>;
8
+ /**
9
+ * A save failure. `message` is the plain-text summary (toasts, logging);
10
+ * `content`, when present, is rendered in the failure block instead —
11
+ * allowing rich markup (e.g. a structured list of row errors). The failure
12
+ * block renders with `white-space: pre-wrap`, so keep templates free of
13
+ * indentation whitespace.
14
+ */
15
+ export interface Failure {
16
+ message: string;
17
+ content?: unknown;
18
+ }
8
19
  interface RenderAddFields<T extends object> extends UseValidatedForm<T> {
9
- error?: Error | {
20
+ error?: Error | ({
10
21
  message: string;
11
- };
22
+ } & Partial<Failure>);
12
23
  }
13
24
  export declare const renderAddFields: <T extends object>({ error, ...thru }: RenderAddFields<T>) => import("..").Renderable[];
14
25
  export declare const renderFailure$: <T>(save$?: PromiseLike<T>) => import("lit-html/directive.js").DirectiveResult<{
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/add/render.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAAQ,OAAO,EAAE,MAAM,UAAU,CAAC;AAKzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,eAAO,MAAM,QAAQ,cAAgB,CAAC;AACtC,eAAO,MAAM,KAAK,GAAI,CAAC,EAAE,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,eAC5B,CAAC;AAElC,UAAU,eAAe,CAAC,CAAC,SAAS,MAAM,CAAE,SAAQ,gBAAgB,CAAC,CAAC,CAAC;IACtE,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACpC;AACD,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,oBAG/C,eAAe,CAAC,CAAC,CAAC,8BAGpB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,QAAQ,WAAW,CAAC,CAAC,CAAC;;EAItD,CAAC;AAEH,UAAU,MAAM;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AACD,UAAU,YAAa,SAAQ,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC;IACtD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAC7B;AACD,eAAO,MAAM,aAAa,GAAI,8BAA8B,YAAY;;EAiCvE,CAAC;AAEF,UAAU,aAAa;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;CACnB;AACD,eAAO,MAAM,cAAc,GACzB,CAAC,SAAS,MAAM,EAAE,8CAKhB,aAAa,MACf,sCAAsC,iBAAiB,CAAC,CAAC,CAAC,yCAUpC,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/add/render.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAAQ,OAAO,EAAE,MAAM,UAAU,CAAC;AAKzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,eAAO,MAAM,QAAQ,cAAgB,CAAC;AACtC,eAAO,MAAM,KAAK,GAAI,CAAC,EAAE,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,eAC5B,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAID,UAAU,eAAe,CAAC,CAAC,SAAS,MAAM,CAAE,SAAQ,gBAAgB,CAAC,CAAC,CAAC;IACtE,KAAK,CAAC,EAAE,KAAK,GAAG,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;CACzD;AACD,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,EAAE,oBAG/C,eAAe,CAAC,CAAC,CAAC,8BAGpB,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,QAAQ,WAAW,CAAC,CAAC,CAAC;;EAOtD,CAAC;AAEH,UAAU,MAAM;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AACD,UAAU,YAAa,SAAQ,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC;IACtD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAC7B;AACD,eAAO,MAAM,aAAa,GAAI,8BAA8B,YAAY;;EAiCvE,CAAC;AAEF,UAAU,aAAa;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;CACnB;AACD,eAAO,MAAM,cAAc,GACzB,CAAC,SAAS,MAAM,EAAE,8CAKhB,aAAa,MACf,sCAAsC,iBAAiB,CAAC,CAAC,CAAC,yCAUpC,CAAC"}
@@ -7,11 +7,12 @@ import { when } from 'lit-html/directives/when.js';
7
7
  import { renderFields } from '../render';
8
8
  export const nothing$ = () => nothing;
9
9
  export const then$ = (p$, fn) => Promise.resolve(p$).then(fn, fn);
10
+ const failureHtml = (e) => e.content ?? e.message;
10
11
  export const renderAddFields = ({ error, ...thru }) => [
11
12
  renderFields(thru),
12
- when(error, (err) => html `<div class="failure">${err.message}</div>`),
13
+ when(error, (err) => html `<div class="failure">${failureHtml(err)}</div>`),
13
14
  ];
14
- export const renderFailure$ = (save$) => until(save$?.then(nothing$, (e) => html `<div class="failure">${e.message}</div>`), nothing);
15
+ export const renderFailure$ = (save$) => until(save$?.then(nothing$, (e) => html `<div class="failure">${failureHtml(e)}</div>`), nothing);
15
16
  export const renderButton$ = ({ save$, progress, ...thru }) => {
16
17
  const button = ({ onSave, onClick = onSave, title, disabled, progress, content = nothing, slot, }) => html ` <cosmoz-button
17
18
  class="button save"
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/add/styles.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,cAmB3B,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/add/styles.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,cA2B3B,CAAC"}
@@ -11,7 +11,15 @@ export const renderAddStyles = () => css `
11
11
  font-size: var(--cz-text-xs);
12
12
  line-height: var(--cz-text-xs-line-height);
13
13
  margin: calc(var(--cz-spacing) * 2) 0;
14
+ padding: calc(var(--cz-spacing) * 3);
14
15
  color: var(--cz-color-text-error);
16
+ background: var(--cz-color-bg-error);
17
+ border: 1px solid var(--cz-color-border-error-subtle);
18
+ border-radius: var(--cz-radius-lg);
19
+ white-space: pre-wrap;
20
+ word-break: break-word;
21
+ max-height: 40vh;
22
+ overflow-y: auto;
15
23
  }
16
24
 
17
25
  .input-toggle {
@@ -26,8 +26,8 @@ const FormDialog = (host) => {
26
26
  </style>
27
27
  ${when(description, () => html `<div class="description">${description}</div>`)}
28
28
  <div class="form" part="form">${renderFields(form)}</div>
29
+ ${renderFailure$(save$)}
29
30
  <div class="buttons">
30
- ${renderFailure$(save$)}
31
31
  ${renderButton$({ save$, onSave, disabled, title: saveText, progress })}
32
32
  ${when(!hideCancelButton, () => html `<cosmoz-button
33
33
  class="button"
@@ -1 +1 @@
1
- {"version":3,"file":"style.css.d.ts","sourceRoot":"","sources":["../../src/form-dialog/style.css.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM,QAoBX,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"style.css.d.ts","sourceRoot":"","sources":["../../src/form-dialog/style.css.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM,QA4BX,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -16,8 +16,16 @@ const styles = css `
16
16
 
17
17
  .failure {
18
18
  color: var(--cz-color-text-error);
19
- align-self: center;
20
- flex: 1;
19
+ background: var(--cz-color-bg-error);
20
+ border: 1px solid var(--cz-color-border-error-subtle);
21
+ border-radius: var(--cz-radius-lg);
22
+ /* spans the dialog body's content box; text inset by its own padding */
23
+ margin: calc(var(--cz-spacing) * 2) 0;
24
+ padding: calc(var(--cz-spacing) * 3);
25
+ white-space: pre-wrap;
26
+ word-break: break-word;
27
+ max-height: 40vh;
28
+ overflow-y: auto;
21
29
  }
22
30
  `;
23
31
  export default styles;
@@ -0,0 +1,2 @@
1
+ import '../form-dialog/form-dialog.js';
2
+ //# sourceMappingURL=form-dialog.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-dialog.test.d.ts","sourceRoot":"","sources":["../../src/test/form-dialog.test.ts"],"names":[],"mappings":"AAGA,OAAO,+BAA+B,CAAC"}
@@ -0,0 +1,64 @@
1
+ import { assert, fixture, waitUntil } from '@open-wc/testing';
2
+ import { html } from 'lit-html';
3
+ import '../form-dialog/form-dialog.js';
4
+ // design tokens the alert-card chrome relies on; applied via inline style on
5
+ // the host so they inherit into its shadow root (the app loads them globally)
6
+ const tokenStyle = '--cz-color-bg-error: #fef3f2; --cz-color-border-error-subtle: #fda29b; --cz-radius-lg: 0.625rem; --cz-spacing: 0.25rem; --cz-color-text-error: #d92d20;';
7
+ const LONG_ERROR = Array.from({ length: 8 }, (_, i) => `Row ${1078 + i}: Category 'Sheeeesh' is not valid.`).join(' ');
8
+ const openFailingDialog = async (onSave) => {
9
+ const el = (await fixture(html `
10
+ <cosmoz-form-dialog
11
+ heading="Test"
12
+ style=${tokenStyle}
13
+ .fields=${[]}
14
+ .initial=${{}}
15
+ .onSave=${onSave}
16
+ ></cosmoz-form-dialog>
17
+ `));
18
+ const root = el.shadowRoot;
19
+ const save = root.querySelector('.buttons .save');
20
+ assert.ok(save, 'save button rendered');
21
+ save.click();
22
+ await waitUntil(() => root.querySelector('.failure') != null);
23
+ return { el, root };
24
+ };
25
+ suite('cosmoz-form-dialog failure rendering', () => {
26
+ test('failure renders above the footer, not inside the buttons row', async () => {
27
+ const { root } = await openFailingDialog(() => Promise.reject(new Error('Short failure')));
28
+ const failure = root.querySelector('.failure'), buttons = root.querySelector('.buttons');
29
+ assert.include(failure.textContent ?? '', 'Short failure');
30
+ assert.isNull(buttons.querySelector('.failure'), 'footer must contain only buttons');
31
+ assert.isTrue(failure.compareDocumentPosition(buttons) ===
32
+ Node.DOCUMENT_POSITION_FOLLOWING, 'failure block precedes the footer');
33
+ });
34
+ test('long failure is capped and scrollable', async () => {
35
+ const { root } = await openFailingDialog(() => Promise.reject(new Error(LONG_ERROR)));
36
+ const failure = root.querySelector('.failure');
37
+ const style = getComputedStyle(failure);
38
+ assert.equal(style.overflowY, 'auto');
39
+ // 40vh resolves to a px value; assert it is a bounded, finite cap
40
+ assert.isTrue(style.maxHeight.endsWith('px'));
41
+ assert.isBelow(Number.parseFloat(style.maxHeight), 500);
42
+ assert.equal(style.whiteSpace, 'pre-wrap');
43
+ });
44
+ test('failure renders as an error alert card', async () => {
45
+ const { root } = await openFailingDialog(() => Promise.reject(new Error(LONG_ERROR)));
46
+ const style = getComputedStyle(root.querySelector('.failure'));
47
+ assert.notEqual(style.backgroundColor, 'rgba(0, 0, 0, 0)', 'has a tinted background');
48
+ assert.notEqual(style.borderWidth, '0px', 'has a border');
49
+ assert.notEqual(style.borderRadius, '0px', 'is rounded');
50
+ });
51
+ test('rich failure content (e.content) renders instead of the message', async () => {
52
+ const { root } = await openFailingDialog(() => {
53
+ const err = new Error('The file contains 3 errors');
54
+ err.content = html `
55
+ <div><b>The file contains 3 errors</b></div>
56
+ <div name="row-1">Row 1078: Category 'Sheeeesh' is not valid.</div>
57
+ `;
58
+ return Promise.reject(err);
59
+ });
60
+ const failure = root.querySelector('.failure');
61
+ assert.equal(failure.querySelector('b')?.textContent, 'The file contains 3 errors');
62
+ assert.include(failure.querySelector('[name="row-1"]')?.textContent ?? '', 'Row 1078: Category \'Sheeeesh\' is not valid.');
63
+ });
64
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-form",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "A pionjs component",
5
5
  "keywords": [
6
6
  "web-components",