@things-factory/webhook 10.1.254 → 10.1.257

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.
@@ -2,10 +2,11 @@ import { LitElement } from 'lit';
2
2
  declare const WebhookDestinationsPage_base: (new (...args: any[]) => LitElement) & typeof LitElement;
3
3
  export declare class WebhookDestinationsPage extends WebhookDestinationsPage_base {
4
4
  static styles: import("lit").CSSResult;
5
- private destinations;
6
- private failure?;
7
- connectedCallback(): void;
8
- private reload;
5
+ private destinationsRead;
6
+ private activating;
7
+ private saving;
8
+ private get destinations();
9
+ private get failure();
9
10
  private toggle;
10
11
  /** Exposed so a host page can add a peer without this component owning a form. */
11
12
  save(peer: string, patch: Record<string, unknown>): Promise<void>;
@@ -1,8 +1,8 @@
1
- import { __decorate, __metadata } from "tslib";
1
+ import { __decorate } from "tslib";
2
2
  import gql from 'graphql-tag';
3
3
  import { LitElement, css, html } from 'lit';
4
- import { customElement, state } from 'lit/decorators.js';
5
- import { client } from '@operato/graphql';
4
+ import { customElement } from 'lit/decorators.js';
5
+ import { MutationController, QueryController } from '@operato/graphql';
6
6
  import { i18next, localize } from '@operato/i18n';
7
7
  /*
8
8
  * ═══════════════════════════════════════════════════════════════════════════
@@ -55,7 +55,13 @@ const SET_ACTIVE = gql `
55
55
  let WebhookDestinationsPage = class WebhookDestinationsPage extends localize(i18next)(LitElement) {
56
56
  constructor() {
57
57
  super(...arguments);
58
- this.destinations = [];
58
+ /*
59
+ * `no-cache` (the controller's default) because this page is opened right after someone changed a value, often in
60
+ * another tab. A cached list would show the value they just replaced and read as a failed save.
61
+ */
62
+ this.destinationsRead = new QueryController(this, DESTINATIONS);
63
+ this.activating = new MutationController(this, SET_ACTIVE, { refetch: () => [this.destinationsRead] });
64
+ this.saving = new MutationController(this, SAVE, { refetch: () => [this.destinationsRead] });
59
65
  }
60
66
  static { this.styles = css `
61
67
  :host {
@@ -129,37 +135,23 @@ let WebhookDestinationsPage = class WebhookDestinationsPage extends localize(i18
129
135
  color: var(--webhook-muted, #616161);
130
136
  }
131
137
  `; }
