@things-factory/auth-ui 10.1.16 → 10.1.17

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 (70) hide show
  1. package/dist-client/components/secret-field.d.ts +35 -0
  2. package/dist-client/components/secret-field.js +149 -0
  3. package/dist-client/components/secret-field.js.map +1 -0
  4. package/dist-client/pages/app-binding/app-binding-list-query.test.d.ts +1 -0
  5. package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
  6. package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
  7. package/dist-client/pages/app-binding/app-binding.d.ts +1 -0
  8. package/dist-client/pages/app-binding/app-binding.js +8 -8
  9. package/dist-client/pages/app-binding/app-binding.js.map +1 -1
  10. package/dist-client/pages/app-binding/app-bindings.d.ts +63 -2
  11. package/dist-client/pages/app-binding/app-bindings.js +199 -117
  12. package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
  13. package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
  14. package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
  15. package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
  16. package/dist-client/pages/appliance/appliance.d.ts +1 -0
  17. package/dist-client/pages/appliance/appliance.js +2 -4
  18. package/dist-client/pages/appliance/appliance.js.map +1 -1
  19. package/dist-client/pages/appliance/home.d.ts +61 -2
  20. package/dist-client/pages/appliance/home.js +200 -122
  21. package/dist-client/pages/appliance/home.js.map +1 -1
  22. package/dist-client/pages/application/application.d.ts +1 -0
  23. package/dist-client/pages/application/application.js +10 -3
  24. package/dist-client/pages/application/application.js.map +1 -1
  25. package/dist-client/pages/application/applications.d.ts +61 -2
  26. package/dist-client/pages/application/applications.js +193 -128
  27. package/dist-client/pages/application/applications.js.map +1 -1
  28. package/dist-client/pages/application/register.js +81 -27
  29. package/dist-client/pages/application/register.js.map +1 -1
  30. package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
  31. package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
  32. package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
  33. package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
  34. package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
  35. package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
  36. package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
  37. package/dist-client/pages/secrets-are-not-printed.test.js +70 -0
  38. package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
  39. package/dist-client/tsconfig.tsbuildinfo +1 -1
  40. package/dist-client/utils/application-status.d.ts +32 -0
  41. package/dist-client/utils/application-status.js +45 -0
  42. package/dist-client/utils/application-status.js.map +1 -0
  43. package/dist-client/utils/application-status.test.d.ts +1 -0
  44. package/dist-client/utils/application-status.test.js +45 -0
  45. package/dist-client/utils/application-status.test.js.map +1 -0
  46. package/dist-client/utils/form-values.d.ts +42 -0
  47. package/dist-client/utils/form-values.js +57 -0
  48. package/dist-client/utils/form-values.js.map +1 -0
  49. package/dist-client/utils/form-values.test.d.ts +1 -0
  50. package/dist-client/utils/form-values.test.js +62 -0
  51. package/dist-client/utils/form-values.test.js.map +1 -0
  52. package/dist-client/utils/list-result.d.ts +62 -0
  53. package/dist-client/utils/list-result.js +92 -0
  54. package/dist-client/utils/list-result.js.map +1 -0
  55. package/dist-client/utils/list-result.test.d.ts +1 -0
  56. package/dist-client/utils/list-result.test.js +160 -0
  57. package/dist-client/utils/list-result.test.js.map +1 -0
  58. package/dist-client/utils/secret-display.d.ts +51 -0
  59. package/dist-client/utils/secret-display.js +61 -0
  60. package/dist-client/utils/secret-display.js.map +1 -0
  61. package/dist-client/utils/secret-display.test.d.ts +1 -0
  62. package/dist-client/utils/secret-display.test.js +59 -0
  63. package/dist-client/utils/secret-display.test.js.map +1 -0
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +3 -3
  66. package/translations/en.json +31 -0
  67. package/translations/ja.json +31 -0
  68. package/translations/ko.json +31 -0
  69. package/translations/ms.json +31 -0
  70. package/translations/zh.json +31 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * How an application's status is shown.
