@aglyn/plugins-outreach 1.0.0-beta.168 → 1.0.0-beta.170

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.168",
3
+ "version": "1.0.0-beta.170",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.168",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.168",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.168",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.168",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.168",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.168",
34
- "@aglyn/shared-util-http": "1.0.0-beta.168",
35
- "@aglyn/shared-util-timestamp": "1.0.0-beta.168",
36
- "@aglyn/shared-util-tools": "1.0.0-beta.168",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.168",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.168",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.168",
28
+ "@aglyn/aglyn": "1.0.0-beta.170",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.170",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.170",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.170",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.170",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.170",
34
+ "@aglyn/shared-util-http": "1.0.0-beta.170",
35
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.170",
36
+ "@aglyn/shared-util-tools": "1.0.0-beta.170",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.170",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.170",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.170",
40
40
  "@swc/helpers": "0.5.23"
41
41
  },
42
42
  "peerDependencies": {
@@ -17,6 +17,23 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { useCallback, useEffect, useState } from "react";
19
19
  import { OutreachRouteError } from "./use-outreach-api.js";
20
+ /**
21
+ * Whether two answers from the settings route say the same thing.
22
+ *
23
+ * Compared by their serialization rather than field by field: the document
24
+ * carries whatever the route returns, and a comparison that named today's
25
+ * fields would silently stop noticing a new one. Guarded, because a value
26
+ * that cannot be serialized must read as "different" — the safe direction,
27
+ * which merely costs a render.
28
+ */ function sameSettings(left, right) {
29
+ if (left === right) return true;
30
+ if (!left || !right) return false;
31
+ try {
32
+ return JSON.stringify(left) === JSON.stringify(right);
33
+ } catch (unused) {
34
+ return false;
35
+ }
36
+ }
20
37
  /**
21
38
  * The organization's compliance settings, read through `outreach/settings`
22
39
  * (AGL-2980): the Compliance page edits them, and the sequence editor's
@@ -31,14 +48,37 @@ import { OutreachRouteError } from "./use-outreach-api.js";
31
48
  useEffect(()=>{
32
49
  if (!orgId) return undefined;
33
50
  let current = true;
34
- setState((previous)=>_extends({}, previous, {
35
- status: previous.settings ? previous.status : 'loading'
36
- }));
37
- api.readSettings().then((answer)=>current && setState({
38
- status: 'ready',
39
- settings: answer.settings,
40
- message: null
41
- })).catch((error)=>{
51
+ /*
52
+ * THE SAME OBJECT WHEN NOTHING CHANGED (AGL-3279).
53
+ *
54
+ * This used to spread unconditionally, which stores a NEW state object
55
+ * on every run and re-renders even when the status is what it already
56
+ * was. That is harmless while the effect's dependencies are stable and
57
+ * a render loop the moment one of them is not — a re-render remakes
58
+ * the dependency, the effect runs, the state changes identity, and
59
+ * React stops it at "Maximum update depth exceeded" (the minified
60
+ * error #185 this page reported on 2026-09-21).
61
+ *
62
+ * Returning `previous` unchanged makes the hook immune to that: the
63
+ * effect may run as often as it likes without ever being the reason it
64
+ * runs again.
65
+ */ setState((previous)=>{
66
+ const status = previous.settings ? previous.status : 'loading';
67
+ return status === previous.status ? previous : _extends({}, previous, {
68
+ status
69
+ });
70
+ });
71
+ api.readSettings().then((answer)=>current && setState((previous)=>// IDEMPOTENT (AGL-3279): a read that answers what the hook
72
+ // already holds leaves the state object alone. Identity is what
73
+ // a caller's effects and memos key on, so a fresh object for an
74
+ // unchanged answer is a re-render at best and, with an unstable
75
+ // dependency above, the render loop that ends in "Maximum
76
+ // update depth exceeded".
77
+ previous.status === 'ready' && previous.message === null && sameSettings(previous.settings, answer.settings) ? previous : {
78
+ status: 'ready',
79
+ settings: answer.settings,
80
+ message: null
81
+ })).catch((error)=>{
42
82
  if (!current) return;
43
83
  const refused = error instanceof OutreachRouteError && error.refused;
44
84
  setState({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-settings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { OutreachComplianceSettingsDocument } from '../model/outreach.types'\nimport { OutreachRouteError, type OutreachApi } from './use-outreach-api'\nimport type { OutreachLoadStatus } from './use-outreach-data'\n\nexport interface OutreachSettingsLoad {\n status: OutreachLoadStatus\n settings: OutreachComplianceSettingsDocument | null\n /** The route's sentence for an error or a refusal. */\n message: string | null\n /** Reads the settings again — after a save elsewhere, or to retry. */\n reload(): void\n}\n\n/**\n * The organization's compliance settings, read through `outreach/settings`\n * (AGL-2980): the Compliance page edits them, and the sequence editor's\n * preview prints the footer from them.\n */\nexport function useOutreachComplianceSettings(\n api: OutreachApi,\n orgId: string | null,\n): OutreachSettingsLoad {\n const [state, setState] = useState<Omit<OutreachSettingsLoad, 'reload'>>({\n status: 'loading',\n settings: null,\n message: null,\n })\n const [attempt, setAttempt] = useState(0)\n useEffect(() => {\n if (!orgId) return undefined\n let current = true\n setState((previous) => ({\n ...previous,\n status: previous.settings ? previous.status : 'loading',\n }))\n api\n .readSettings()\n .then(\n (answer) =>\n current &&\n setState({\n status: 'ready',\n settings: answer.settings,\n message: null,\n }),\n )\n .catch((error: unknown) => {\n if (!current) return\n const refused = error instanceof OutreachRouteError && error.refused\n setState({\n status: refused ? 'refused' : 'error',\n settings: null,\n message: (error as Error).message,\n })\n })\n return () => {\n current = false\n }\n }, [api, orgId, attempt])\n const reload = useCallback(() => setAttempt((count) => count + 1), [])\n return { ...state, reload }\n}\n"],"names":["useCallback","useEffect","useState","OutreachRouteError","useOutreachComplianceSettings","api","orgId","state","setState","status","settings","message","attempt","setAttempt","undefined","current","previous","readSettings","then","answer","catch","error","refused","reload","count"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAExD,SAASC,kBAAkB,QAA0B,wBAAoB;AAYzE;;;;CAIC,GACD,OAAO,SAASC,8BACdC,GAAgB,EAChBC,KAAoB;IAEpB,MAAM,CAACC,OAAOC,SAAS,GAAGN,SAA+C;QACvEO,QAAQ;QACRC,UAAU;QACVC,SAAS;IACX;IACA,MAAM,CAACC,SAASC,WAAW,GAAGX,SAAS;IACvCD,UAAU;QACR,IAAI,CAACK,OAAO,OAAOQ;QACnB,IAAIC,UAAU;QACdP,SAAS,CAACQ,WAAc,aACnBA;gBACHP,QAAQO,SAASN,QAAQ,GAAGM,SAASP,MAAM,GAAG;;QAEhDJ,IACGY,YAAY,GACZC,IAAI,CACH,CAACC,SACCJ,WACAP,SAAS;gBACPC,QAAQ;gBACRC,UAAUS,OAAOT,QAAQ;gBACzBC,SAAS;YACX,IAEHS,KAAK,CAAC,CAACC;YACN,IAAI,CAACN,SAAS;YACd,MAAMO,UAAUD,iBAAiBlB,sBAAsBkB,MAAMC,OAAO;YACpEd,SAAS;gBACPC,QAAQa,UAAU,YAAY;gBAC9BZ,UAAU;gBACVC,SAAS,AAACU,MAAgBV,OAAO;YACnC;QACF;QACF,OAAO;YACLI,UAAU;QACZ;IACF,GAAG;QAACV;QAAKC;QAAOM;KAAQ;IACxB,MAAMW,SAASvB,YAAY,IAAMa,WAAW,CAACW,QAAUA,QAAQ,IAAI,EAAE;IACrE,OAAO,aAAKjB;QAAOgB;;AACrB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-settings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { OutreachComplianceSettingsDocument } from '../model/outreach.types'\nimport { OutreachRouteError, type OutreachApi } from './use-outreach-api'\nimport type { OutreachLoadStatus } from './use-outreach-data'\n\nexport interface OutreachSettingsLoad {\n status: OutreachLoadStatus\n settings: OutreachComplianceSettingsDocument | null\n /** The route's sentence for an error or a refusal. */\n message: string | null\n /** Reads the settings again — after a save elsewhere, or to retry. */\n reload(): void\n}\n\n/**\n * Whether two answers from the settings route say the same thing.\n *\n * Compared by their serialization rather than field by field: the document\n * carries whatever the route returns, and a comparison that named today's\n * fields would silently stop noticing a new one. Guarded, because a value\n * that cannot be serialized must read as \"different\" — the safe direction,\n * which merely costs a render.\n */\nfunction sameSettings(\n left: OutreachComplianceSettingsDocument | null,\n right: OutreachComplianceSettingsDocument | null,\n): boolean {\n if (left === right) return true\n if (!left || !right) return false\n try {\n return JSON.stringify(left) === JSON.stringify(right)\n } catch {\n return false\n }\n}\n\n/**\n * The organization's compliance settings, read through `outreach/settings`\n * (AGL-2980): the Compliance page edits them, and the sequence editor's\n * preview prints the footer from them.\n */\nexport function useOutreachComplianceSettings(\n api: OutreachApi,\n orgId: string | null,\n): OutreachSettingsLoad {\n const [state, setState] = useState<Omit<OutreachSettingsLoad, 'reload'>>({\n status: 'loading',\n settings: null,\n message: null,\n })\n const [attempt, setAttempt] = useState(0)\n useEffect(() => {\n if (!orgId) return undefined\n let current = true\n /*\n * THE SAME OBJECT WHEN NOTHING CHANGED (AGL-3279).\n *\n * This used to spread unconditionally, which stores a NEW state object\n * on every run and re-renders even when the status is what it already\n * was. That is harmless while the effect's dependencies are stable and\n * a render loop the moment one of them is not — a re-render remakes\n * the dependency, the effect runs, the state changes identity, and\n * React stops it at \"Maximum update depth exceeded\" (the minified\n * error #185 this page reported on 2026-09-21).\n *\n * Returning `previous` unchanged makes the hook immune to that: the\n * effect may run as often as it likes without ever being the reason it\n * runs again.\n */\n setState((previous) => {\n const status = previous.settings ? previous.status : 'loading'\n return status === previous.status ? previous : { ...previous, status }\n })\n api\n .readSettings()\n .then(\n (answer) =>\n current &&\n setState((previous) =>\n // IDEMPOTENT (AGL-3279): a read that answers what the hook\n // already holds leaves the state object alone. Identity is what\n // a caller's effects and memos key on, so a fresh object for an\n // unchanged answer is a re-render at best and, with an unstable\n // dependency above, the render loop that ends in \"Maximum\n // update depth exceeded\".\n previous.status === 'ready' &&\n previous.message === null &&\n sameSettings(previous.settings, answer.settings)\n ? previous\n : {\n status: 'ready',\n settings: answer.settings,\n message: null,\n },\n ),\n )\n .catch((error: unknown) => {\n if (!current) return\n const refused = error instanceof OutreachRouteError && error.refused\n setState({\n status: refused ? 'refused' : 'error',\n settings: null,\n message: (error as Error).message,\n })\n })\n return () => {\n current = false\n }\n }, [api, orgId, attempt])\n const reload = useCallback(() => setAttempt((count) => count + 1), [])\n return { ...state, reload }\n}\n"],"names":["useCallback","useEffect","useState","OutreachRouteError","sameSettings","left","right","JSON","stringify","useOutreachComplianceSettings","api","orgId","state","setState","status","settings","message","attempt","setAttempt","undefined","current","previous","readSettings","then","answer","catch","error","refused","reload","count"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAExD,SAASC,kBAAkB,QAA0B,wBAAoB;AAYzE;;;;;;;;CAQC,GACD,SAASC,aACPC,IAA+C,EAC/CC,KAAgD;IAEhD,IAAID,SAASC,OAAO,OAAO;IAC3B,IAAI,CAACD,QAAQ,CAACC,OAAO,OAAO;IAC5B,IAAI;QACF,OAAOC,KAAKC,SAAS,CAACH,UAAUE,KAAKC,SAAS,CAACF;IACjD,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASG,8BACdC,GAAgB,EAChBC,KAAoB;IAEpB,MAAM,CAACC,OAAOC,SAAS,GAAGX,SAA+C;QACvEY,QAAQ;QACRC,UAAU;QACVC,SAAS;IACX;IACA,MAAM,CAACC,SAASC,WAAW,GAAGhB,SAAS;IACvCD,UAAU;QACR,IAAI,CAACU,OAAO,OAAOQ;QACnB,IAAIC,UAAU;QACd;;;;;;;;;;;;;;KAcC,GACDP,SAAS,CAACQ;YACR,MAAMP,SAASO,SAASN,QAAQ,GAAGM,SAASP,MAAM,GAAG;YACrD,OAAOA,WAAWO,SAASP,MAAM,GAAGO,WAAW,aAAKA;gBAAUP;;QAChE;QACAJ,IACGY,YAAY,GACZC,IAAI,CACH,CAACC,SACCJ,WACAP,SAAS,CAACQ,WACR,2DAA2D;gBAC3D,gEAAgE;gBAChE,gEAAgE;gBAChE,gEAAgE;gBAChE,0DAA0D;gBAC1D,0BAA0B;gBAC1BA,SAASP,MAAM,KAAK,WACpBO,SAASL,OAAO,KAAK,QACrBZ,aAAaiB,SAASN,QAAQ,EAAES,OAAOT,QAAQ,IAC3CM,WACA;oBACEP,QAAQ;oBACRC,UAAUS,OAAOT,QAAQ;oBACzBC,SAAS;gBACX,IAGTS,KAAK,CAAC,CAACC;YACN,IAAI,CAACN,SAAS;YACd,MAAMO,UAAUD,iBAAiBvB,sBAAsBuB,MAAMC,OAAO;YACpEd,SAAS;gBACPC,QAAQa,UAAU,YAAY;gBAC9BZ,UAAU;gBACVC,SAAS,AAACU,MAAgBV,OAAO;YACnC;QACF;QACF,OAAO;YACLI,UAAU;QACZ;IACF,GAAG;QAACV;QAAKC;QAAOM;KAAQ;IACxB,MAAMW,SAAS5B,YAAY,IAAMkB,WAAW,CAACW,QAAUA,QAAQ,IAAI,EAAE;IACrE,OAAO,aAAKjB;QAAOgB;;AACrB"}