132
- connectedCallback() {
133
- super.connectedCallback();
134
- this.reload();
138
+ get destinations() {
139
+ return this.destinationsRead.data?.webhookDestinations ?? [];
135
140
  }
136
- async reload() {
137
- /*
138
- * `no-cache` because this page is opened right after someone changed a value, often in
139
- * another tab. A cached list would show the value they just replaced and read as a
140
- * failed save.
141
- */
142
- const response = await client.query({ query: DESTINATIONS, fetchPolicy: 'no-cache' });
143
- /*
144
- * This client resolves GraphQL errors instead of rejecting them, so `data?.x ?? []`
145
- * turns a failure into an empty list and the page says "nothing configured" when the
146
- * truth is "we could not ask". Check for errors before reading data.
147
- */
148
- if (response.errors?.length) {
149
- this.failure = response.errors[0].message;
150
- return;
151
- }
152
- this.failure = undefined;
153
- this.destinations = response.data?.webhookDestinations ?? [];
141
+ /*
142
+ * A failure is not an empty list: the page says "nothing configured" only when the ask answered, and says it could
143
+ * not ask otherwise.
144
+ */
145
+ get failure() {
146
+ const error = this.destinationsRead.error;
147
+ return error ? (error.message ?? String(error)) : undefined;
154
148
  }
155
149
  async toggle(peer, active) {
156
- await client.mutate({ mutation: SET_ACTIVE, variables: { peer, active } });
157
- await this.reload();
150
+ await this.activating.run({ peer, active });
158
151
  }
159
152
  /** Exposed so a host page can add a peer without this component owning a form. */
160
153
  async save(peer, patch) {
161
- await client.mutate({ mutation: SAVE, variables: { peer, patch } });
162
- await this.reload();
154
+ await this.saving.run({ peer, patch });
163
155
  }
164
156
  renderEmpty() {
165
157
  return html `
@@ -213,14 +205,6 @@ let WebhookDestinationsPage = class WebhookDestinationsPage extends localize(i18
213
205
  `;
214
206
  }
215
207
  };
216
- __decorate([
217
- state(),
218
- __metadata("design:type", Array)
219
- ], WebhookDestinationsPage.prototype, "destinations", void 0);
220
- __decorate([
221
- state(),
222
- __metadata("design:type", String)
223
- ], WebhookDestinationsPage.prototype, "failure", void 0);
224
208
  WebhookDestinationsPage = __decorate([
225
209
  customElement('webhook-destinations-page')
226
210
  ], WebhookDestinationsPage);
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-destinations-page.js","sourceRoot":"","sources":["../../client/pages/webhook-destinations-page.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { LitElement, css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\n\n/*\n * ═══════════════════════════════════════════════════════════════════════════\n * Where this tenant pushes facts — one row per peer.\n *\n * ── Why a list ─────────────────────────────────────────────────────────────\n * The screen this replaces edited a single destination, because the table it read had one\n * row per tenant. A tenant that mirrors into two twins, or pushes to a twin and an ERP, could\n * not enter the second peer at all — there was no field to say which one you meant\n * (ADR-0041).\n *\n * ── The empty screen is the important one ──────────────────────────────────\n * A tenant that has never pushed anywhere sees this page first, and \"no rows\" tells them\n * nothing about what to do. So the empty state names what a destination is for and what a\n * peer name is, rather than showing an empty table with a plus button.\n *\n * ── Secrets are not on this screen ─────────────────────────────────────────\n * Signing and verifying keys come from the environment. The page can say whether one is\n * present, and that is a different fact from the value — a field that accepted the value\n * would put it in a request body and a browser's form history.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nconst DESTINATIONS = gql`\n query {\n webhookDestinations {\n id\n peer\n description\n endpoint\n params\n active\n state\n updatedAt\n }\n }\n`\n\nconst SAVE = gql`\n mutation ($peer: String!, $patch: Object!) {\n saveWebhookDestination(peer: $peer, patch: $patch) {\n id\n peer\n }\n }\n`\n\nconst SET_ACTIVE = gql`\n mutation ($peer: String!, $active: Boolean!) {\n setWebhookDestinationActive(peer: $peer, active: $active)\n }\n`\n\ntype Destination = {\n id: string\n peer: string\n description?: string\n endpoint: string\n params?: Record<string, unknown>\n active: boolean\n state?: string\n updatedAt?: string\n}\n\n@customElement('webhook-destinations-page')\nexport class WebhookDestinationsPage extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n padding: 16px;\n font-size: 13px;\n }\n\n h2 {\n margin: 0 0 4px;\n font-size: 18px;\n }\n\n .lede {\n margin: 0 0 16px;\n color: var(--webhook-muted, #616161);\n max-width: 62ch;\n line-height: 1.6;\n }\n\n .empty {\n border: 1px dashed var(--webhook-line, #cfd8dc);\n border-radius: 6px;\n padding: 24px;\n max-width: 62ch;\n line-height: 1.7;\n }\n\n .empty strong {\n display: block;\n margin-bottom: 8px;\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n display: grid;\n gap: 8px;\n }\n\n li {\n border: 1px solid var(--webhook-line, #cfd8dc);\n border-radius: 6px;\n padding: 12px 14px;\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 4px 12px;\n align-items: center;\n }\n\n .peer {\n font-weight: 600;\n }\n\n .endpoint {\n grid-column: 1;\n color: var(--webhook-muted, #616161);\n word-break: break-all;\n }\n\n .flags {\n grid-column: 2;\n grid-row: 1 / span 2;\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .off {\n color: var(--webhook-muted, #616161);\n }\n `\n\n @state() private destinations: Destination[] = []\n @state() private failure?: string\n\n connectedCallback() {\n super.connectedCallback()\n this.reload()\n }\n\n private async reload() {\n /*\n * `no-cache` because this page is opened right after someone changed a value, often in\n * another tab. A cached list would show the value they just replaced and read as a\n * failed save.\n */\n const response = await client.query({ query: DESTINATIONS, fetchPolicy: 'no-cache' })\n\n /*\n * This client resolves GraphQL errors instead of rejecting them, so `data?.x ?? []`\n * turns a failure into an empty list and the page says \"nothing configured\" when the\n * truth is \"we could not ask\". Check for errors before reading data.\n */\n if (response.errors?.length) {\n this.failure = response.errors[0].message\n return\n }\n\n this.failure = undefined\n this.destinations = response.data?.webhookDestinations ?? []\n }\n\n private async toggle(peer: string, active: boolean) {\n await client.mutate({ mutation: SET_ACTIVE, variables: { peer, active } })\n await this.reload()\n }\n\n /** Exposed so a host page can add a peer without this component owning a form. */\n async save(peer: string, patch: Record<string, unknown>) {\n await client.mutate({ mutation: SAVE, variables: { peer, patch } })\n await this.reload()\n }\n\n private renderEmpty() {\n return html`\n <div class=\"empty\">\n <strong>${i18next.t('webhook.destinations.empty.title')}</strong>\n ${i18next.t('webhook.destinations.empty.what')}\n <br /><br />\n ${i18next.t('webhook.destinations.empty.two-peers')}\n </div>\n `\n }\n\n render() {\n if (this.failure) {\n /*\n * The message says the ask failed, not that there is nothing — those are different\n * facts and only one of them is the reader's problem to fix here.\n */\n return html`\n <h2>${i18next.t('webhook.destinations.title')}</h2>\n <p class=\"lede\">${i18next.t('webhook.destinations.load-failed')}: ${this.failure}</p>\n `\n }\n\n return html`\n <h2>${i18next.t('webhook.destinations.title')}</h2>\n <p class=\"lede\">\n ${i18next.t('webhook.destinations.lede')}\n </p>\n ${this.destinations.length\n ? html`\n <ul>\n ${this.destinations.map(\n destination => html`\n <li>\n <span class=\"peer\">${destination.peer}</span>\n <span class=\"endpoint\">${destination.endpoint}</span>\n <span class=\"flags\">\n ${destination.state ? html`<span class=\"off\">${destination.state}</span>` : null}\n <label>\n <input\n type=\"checkbox\"\n .checked=${destination.active}\n @change=${(e: Event) =>\n this.toggle(destination.peer, (e.target as HTMLInputElement).checked)}\n />\n ${i18next.t('webhook.field.active')}\n </label>\n </span>\n </li>\n `\n )}\n </ul>\n `\n : this.renderEmpty()}\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'webhook-destinations-page': WebhookDestinationsPage\n }\n}\n"]}
1
+ {"version":3,"file":"webhook-destinations-page.js","sourceRoot":"","sources":["../../client/pages/webhook-destinations-page.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { LitElement, css, html } from 'lit'\nimport { customElement } from 'lit/decorators.js'\n\nimport { MutationController, QueryController } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\n\n/*\n * ═══════════════════════════════════════════════════════════════════════════\n * Where this tenant pushes facts — one row per peer.\n *\n * ── Why a list ─────────────────────────────────────────────────────────────\n * The screen this replaces edited a single destination, because the table it read had one\n * row per tenant. A tenant that mirrors into two twins, or pushes to a twin and an ERP, could\n * not enter the second peer at all — there was no field to say which one you meant\n * (ADR-0041).\n *\n * ── The empty screen is the important one ──────────────────────────────────\n * A tenant that has never pushed anywhere sees this page first, and \"no rows\" tells them\n * nothing about what to do. So the empty state names what a destination is for and what a\n * peer name is, rather than showing an empty table with a plus button.\n *\n * ── Secrets are not on this screen ─────────────────────────────────────────\n * Signing and verifying keys come from the environment. The page can say whether one is\n * present, and that is a different fact from the value — a field that accepted the value\n * would put it in a request body and a browser's form history.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nconst DESTINATIONS = gql`\n query {\n webhookDestinations {\n id\n peer\n description\n endpoint\n params\n active\n state\n updatedAt\n }\n }\n`\n\nconst SAVE = gql`\n mutation ($peer: String!, $patch: Object!) {\n saveWebhookDestination(peer: $peer, patch: $patch) {\n id\n peer\n }\n }\n`\n\nconst SET_ACTIVE = gql`\n mutation ($peer: String!, $active: Boolean!) {\n setWebhookDestinationActive(peer: $peer, active: $active)\n }\n`\n\ntype Destination = {\n id: string\n peer: string\n description?: string\n endpoint: string\n params?: Record<string, unknown>\n active: boolean\n state?: string\n updatedAt?: string\n}\n\n@customElement('webhook-destinations-page')\nexport class WebhookDestinationsPage extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n padding: 16px;\n font-size: 13px;\n }\n\n h2 {\n margin: 0 0 4px;\n font-size: 18px;\n }\n\n .lede {\n margin: 0 0 16px;\n color: var(--webhook-muted, #616161);\n max-width: 62ch;\n line-height: 1.6;\n }\n\n .empty {\n border: 1px dashed var(--webhook-line, #cfd8dc);\n border-radius: 6px;\n padding: 24px;\n max-width: 62ch;\n line-height: 1.7;\n }\n\n .empty strong {\n display: block;\n margin-bottom: 8px;\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n display: grid;\n gap: 8px;\n }\n\n li {\n border: 1px solid var(--webhook-line, #cfd8dc);\n border-radius: 6px;\n padding: 12px 14px;\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 4px 12px;\n align-items: center;\n }\n\n .peer {\n font-weight: 600;\n }\n\n .endpoint {\n grid-column: 1;\n color: var(--webhook-muted, #616161);\n word-break: break-all;\n }\n\n .flags {\n grid-column: 2;\n grid-row: 1 / span 2;\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .off {\n color: var(--webhook-muted, #616161);\n }\n `\n\n /*\n * `no-cache` (the controller's default) because this page is opened right after someone changed a value, often in\n * another tab. A cached list would show the value they just replaced and read as a failed save.\n */\n private destinationsRead = new QueryController(this, DESTINATIONS)\n private activating = new MutationController(this, SET_ACTIVE, { refetch: () => [this.destinationsRead] })\n private saving = new MutationController(this, SAVE, { refetch: () => [this.destinationsRead] })\n\n private get destinations(): Destination[] {\n return this.destinationsRead.data?.webhookDestinations ?? []\n }\n\n /*\n * A failure is not an empty list: the page says \"nothing configured\" only when the ask answered, and says it could\n * not ask otherwise.\n */\n private get failure(): string | undefined {\n const error = this.destinationsRead.error as any\n return error ? (error.message ?? String(error)) : undefined\n }\n\n private async toggle(peer: string, active: boolean) {\n await this.activating.run({ peer, active })\n }\n\n /** Exposed so a host page can add a peer without this component owning a form. */\n async save(peer: string, patch: Record<string, unknown>) {\n await this.saving.run({ peer, patch })\n }\n\n private renderEmpty() {\n return html`\n <div class=\"empty\">\n <strong>${i18next.t('webhook.destinations.empty.title')}</strong>\n ${i18next.t('webhook.destinations.empty.what')}\n <br /><br />\n ${i18next.t('webhook.destinations.empty.two-peers')}\n </div>\n `\n }\n\n render() {\n if (this.failure) {\n /*\n * The message says the ask failed, not that there is nothing — those are different\n * facts and only one of them is the reader's problem to fix here.\n */\n return html`\n <h2>${i18next.t('webhook.destinations.title')}</h2>\n <p class=\"lede\">${i18next.t('webhook.destinations.load-failed')}: ${this.failure}</p>\n `\n }\n\n return html`\n <h2>${i18next.t('webhook.destinations.title')}</h2>\n <p class=\"lede\">\n ${i18next.t('webhook.destinations.lede')}\n </p>\n ${this.destinations.length\n ? html`\n <ul>\n ${this.destinations.map(\n destination => html`\n <li>\n <span class=\"peer\">${destination.peer}</span>\n <span class=\"endpoint\">${destination.endpoint}</span>\n <span class=\"flags\">\n ${destination.state ? html`<span class=\"off\">${destination.state}</span>` : null}\n <label>\n <input\n type=\"checkbox\"\n .checked=${destination.active}\n @change=${(e: Event) =>\n this.toggle(destination.peer, (e.target as HTMLInputElement).checked)}\n />\n ${i18next.t('webhook.field.active')}\n </label>\n </span>\n </li>\n `\n )}\n </ul>\n `\n : this.renderEmpty()}\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'webhook-destinations-page': WebhookDestinationsPage\n }\n}\n"]}