3
+ *
4
+ * ── The rule that matters: a value we do not know is shown as it is ──────────
5
+ * `ApplicationStatus` is DRAFT or ACTIVATED today. A screen that maps those two to friendly
6
+ * words has to decide what to do with a third, and the tempting answer — fall through to one
7
+ * of the two, or to an empty cell — is the one that lies.
8
+ *
9
+ * This is the same mistake the role screen made with privilege axes, where anything that was
10
+ * not `query` was drawn as "write" and so a value first seen on the day it shipped landed in
11
+ * the dangerous bucket by accident rather than by verification. The fix there and here is the
12
+ * same: translate what is declared, and print anything else verbatim.
13
+ *
14
+ * An unknown status is not a failure either — the server is free to add one, and a screen a
15
+ * version behind should say "PENDING" rather than pretend it knows what PENDING means.
16
+ */
17
+ /** The statuses this screen has words for. Everything else is printed as it arrives. */
18
+ export declare const KNOWN_APPLICATION_STATUSES: string[];
19
+ /**
20
+ * The i18n key for a status, or nothing when there is no key for it.
21
+ *
22
+ * Returning nothing rather than a guess lets the caller print the raw value, which is the
23
+ * whole point. `translate` is passed in so this stays testable without i18next.
24
+ */
25
+ export declare function applicationStatusKey(status: string | null | undefined): string | undefined;
26
+ /**
27
+ * What the cell shows.
28
+ *
29
+ * Empty for a missing status — the row genuinely has none, and an empty cell says that more
30
+ * honestly than the word "unknown", which reads as a status of its own.
31
+ */
32
+ export declare function applicationStatusLabel(status: string | null | undefined, translate: (key: string) => string): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * How an application's status is shown.
3
+ *
4
+ * ── The rule that matters: a value we do not know is shown as it is ──────────
5
+ * `ApplicationStatus` is DRAFT or ACTIVATED today. A screen that maps those two to friendly
6
+ * words has to decide what to do with a third, and the tempting answer — fall through to one
7
+ * of the two, or to an empty cell — is the one that lies.
8
+ *
9
+ * This is the same mistake the role screen made with privilege axes, where anything that was
10
+ * not `query` was drawn as "write" and so a value first seen on the day it shipped landed in
11
+ * the dangerous bucket by accident rather than by verification. The fix there and here is the
12
+ * same: translate what is declared, and print anything else verbatim.
13
+ *
14
+ * An unknown status is not a failure either — the server is free to add one, and a screen a
15
+ * version behind should say "PENDING" rather than pretend it knows what PENDING means.
16
+ */
17
+ /** The statuses this screen has words for. Everything else is printed as it arrives. */
18
+ export const KNOWN_APPLICATION_STATUSES = ['DRAFT', 'ACTIVATED'];
19
+ const LABEL = {
20
+ DRAFT: 'label.draft',
21
+ ACTIVATED: 'label.activated'
22
+ };
23
+ /**
24
+ * The i18n key for a status, or nothing when there is no key for it.
25
+ *
26
+ * Returning nothing rather than a guess lets the caller print the raw value, which is the
27
+ * whole point. `translate` is passed in so this stays testable without i18next.
28
+ */
29
+ export function applicationStatusKey(status) {
30
+ return status ? LABEL[status] : undefined;
31
+ }
32
+ /**
33
+ * What the cell shows.
34
+ *
35
+ * Empty for a missing status — the row genuinely has none, and an empty cell says that more
36
+ * honestly than the word "unknown", which reads as a status of its own.
37
+ */
38
+ export function applicationStatusLabel(status, translate) {
39
+ if (!status) {
40
+ return '';
41
+ }
42
+ const key = applicationStatusKey(status);
43
+ return key ? translate(key) : status;
44
+ }
45
+ //# sourceMappingURL=application-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-status.js","sourceRoot":"","sources":["../../client/utils/application-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,wFAAwF;AACxF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;AAEhE,MAAM,KAAK,GAA2B;IACpC,KAAK,EAAE,aAAa;IACpB,SAAS,EAAE,iBAAiB;CAC7B,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAiC;IACpE,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAiC,EACjC,SAAkC;IAElC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,GAAG,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAA;IAExC,OAAO,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;AACtC,CAAC","sourcesContent":["/**\n * How an application's status is shown.\n *\n * ── The rule that matters: a value we do not know is shown as it is ──────────\n * `ApplicationStatus` is DRAFT or ACTIVATED today. A screen that maps those two to friendly\n * words has to decide what to do with a third, and the tempting answer — fall through to one\n * of the two, or to an empty cell — is the one that lies.\n *\n * This is the same mistake the role screen made with privilege axes, where anything that was\n * not `query` was drawn as \"write\" and so a value first seen on the day it shipped landed in\n * the dangerous bucket by accident rather than by verification. The fix there and here is the\n * same: translate what is declared, and print anything else verbatim.\n *\n * An unknown status is not a failure either — the server is free to add one, and a screen a\n * version behind should say \"PENDING\" rather than pretend it knows what PENDING means.\n */\n\n/** The statuses this screen has words for. Everything else is printed as it arrives. */\nexport const KNOWN_APPLICATION_STATUSES = ['DRAFT', 'ACTIVATED']\n\nconst LABEL: Record<string, string> = {\n DRAFT: 'label.draft',\n ACTIVATED: 'label.activated'\n}\n\n/**\n * The i18n key for a status, or nothing when there is no key for it.\n *\n * Returning nothing rather than a guess lets the caller print the raw value, which is the\n * whole point. `translate` is passed in so this stays testable without i18next.\n */\nexport function applicationStatusKey(status: string | null | undefined): string | undefined {\n return status ? LABEL[status] : undefined\n}\n\n/**\n * What the cell shows.\n *\n * Empty for a missing status — the row genuinely has none, and an empty cell says that more\n * honestly than the word \"unknown\", which reads as a status of its own.\n */\nexport function applicationStatusLabel(\n status: string | null | undefined,\n translate: (key: string) => string\n): string {\n if (!status) {\n return ''\n }\n\n const key = applicationStatusKey(status)\n\n return key ? translate(key) : status\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { applicationStatusKey, applicationStatusLabel, KNOWN_APPLICATION_STATUSES } from './application-status';
2
+ /**
3
+ * The applications list used to print "draft" for every row, hardcoded, so an activated
4
+ * application read as a draft. The column now comes from the row — and the question that
5
+ * replaces it is what to print for a value this screen has no word for.
6
+ */
7
+ const translate = (key) => ({ 'label.draft': '초안', 'label.activated': '사용 중' })[key] || key;
8
+ describe('the statuses this screen has words for', () => {
9
+ it('translates the two the server declares', () => {
10
+ expect(applicationStatusLabel('DRAFT', translate)).toBe('초안');
11
+ expect(applicationStatusLabel('ACTIVATED', translate)).toBe('사용 중');
12
+ expect(KNOWN_APPLICATION_STATUSES).toEqual(['DRAFT', 'ACTIVATED']);
13
+ });
14
+ });
15
+ describe('a status this screen has never seen', () => {
16
+ it('prints it as it arrived instead of guessing', () => {
17
+ /*
18
+ * ⚠ The case this file exists for. The server may add a status; a client one release
19
+ * behind must not decide what it means. Mapping the unknown onto DRAFT or ACTIVATED
20
+ * would make the screen wrong with full confidence.
21
+ */
22
+ expect(applicationStatusLabel('PENDING', translate)).toBe('PENDING');
23
+ expect(applicationStatusKey('PENDING')).toBeUndefined();
24
+ });
25
+ it('does not fall through to either known word', () => {
26
+ const shown = applicationStatusLabel('SUSPENDED', translate);
27
+ expect(shown).not.toBe('초안');
28
+ expect(shown).not.toBe('사용 중');
29
+ });
30
+ it('is not treated as an error — it is a status, just an unfamiliar one', () => {
31
+ expect(applicationStatusLabel('PENDING', translate)).toBeTruthy();
32
+ });
33
+ });
34
+ describe('a row with no status at all', () => {
35
+ it('shows nothing rather than a word', () => {
36
+ /*
37
+ * Empty, not "unknown". "Unknown" reads as a status the server assigned; empty reads as
38
+ * the absence this actually is.
39
+ */
40
+ expect(applicationStatusLabel(undefined, translate)).toBe('');
41
+ expect(applicationStatusLabel(null, translate)).toBe('');
42
+ expect(applicationStatusLabel('', translate)).toBe('');
43
+ });
44
+ });
45
+ //# sourceMappingURL=application-status.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-status.test.js","sourceRoot":"","sources":["../../client/utils/application-status.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAA;AAE/G;;;;GAIG;AAEH,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,CAAA;AAEnG,QAAQ,CAAC,wCAAwC,EAAE,GAAG,EAAE;IACtD,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,sBAAsB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7D,MAAM,CAAC,sBAAsB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACnE,MAAM,CAAC,0BAA0B,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD;;;;WAIG;QACH,MAAM,CAAC,sBAAsB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACpE,MAAM,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IACzD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,KAAK,GAAG,sBAAsB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;QAE5D,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,CAAC,sBAAsB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C;;;WAGG;QACH,MAAM,CAAC,sBAAsB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC7D,MAAM,CAAC,sBAAsB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACxD,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACxD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { applicationStatusKey, applicationStatusLabel, KNOWN_APPLICATION_STATUSES } from './application-status'\n\n/**\n * The applications list used to print \"draft\" for every row, hardcoded, so an activated\n * application read as a draft. The column now comes from the row — and the question that\n * replaces it is what to print for a value this screen has no word for.\n */\n\nconst translate = (key: string) => ({ 'label.draft': '초안', 'label.activated': '사용 중' })[key] || key\n\ndescribe('the statuses this screen has words for', () => {\n it('translates the two the server declares', () => {\n expect(applicationStatusLabel('DRAFT', translate)).toBe('초안')\n expect(applicationStatusLabel('ACTIVATED', translate)).toBe('사용 중')\n expect(KNOWN_APPLICATION_STATUSES).toEqual(['DRAFT', 'ACTIVATED'])\n })\n})\n\ndescribe('a status this screen has never seen', () => {\n it('prints it as it arrived instead of guessing', () => {\n /*\n * ⚠ The case this file exists for. The server may add a status; a client one release\n * behind must not decide what it means. Mapping the unknown onto DRAFT or ACTIVATED\n * would make the screen wrong with full confidence.\n */\n expect(applicationStatusLabel('PENDING', translate)).toBe('PENDING')\n expect(applicationStatusKey('PENDING')).toBeUndefined()\n })\n\n it('does not fall through to either known word', () => {\n const shown = applicationStatusLabel('SUSPENDED', translate)\n\n expect(shown).not.toBe('초안')\n expect(shown).not.toBe('사용 중')\n })\n\n it('is not treated as an error — it is a status, just an unfamiliar one', () => {\n expect(applicationStatusLabel('PENDING', translate)).toBeTruthy()\n })\n})\n\ndescribe('a row with no status at all', () => {\n it('shows nothing rather than a word', () => {\n /*\n * Empty, not \"unknown\". \"Unknown\" reads as a status the server assigned; empty reads as\n * the absence this actually is.\n */\n expect(applicationStatusLabel(undefined, translate)).toBe('')\n expect(applicationStatusLabel(null, translate)).toBe('')\n expect(applicationStatusLabel('', translate)).toBe('')\n })\n})\n"]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Turning a form into the object a mutation takes.
3
+ *
4
+ * ── The bug this exists for ──────────────────────────────────────────────────
5
+ * The application register screen built its input like this:
6
+ *
7
+ * Array.from(formData.entries()).reduce((app, [key, value]) => { app[key] = value; … })
8
+ *
9
+ * `FormData` carries **every named input**, including the ones nobody typed in, each as an
10
+ * empty string. So an untouched optional field went to the server as `''` rather than as
11
+ * absent — and for `email` that is not the same thing at all:
12
+ *
13
+ * NewApplication.email is GraphQLEmailAddress, nullable
14
+ * EmailAddressResolver.parseValue('') → "Value is not a valid email address"
15
+ *
16
+ * Registering an application without a contact email therefore failed, on a field the schema
17
+ * marks optional. The screen said nothing useful about why.
18
+ *
19
+ * ── What "absent" means here ─────────────────────────────────────────────────
20
+ * An empty text input means the person did not fill it in. It does not mean "set this to the
21
+ * empty string" — HTML has no way to say that, and no field in these forms wants it. So an
22
+ * empty value is dropped and the key never appears, which is what "optional" means on the
23
+ * wire.
24
+ *
25
+ * Whitespace counts as empty for the same reason: a space typed by accident is not an answer,
26
+ * and every scalar that rejects `''` rejects `' '` too.
27
+ */
28
+ /** Values a form can carry. Files pass through untouched — there is no empty file to drop. */
29
+ export type FormValue = string | File;
30
+ /**
31
+ * The filled-in fields, as an object.
32
+ *
33
+ * Keys whose value is empty or whitespace are left out entirely rather than sent as `''`.
34
+ */
35
+ export declare function filledValues(formData: FormData): Record<string, FormValue>;
36
+ /**
37
+ * Which of the named fields are still missing.
38
+ *
39
+ * The screen asks before it sends, so the person is told which box to fill rather than
40
+ * watching a mutation fail on a rule the form never mentioned.
41
+ */
42
+ export declare function missingRequired(formData: FormData, required: string[]): string[];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Turning a form into the object a mutation takes.
3
+ *
4
+ * ── The bug this exists for ──────────────────────────────────────────────────
5
+ * The application register screen built its input like this:
6
+ *
7
+ * Array.from(formData.entries()).reduce((app, [key, value]) => { app[key] = value; … })
8
+ *
9
+ * `FormData` carries **every named input**, including the ones nobody typed in, each as an
10
+ * empty string. So an untouched optional field went to the server as `''` rather than as
11
+ * absent — and for `email` that is not the same thing at all:
12
+ *
13
+ * NewApplication.email is GraphQLEmailAddress, nullable
14
+ * EmailAddressResolver.parseValue('') → "Value is not a valid email address"
15
+ *
16
+ * Registering an application without a contact email therefore failed, on a field the schema
17
+ * marks optional. The screen said nothing useful about why.
18
+ *
19
+ * ── What "absent" means here ─────────────────────────────────────────────────
20
+ * An empty text input means the person did not fill it in. It does not mean "set this to the
21
+ * empty string" — HTML has no way to say that, and no field in these forms wants it. So an
22
+ * empty value is dropped and the key never appears, which is what "optional" means on the
23
+ * wire.
24
+ *
25
+ * Whitespace counts as empty for the same reason: a space typed by accident is not an answer,
26
+ * and every scalar that rejects `''` rejects `' '` too.
27
+ */
28
+ /**
29
+ * The filled-in fields, as an object.
30
+ *
31
+ * Keys whose value is empty or whitespace are left out entirely rather than sent as `''`.
32
+ */
33
+ export function filledValues(formData) {
34
+ const values = {};
35
+ for (const [key, value] of Array.from(formData.entries())) {
36
+ if (typeof value === 'string') {
37
+ const trimmed = value.trim();
38
+ if (trimmed) {
39
+ values[key] = trimmed;
40
+ }
41
+ continue;
42
+ }
43
+ values[key] = value;
44
+ }
45
+ return values;
46
+ }
47
+ /**
48
+ * Which of the named fields are still missing.
49
+ *
50
+ * The screen asks before it sends, so the person is told which box to fill rather than
51
+ * watching a mutation fail on a rule the form never mentioned.
52
+ */
53
+ export function missingRequired(formData, required) {
54
+ const values = filledValues(formData);
55
+ return required.filter(name => !values[name]);
56
+ }
57
+ //# sourceMappingURL=form-values.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-values.js","sourceRoot":"","sources":["../../client/utils/form-values.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,QAAkB;IAC7C,MAAM,MAAM,GAA8B,EAAE,CAAA;IAE5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;YAE5B,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,CAAC,GAAG,CAAC,GAAG,OAAO,CAAA;YACvB,CAAC;YAED,SAAQ;QACV,CAAC;QAED,MAAM,CAAC,GAAG,CAAC,GAAG,KAAa,CAAA;IAC7B,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,QAAkB,EAAE,QAAkB;IACpE,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;IAErC,OAAO,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;AAC/C,CAAC","sourcesContent":["/**\n * Turning a form into the object a mutation takes.\n *\n * ── The bug this exists for ──────────────────────────────────────────────────\n * The application register screen built its input like this:\n *\n * Array.from(formData.entries()).reduce((app, [key, value]) => { app[key] = value; … })\n *\n * `FormData` carries **every named input**, including the ones nobody typed in, each as an\n * empty string. So an untouched optional field went to the server as `''` rather than as\n * absent — and for `email` that is not the same thing at all:\n *\n * NewApplication.email is GraphQLEmailAddress, nullable\n * EmailAddressResolver.parseValue('') → \"Value is not a valid email address\"\n *\n * Registering an application without a contact email therefore failed, on a field the schema\n * marks optional. The screen said nothing useful about why.\n *\n * ── What \"absent\" means here ─────────────────────────────────────────────────\n * An empty text input means the person did not fill it in. It does not mean \"set this to the\n * empty string\" — HTML has no way to say that, and no field in these forms wants it. So an\n * empty value is dropped and the key never appears, which is what \"optional\" means on the\n * wire.\n *\n * Whitespace counts as empty for the same reason: a space typed by accident is not an answer,\n * and every scalar that rejects `''` rejects `' '` too.\n */\n\n/** Values a form can carry. Files pass through untouched — there is no empty file to drop. */\nexport type FormValue = string | File\n\n/**\n * The filled-in fields, as an object.\n *\n * Keys whose value is empty or whitespace are left out entirely rather than sent as `''`.\n */\nexport function filledValues(formData: FormData): Record<string, FormValue> {\n const values: Record<string, FormValue> = {}\n\n for (const [key, value] of Array.from(formData.entries())) {\n if (typeof value === 'string') {\n const trimmed = value.trim()\n\n if (trimmed) {\n values[key] = trimmed\n }\n\n continue\n }\n\n values[key] = value as File\n }\n\n return values\n}\n\n/**\n * Which of the named fields are still missing.\n *\n * The screen asks before it sends, so the person is told which box to fill rather than\n * watching a mutation fail on a rule the form never mentioned.\n */\nexport function missingRequired(formData: FormData, required: string[]): string[] {\n const values = filledValues(formData)\n\n return required.filter(name => !values[name])\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import { filledValues, missingRequired } from './form-values';
2
+ /**
3
+ * The application register screen sent every input it had, including the untouched ones, each
4
+ * as an empty string. `NewApplication.email` is a GraphQLEmailAddress and rejects `''`, so
5
+ * leaving the optional contact email blank made registration fail.
6
+ */
7
+ function form(entries) {
8
+ const data = new FormData();
9
+ for (const [key, value] of Object.entries(entries)) {
10
+ data.append(key, value);
11
+ }
12
+ return data;
13
+ }
14
+ describe('the fields a person actually filled in', () => {
15
+ it('keeps what was typed', () => {
16
+ expect(filledValues(form({ name: 'Billing', url: 'https://example.com' }))).toEqual({
17
+ name: 'Billing',
18
+ url: 'https://example.com'
19
+ });
20
+ });
21
+ it('leaves an untouched field out instead of sending an empty string', () => {
22
+ /*
23
+ * ⚠ The case this file exists for. `email: ''` is not "no email" to a scalar that
24
+ * validates format — it is an invalid email, and the mutation fails on a field the
25
+ * schema marks optional.
26
+ */
27
+ const values = filledValues(form({ name: 'Billing', email: '', url: '' }));
28
+ expect(values).toEqual({ name: 'Billing' });
29
+ expect('email' in values).toBe(false);
30
+ });
31
+ it('treats whitespace as untouched', () => {
32
+ /* A space typed by accident is not an answer, and the scalars reject it too. */
33
+ expect(filledValues(form({ name: 'Billing', email: ' ' }))).toEqual({ name: 'Billing' });
34
+ });
35
+ it('trims what it keeps', () => {
36
+ expect(filledValues(form({ name: ' Billing ' }))).toEqual({ name: 'Billing' });
37
+ });
38
+ it('keeps a value that only looks empty', () => {
39
+ /* '0' is falsy as a string in some hands. It is an answer. */
40
+ expect(filledValues(form({ name: '0' }))).toEqual({ name: '0' });
41
+ });
42
+ });
43
+ describe('what the form still needs', () => {
44
+ it('names the required field nobody filled in', () => {
45
+ expect(missingRequired(form({ name: '', description: 'x' }), ['name'])).toEqual(['name']);
46
+ });
47
+ it('counts whitespace as not filled in', () => {
48
+ expect(missingRequired(form({ name: ' ' }), ['name'])).toEqual(['name']);
49
+ });
50
+ it('says nothing when the required fields are there', () => {
51
+ expect(missingRequired(form({ name: 'Billing', email: '' }), ['name'])).toEqual([]);
52
+ });
53
+ it('does not require what the schema left optional', () => {
54
+ /*
55
+ * The list comes from the schema — NewApplication marks only `name` non-null. Adding a
56
+ * field here that the server accepts as absent would invent a rule the product does not
57
+ * have.
58
+ */
59
+ expect(missingRequired(form({ name: 'Billing' }), ['name'])).toEqual([]);
60
+ });
61
+ });
62
+ //# sourceMappingURL=form-values.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-values.test.js","sourceRoot":"","sources":["../../client/utils/form-values.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { filledValues, missingRequired } from './form-values'\n\n/**\n * The application register screen sent every input it had, including the untouched ones, each\n * as an empty string. `NewApplication.email` is a GraphQLEmailAddress and rejects `''`, so\n * leaving the optional contact email blank made registration fail.\n */\n\nfunction form(entries: Record<string, string>): FormData {\n const data = new FormData()\n\n for (const [key, value] of Object.entries(entries)) {\n data.append(key, value)\n }\n\n return data\n}\n\ndescribe('the fields a person actually filled in', () => {\n it('keeps what was typed', () => {\n expect(filledValues(form({ name: 'Billing', url: 'https://example.com' }))).toEqual({\n name: 'Billing',\n url: 'https://example.com'\n })\n })\n\n it('leaves an untouched field out instead of sending an empty string', () => {\n /*\n * ⚠ The case this file exists for. `email: ''` is not \"no email\" to a scalar that\n * validates format — it is an invalid email, and the mutation fails on a field the\n * schema marks optional.\n */\n const values = filledValues(form({ name: 'Billing', email: '', url: '' }))\n\n expect(values).toEqual({ name: 'Billing' })\n expect('email' in values).toBe(false)\n })\n\n it('treats whitespace as untouched', () => {\n /* A space typed by accident is not an answer, and the scalars reject it too. */\n expect(filledValues(form({ name: 'Billing', email: ' ' }))).toEqual({ name: 'Billing' })\n })\n\n it('trims what it keeps', () => {\n expect(filledValues(form({ name: ' Billing ' }))).toEqual({ name: 'Billing' })\n })\n\n it('keeps a value that only looks empty', () => {\n /* '0' is falsy as a string in some hands. It is an answer. */\n expect(filledValues(form({ name: '0' }))).toEqual({ name: '0' })\n })\n})\n\ndescribe('what the form still needs', () => {\n it('names the required field nobody filled in', () => {\n expect(missingRequired(form({ name: '', description: 'x' }), ['name'])).toEqual(['name'])\n })\n\n it('counts whitespace as not filled in', () => {\n expect(missingRequired(form({ name: ' ' }), ['name'])).toEqual(['name'])\n })\n\n it('says nothing when the required fields are there', () => {\n expect(missingRequired(form({ name: 'Billing', email: '' }), ['name'])).toEqual([])\n })\n\n it('does not require what the schema left optional', () => {\n /*\n * The list comes from the schema — NewApplication marks only `name` non-null. Adding a\n * field here that the server accepts as absent would invent a rule the product does not\n * have.\n */\n expect(missingRequired(form({ name: 'Billing' }), ['name'])).toEqual([])\n })\n})\n"]}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Turning a list query's response into "what the screen may claim".
3
+ *
4
+ * ── Why this is not inline in each page ──────────────────────────────────────
5
+ * Three auth screens had the same two lines each, and all three were wrong the same way:
6
+ *
7
+ * this.items = (await this.fetch()).items
8
+ * …
9
+ * if (!response.errors) { return response.data.applications }
10
+ *
11
+ * The shared client runs queries with `errorPolicy: 'all'`, so a GraphQL error comes back
12
+ * **in the response** instead of throwing. The helper then returned nothing, `.items` threw
13
+ * inside an async lifecycle method with nobody to catch it, and the table stayed empty. A
14
+ * failure and an empty list looked identical to the person reading the screen.
15
+ *
16
+ * The rule is small enough to get wrong in three places and small enough to test once.
17
+ */
18
+ /** What a list query returns when it answers. */
19
+ export type ListPage<T = any> = {
20
+ items: T[];
21
+ total?: number;
22
+ };
23
+ /**
24
+ * What a screen is entitled to say afterwards.
25
+ *
26
+ * `failed` is not "empty". A screen that could not ask has to say so; saying nothing reads
27
+ * as "there are none", which is a claim it has no basis for.
28
+ */
29
+ export type ListOutcome<T = any> = {
30
+ items: T[];
31
+ total: number;
32
+ failed: boolean;
33
+ };
34
+ /**
35
+ * Read a list page out of an Apollo response, or nothing if the list did not arrive.
36
+ *
37
+ * Ways not to arrive:
38
+ * · an error at the query itself (errorPolicy 'all' puts errors here rather than throwing)
39
+ * · `data` missing entirely
40
+ * · `data` present but the field we asked for is not, or is not a page
41
+ *
42
+ * An error *inside* the page is not one of them — see `stoppedTheList`.
43
+ */
44
+ export declare function pageFromResponse<T = any>(response: any, field: string): ListPage<T> | undefined;
45
+ /**
46
+ * The same question for a query that returns a bare array rather than a page.
47
+ *
48
+ * `domainOwners` is one: it answers with a list and no total. The screen was reading it as
49
+ * `response.data?.domainOwners || []`, which turns a refused query into "this domain has no
50
+ * owners" — a sentence with real consequences, since the reader may then go and add one.
51
+ *
52
+ * Returns nothing on failure for the same reason as `pageFromResponse`, so `listOutcome`
53
+ * can tell the two apart.
54
+ */
55
+ export declare function arrayFromResponse<T = any>(response: any, field: string): ListPage<T> | undefined;
56
+ /**
57
+ * What the screen shows, given a page or the absence of one.
58
+ *
59
+ * An absent page yields an empty list **and** `failed`, so the caller cannot accidentally
60
+ * render "none" — the flag is right there next to the rows.
61
+ */
62
+ export declare function listOutcome<T = any>(page: ListPage<T> | undefined | null): ListOutcome<T>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Turning a list query's response into "what the screen may claim".
3
+ *
4
+ * ── Why this is not inline in each page ──────────────────────────────────────
5
+ * Three auth screens had the same two lines each, and all three were wrong the same way:
6
+ *
7
+ * this.items = (await this.fetch()).items
8
+ * …
9
+ * if (!response.errors) { return response.data.applications }
10
+ *
11
+ * The shared client runs queries with `errorPolicy: 'all'`, so a GraphQL error comes back
12
+ * **in the response** instead of throwing. The helper then returned nothing, `.items` threw
13
+ * inside an async lifecycle method with nobody to catch it, and the table stayed empty. A
14
+ * failure and an empty list looked identical to the person reading the screen.
15
+ *
16
+ * The rule is small enough to get wrong in three places and small enough to test once.
17
+ */
18
+ /**
19
+ * Did an error stop the list, or only withhold a field inside it?
20
+ *
21
+ * GraphQL says where an error happened. A gate on a whole query rejects at the root, so the
22
+ * path is the query field itself or nothing at all. A gate on one **column** — and this house
23
+ * has them, `Appliance.accessToken` and `AuthProvider.clientSecret` both carry their own
24
+ * `@privilege` — rejects deeper in, at `[field, 'items', 3, 'clientSecret']`.
25
+ *
26
+ * The difference decides what the screen may show. Treating both as failure blacks out a list
27
+ * that arrived intact apart from one column a viewer may not read; treating both as success
28
+ * renders a refused query as an empty list. Neither is acceptable, so the path is read.
29
+ *
30
+ * An error with no path at all is counted as fatal: unknown scope leans to the safe side.
31
+ */
32
+ function stoppedTheList(response, field) {
33
+ const errors = response?.errors;
34
+ if (!errors || !errors.length) {
35
+ return false;
36
+ }
37
+ return errors.some((error) => {
38
+ const path = error?.path;
39
+ if (!Array.isArray(path) || path.length === 0) {
40
+ return true;
41
+ }
42
+ /* `[field]` alone is the query itself being refused; deeper is one value inside it. */
43
+ return path[0] === field && path.length === 1;
44
+ });
45
+ }
46
+ /**
47
+ * Read a list page out of an Apollo response, or nothing if the list did not arrive.
48
+ *
49
+ * Ways not to arrive:
50
+ * · an error at the query itself (errorPolicy 'all' puts errors here rather than throwing)
51
+ * · `data` missing entirely
52
+ * · `data` present but the field we asked for is not, or is not a page
53
+ *
54
+ * An error *inside* the page is not one of them — see `stoppedTheList`.
55
+ */
56
+ export function pageFromResponse(response, field) {
57
+ if (!response || stoppedTheList(response, field)) {
58
+ return undefined;
59
+ }
60
+ const page = response.data?.[field];
61
+ return page && Array.isArray(page.items) ? page : undefined;
62
+ }
63
+ /**
64
+ * The same question for a query that returns a bare array rather than a page.
65
+ *
66
+ * `domainOwners` is one: it answers with a list and no total. The screen was reading it as
67
+ * `response.data?.domainOwners || []`, which turns a refused query into "this domain has no
68
+ * owners" — a sentence with real consequences, since the reader may then go and add one.
69
+ *
70
+ * Returns nothing on failure for the same reason as `pageFromResponse`, so `listOutcome`
71
+ * can tell the two apart.
72
+ */
73
+ export function arrayFromResponse(response, field) {
74
+ if (!response || stoppedTheList(response, field)) {
75
+ return undefined;
76
+ }
77
+ const rows = response.data?.[field];
78
+ return Array.isArray(rows) ? { items: rows, total: rows.length } : undefined;
79
+ }
80
+ /**
81
+ * What the screen shows, given a page or the absence of one.
82
+ *
83
+ * An absent page yields an empty list **and** `failed`, so the caller cannot accidentally
84
+ * render "none" — the flag is right there next to the rows.
85
+ */
86
+ export function listOutcome(page) {
87
+ if (!page) {
88
+ return { items: [], total: 0, failed: true };
89
+ }
90
+ return { items: page.items, total: page.total ?? page.items.length, failed: false };
91
+ }
92
+ //# sourceMappingURL=list-result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-result.js","sourceRoot":"","sources":["../../client/utils/list-result.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAaH;;;;;;;;;;;;;GAaG;AACH,SAAS,cAAc,CAAC,QAAa,EAAE,KAAa;IAClD,MAAM,MAAM,GAAG,QAAQ,EAAE,MAAM,CAAA;IAE/B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAU,EAAE,EAAE;QAChC,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI,CAAA;QAExB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9C,OAAO,IAAI,CAAA;QACb,CAAC;QAED,uFAAuF;QACvF,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAU,QAAa,EAAE,KAAa;IACpE,IAAI,CAAC,QAAQ,IAAI,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAA;IAEnC,OAAO,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AAC7D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAU,QAAa,EAAE,KAAa;IACrE,IAAI,CAAC,QAAQ,IAAI,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAA;IAEnC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAU,IAAoC;IACvE,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IAC9C,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AACrF,CAAC","sourcesContent":["/**\n * Turning a list query's response into \"what the screen may claim\".\n *\n * ── Why this is not inline in each page ──────────────────────────────────────\n * Three auth screens had the same two lines each, and all three were wrong the same way:\n *\n * this.items = (await this.fetch()).items\n * …\n * if (!response.errors) { return response.data.applications }\n *\n * The shared client runs queries with `errorPolicy: 'all'`, so a GraphQL error comes back\n * **in the response** instead of throwing. The helper then returned nothing, `.items` threw\n * inside an async lifecycle method with nobody to catch it, and the table stayed empty. A\n * failure and an empty list looked identical to the person reading the screen.\n *\n * The rule is small enough to get wrong in three places and small enough to test once.\n */\n\n/** What a list query returns when it answers. */\nexport type ListPage<T = any> = { items: T[]; total?: number }\n\n/**\n * What a screen is entitled to say afterwards.\n *\n * `failed` is not \"empty\". A screen that could not ask has to say so; saying nothing reads\n * as \"there are none\", which is a claim it has no basis for.\n */\nexport type ListOutcome<T = any> = { items: T[]; total: number; failed: boolean }\n\n/**\n * Did an error stop the list, or only withhold a field inside it?\n *\n * GraphQL says where an error happened. A gate on a whole query rejects at the root, so the\n * path is the query field itself or nothing at all. A gate on one **column** — and this house\n * has them, `Appliance.accessToken` and `AuthProvider.clientSecret` both carry their own\n * `@privilege` — rejects deeper in, at `[field, 'items', 3, 'clientSecret']`.\n *\n * The difference decides what the screen may show. Treating both as failure blacks out a list\n * that arrived intact apart from one column a viewer may not read; treating both as success\n * renders a refused query as an empty list. Neither is acceptable, so the path is read.\n *\n * An error with no path at all is counted as fatal: unknown scope leans to the safe side.\n */\nfunction stoppedTheList(response: any, field: string): boolean {\n const errors = response?.errors\n\n if (!errors || !errors.length) {\n return false\n }\n\n return errors.some((error: any) => {\n const path = error?.path\n\n if (!Array.isArray(path) || path.length === 0) {\n return true\n }\n\n /* `[field]` alone is the query itself being refused; deeper is one value inside it. */\n return path[0] === field && path.length === 1\n })\n}\n\n/**\n * Read a list page out of an Apollo response, or nothing if the list did not arrive.\n *\n * Ways not to arrive:\n * · an error at the query itself (errorPolicy 'all' puts errors here rather than throwing)\n * · `data` missing entirely\n * · `data` present but the field we asked for is not, or is not a page\n *\n * An error *inside* the page is not one of them — see `stoppedTheList`.\n */\nexport function pageFromResponse<T = any>(response: any, field: string): ListPage<T> | undefined {\n if (!response || stoppedTheList(response, field)) {\n return undefined\n }\n\n const page = response.data?.[field]\n\n return page && Array.isArray(page.items) ? page : undefined\n}\n\n/**\n * The same question for a query that returns a bare array rather than a page.\n *\n * `domainOwners` is one: it answers with a list and no total. The screen was reading it as\n * `response.data?.domainOwners || []`, which turns a refused query into \"this domain has no\n * owners\" — a sentence with real consequences, since the reader may then go and add one.\n *\n * Returns nothing on failure for the same reason as `pageFromResponse`, so `listOutcome`\n * can tell the two apart.\n */\nexport function arrayFromResponse<T = any>(response: any, field: string): ListPage<T> | undefined {\n if (!response || stoppedTheList(response, field)) {\n return undefined\n }\n\n const rows = response.data?.[field]\n\n return Array.isArray(rows) ? { items: rows, total: rows.length } : undefined\n}\n\n/**\n * What the screen shows, given a page or the absence of one.\n *\n * An absent page yields an empty list **and** `failed`, so the caller cannot accidentally\n * render \"none\" — the flag is right there next to the rows.\n */\nexport function listOutcome<T = any>(page: ListPage<T> | undefined | null): ListOutcome<T> {\n if (!page) {\n return { items: [], total: 0, failed: true }\n }\n\n return { items: page.items, total: page.total ?? page.items.length, failed: false }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};