@aglyn/tenant-feature-instance 1.0.0-beta.143

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 (154) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +3 -0
  3. package/jest.config.d.ts +36 -0
  4. package/package.json +45 -0
  5. package/src/index.d.ts +17 -0
  6. package/src/index.js +18 -0
  7. package/src/index.js.map +1 -0
  8. package/src/lib/components/duplicate-resource-dialog.d.ts +77 -0
  9. package/src/lib/components/duplicate-resource-dialog.js +217 -0
  10. package/src/lib/components/duplicate-resource-dialog.js.map +1 -0
  11. package/src/lib/constants/firebase-config.d.ts +68 -0
  12. package/src/lib/constants/firebase-config.js +155 -0
  13. package/src/lib/constants/firebase-config.js.map +1 -0
  14. package/src/lib/constants/firebase-emulator-hosts.d.ts +60 -0
  15. package/src/lib/constants/firebase-emulator-hosts.js +81 -0
  16. package/src/lib/constants/firebase-emulator-hosts.js.map +1 -0
  17. package/src/lib/constants/mock-data.d.ts +24 -0
  18. package/src/lib/constants/mock-data.js +49 -0
  19. package/src/lib/constants/mock-data.js.map +1 -0
  20. package/src/lib/constants/site-paths.d.ts +47 -0
  21. package/src/lib/constants/site-paths.js +66 -0
  22. package/src/lib/constants/site-paths.js.map +1 -0
  23. package/src/lib/hooks/firebase/auth-persistence.d.ts +74 -0
  24. package/src/lib/hooks/firebase/auth-persistence.js +117 -0
  25. package/src/lib/hooks/firebase/auth-persistence.js.map +1 -0
  26. package/src/lib/hooks/firebase/firebase-services.d.ts +172 -0
  27. package/src/lib/hooks/firebase/firebase-services.js +522 -0
  28. package/src/lib/hooks/firebase/firebase-services.js.map +1 -0
  29. package/src/lib/hooks/firebase/firestore-cache.d.ts +125 -0
  30. package/src/lib/hooks/firebase/firestore-cache.js +152 -0
  31. package/src/lib/hooks/firebase/firestore-cache.js.map +1 -0
  32. package/src/lib/hooks/firebase/firestore-shared-client-state.d.ts +169 -0
  33. package/src/lib/hooks/firebase/firestore-shared-client-state.js +316 -0
  34. package/src/lib/hooks/firebase/firestore-shared-client-state.js.map +1 -0
  35. package/src/lib/hooks/firestore-denial-reporter.d.ts +192 -0
  36. package/src/lib/hooks/firestore-denial-reporter.js +293 -0
  37. package/src/lib/hooks/firestore-denial-reporter.js.map +1 -0
  38. package/src/lib/hooks/helpers/besigner-nodes-converter.d.ts +62 -0
  39. package/src/lib/hooks/helpers/besigner-nodes-converter.js +91 -0
  40. package/src/lib/hooks/helpers/besigner-nodes-converter.js.map +1 -0
  41. package/src/lib/hooks/helpers/guarded-nodes-save.d.ts +80 -0
  42. package/src/lib/hooks/helpers/guarded-nodes-save.js +89 -0
  43. package/src/lib/hooks/helpers/guarded-nodes-save.js.map +1 -0
  44. package/src/lib/hooks/helpers/guarded-seed-write.d.ts +125 -0
  45. package/src/lib/hooks/helpers/guarded-seed-write.js +193 -0
  46. package/src/lib/hooks/helpers/guarded-seed-write.js.map +1 -0
  47. package/src/lib/hooks/helpers/listen-options.d.ts +91 -0
  48. package/src/lib/hooks/helpers/listen-options.js +92 -0
  49. package/src/lib/hooks/helpers/listen-options.js.map +1 -0
  50. package/src/lib/hooks/helpers/use-doc.d.ts +38 -0
  51. package/src/lib/hooks/helpers/use-doc.js +213 -0
  52. package/src/lib/hooks/helpers/use-doc.js.map +1 -0
  53. package/src/lib/hooks/helpers/use-modify-doc-callback.d.ts +28 -0
  54. package/src/lib/hooks/helpers/use-modify-doc-callback.js +110 -0
  55. package/src/lib/hooks/helpers/use-modify-doc-callback.js.map +1 -0
  56. package/src/lib/hooks/host-collection-queries.d.ts +125 -0
  57. package/src/lib/hooks/host-collection-queries.js +126 -0
  58. package/src/lib/hooks/host-collection-queries.js.map +1 -0
  59. package/src/lib/hooks/list-filter-constraints.d.ts +64 -0
  60. package/src/lib/hooks/list-filter-constraints.js +288 -0
  61. package/src/lib/hooks/list-filter-constraints.js.map +1 -0
  62. package/src/lib/hooks/sorted-collection-window.d.ts +207 -0
  63. package/src/lib/hooks/sorted-collection-window.js +150 -0
  64. package/src/lib/hooks/sorted-collection-window.js.map +1 -0
  65. package/src/lib/hooks/use-component-version.d.ts +43 -0
  66. package/src/lib/hooks/use-component-version.js +73 -0
  67. package/src/lib/hooks/use-component-version.js.map +1 -0
  68. package/src/lib/hooks/use-component.d.ts +39 -0
  69. package/src/lib/hooks/use-component.js +55 -0
  70. package/src/lib/hooks/use-component.js.map +1 -0
  71. package/src/lib/hooks/use-console-host-route.d.ts +24 -0
  72. package/src/lib/hooks/use-console-host-route.js +77 -0
  73. package/src/lib/hooks/use-console-host-route.js.map +1 -0
  74. package/src/lib/hooks/use-duplicate-resource-api.d.ts +43 -0
  75. package/src/lib/hooks/use-duplicate-resource-api.js +61 -0
  76. package/src/lib/hooks/use-duplicate-resource-api.js.map +1 -0
  77. package/src/lib/hooks/use-firestore-collection.d.ts +62 -0
  78. package/src/lib/hooks/use-firestore-collection.js +307 -0
  79. package/src/lib/hooks/use-firestore-collection.js.map +1 -0
  80. package/src/lib/hooks/use-firestore-doc.d.ts +42 -0
  81. package/src/lib/hooks/use-firestore-doc.js +192 -0
  82. package/src/lib/hooks/use-firestore-doc.js.map +1 -0
  83. package/src/lib/hooks/use-form-version.d.ts +44 -0
  84. package/src/lib/hooks/use-form-version.js +73 -0
  85. package/src/lib/hooks/use-form-version.js.map +1 -0
  86. package/src/lib/hooks/use-host-activity-logger.d.ts +35 -0
  87. package/src/lib/hooks/use-host-activity-logger.js +96 -0
  88. package/src/lib/hooks/use-host-activity-logger.js.map +1 -0
  89. package/src/lib/hooks/use-host-campaigns.d.ts +53 -0
  90. package/src/lib/hooks/use-host-campaigns.js +93 -0
  91. package/src/lib/hooks/use-host-campaigns.js.map +1 -0
  92. package/src/lib/hooks/use-host-org-id.d.ts +96 -0
  93. package/src/lib/hooks/use-host-org-id.js +154 -0
  94. package/src/lib/hooks/use-host-org-id.js.map +1 -0
  95. package/src/lib/hooks/use-host-resource-api.d.ts +43 -0
  96. package/src/lib/hooks/use-host-resource-api.js +60 -0
  97. package/src/lib/hooks/use-host-resource-api.js.map +1 -0
  98. package/src/lib/hooks/use-host-template.d.ts +44 -0
  99. package/src/lib/hooks/use-host-template.js +82 -0
  100. package/src/lib/hooks/use-host-template.js.map +1 -0
  101. package/src/lib/hooks/use-host-version-api.d.ts +59 -0
  102. package/src/lib/hooks/use-host-version-api.js +69 -0
  103. package/src/lib/hooks/use-host-version-api.js.map +1 -0
  104. package/src/lib/hooks/use-host.d.ts +101 -0
  105. package/src/lib/hooks/use-host.js +45 -0
  106. package/src/lib/hooks/use-host.js.map +1 -0
  107. package/src/lib/hooks/use-layout-version.d.ts +33 -0
  108. package/src/lib/hooks/use-layout-version.js +64 -0
  109. package/src/lib/hooks/use-layout-version.js.map +1 -0
  110. package/src/lib/hooks/use-layout.d.ts +41 -0
  111. package/src/lib/hooks/use-layout.js +49 -0
  112. package/src/lib/hooks/use-layout.js.map +1 -0
  113. package/src/lib/hooks/use-live-artifact-count.d.ts +50 -0
  114. package/src/lib/hooks/use-live-artifact-count.js +102 -0
  115. package/src/lib/hooks/use-live-artifact-count.js.map +1 -0
  116. package/src/lib/hooks/use-org-member-options.d.ts +57 -0
  117. package/src/lib/hooks/use-org-member-options.js +112 -0
  118. package/src/lib/hooks/use-org-member-options.js.map +1 -0
  119. package/src/lib/hooks/use-org-plan.d.ts +46 -0
  120. package/src/lib/hooks/use-org-plan.js +86 -0
  121. package/src/lib/hooks/use-org-plan.js.map +1 -0
  122. package/src/lib/hooks/use-paged-collection.d.ts +95 -0
  123. package/src/lib/hooks/use-paged-collection.js +130 -0
  124. package/src/lib/hooks/use-paged-collection.js.map +1 -0
  125. package/src/lib/hooks/use-plugin-config.d.ts +38 -0
  126. package/src/lib/hooks/use-plugin-config.js +120 -0
  127. package/src/lib/hooks/use-plugin-config.js.map +1 -0
  128. package/src/lib/hooks/use-scope-tokens.d.ts +28 -0
  129. package/src/lib/hooks/use-scope-tokens.js +100 -0
  130. package/src/lib/hooks/use-scope-tokens.js.map +1 -0
  131. package/src/lib/hooks/use-screen-version.d.ts +33 -0
  132. package/src/lib/hooks/use-screen-version.js +65 -0
  133. package/src/lib/hooks/use-screen-version.js.map +1 -0
  134. package/src/lib/hooks/use-screen.d.ts +68 -0
  135. package/src/lib/hooks/use-screen.js +50 -0
  136. package/src/lib/hooks/use-screen.js.map +1 -0
  137. package/src/lib/hooks/use-sending-identity-api.d.ts +197 -0
  138. package/src/lib/hooks/use-sending-identity-api.js +95 -0
  139. package/src/lib/hooks/use-sending-identity-api.js.map +1 -0
  140. package/src/lib/hooks/use-sorted-paged-collection.d.ts +48 -0
  141. package/src/lib/hooks/use-sorted-paged-collection.js +213 -0
  142. package/src/lib/hooks/use-sorted-paged-collection.js.map +1 -0
  143. package/src/lib/hooks/use-switcher-collection.d.ts +105 -0
  144. package/src/lib/hooks/use-switcher-collection.js +275 -0
  145. package/src/lib/hooks/use-switcher-collection.js.map +1 -0
  146. package/src/lib/hooks/use-user-name.d.ts +51 -0
  147. package/src/lib/hooks/use-user-name.js +90 -0
  148. package/src/lib/hooks/use-user-name.js.map +1 -0
  149. package/src/lib/hooks/use-user-photo.d.ts +50 -0
  150. package/src/lib/hooks/use-user-photo.js +87 -0
  151. package/src/lib/hooks/use-user-photo.js.map +1 -0
  152. package/src/lib/tenant.d.ts +61 -0
  153. package/src/lib/tenant.js +62 -0
  154. package/src/lib/tenant.js.map +1 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // A TYPE-only namespace: every `Aglyn.` reference below is a type, and held
17
+ // as a VALUE the namespace pins every module the barrel reaches into the
18
+ // console shell that renders these hooks. `import type` erases instead.
19
+ import { _ as _extends } from "@swc/helpers/_/_extends";
20
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
21
+ import { Timestamp } from "@aglyn/shared-util-timestamp";
22
+ import { doc } from "firebase/firestore";
23
+ import { useFirestore } from "./firebase/firebase-services.js";
24
+ import useDoc from "./helpers/use-doc.js";
25
+ export const useScreenRef = ({ hostId, screenId })=>{
26
+ const firestore = useFirestore();
27
+ const ref = doc(firestore, 'hosts', hostId, 'screens', screenId);
28
+ return ref.withConverter({
29
+ toFirestore (data) {
30
+ const { $id } = data, rest = _object_without_properties_loose(data, [
31
+ "$id"
32
+ ]);
33
+ return _extends({}, rest, {
34
+ updatedAt: Timestamp.now()
35
+ });
36
+ },
37
+ fromFirestore (snapshot, options) {
38
+ if (!snapshot.exists()) return undefined;
39
+ const data = snapshot.data(options);
40
+ // data.nodes = data.versionRef?.get()
41
+ return data;
42
+ }
43
+ });
44
+ };
45
+ export const useScreen = (data, options)=>{
46
+ return useDoc(useScreenRef(data), options);
47
+ };
48
+ export default useScreen;
49
+
50
+ //# sourceMappingURL=use-screen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-screen.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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\n// A TYPE-only namespace: every `Aglyn.` reference below is a type, and held\n// as a VALUE the namespace pins every module the barrel reaches into the\n// console shell that renders these hooks. `import type` erases instead.\nimport type * as Aglyn from '@aglyn/aglyn'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n doc,\n type DocumentSnapshot,\n type SnapshotOptions,\n} from 'firebase/firestore'\nimport { useFirestore, type FirestoreDocOptions } from './firebase/firebase-services'\nimport useDoc from './helpers/use-doc'\n\nexport const useScreenRef = ({ hostId, screenId }: { hostId: Aglyn.HostUid; screenId: Aglyn.ScreenUid }) => {\n const firestore = useFirestore()\n const ref = doc(firestore, 'hosts', hostId, 'screens', screenId)\n return ref.withConverter({\n toFirestore(data: Aglyn.AglynScreen) {\n const { $id, ...rest } = data\n return { ...rest, updatedAt: Timestamp.now() }\n },\n fromFirestore(\n snapshot: DocumentSnapshot<Aglyn.AglynScreen>,\n options: SnapshotOptions,\n ) {\n if (!snapshot.exists()) return undefined\n const data = snapshot.data(options)\n // data.nodes = data.versionRef?.get()\n return data as Aglyn.AglynScreen\n },\n })\n}\n\nexport const useScreen = (\n data: {\n hostId: Aglyn.HostUid\n screenId: Aglyn.ScreenUid\n },\n options?: FirestoreDocOptions<Aglyn.AglynScreen>,\n) => {\n return useDoc(useScreenRef(data), options)\n}\n\nexport default useScreen\n"],"names":["Timestamp","doc","useFirestore","useDoc","useScreenRef","hostId","screenId","firestore","ref","withConverter","toFirestore","data","$id","rest","updatedAt","now","fromFirestore","snapshot","options","exists","undefined","useScreen"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,4EAA4E;AAC5E,yEAAyE;AACzE,wEAAwE;;;AAExE,SAASA,SAAS,QAAQ,+BAA8B;AACxD,SACEC,GAAG,QAGE,qBAAoB;AAC3B,SAASC,YAAY,QAAkC,kCAA8B;AACrF,OAAOC,YAAY,uBAAmB;AAEtC,OAAO,MAAMC,eAAe,CAAC,EAAEC,MAAM,EAAEC,QAAQ,EAAwD;IACrG,MAAMC,YAAYL;IAClB,MAAMM,MAAMP,IAAIM,WAAW,SAASF,QAAQ,WAAWC;IACvD,OAAOE,IAAIC,aAAa,CAAC;QACvBC,aAAYC,IAAuB;YACjC,MAAM,EAAEC,GAAG,EAAW,GAAGD,MAATE,wCAASF;;;YACzB,OAAO,aAAKE;gBAAMC,WAAWd,UAAUe,GAAG;;QAC5C;QACAC,eACEC,QAA6C,EAC7CC,OAAwB;YAExB,IAAI,CAACD,SAASE,MAAM,IAAI,OAAOC;YAC/B,MAAMT,OAAOM,SAASN,IAAI,CAACO;YAC3B,sCAAsC;YACtC,OAAOP;QACT;IACF;AACF,EAAC;AAED,OAAO,MAAMU,YAAY,CACvBV,MAIAO;IAEA,OAAOf,OAAOC,aAAaO,OAAOO;AACpC,EAAC;AAED,eAAeG,UAAS"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The client of the console's sending-identity routes (`/api/email/*`).
3
+ *
4
+ * Which address a site's mail leaves from, and the domains and senders behind
5
+ * it, are the platform's mail rail: the console serves the routes, and every
6
+ * message goes out under that identity whichever plugin wrote it. So the hook
7
+ * that calls them is here, beside the other client hooks a plugin may import,
8
+ * and a one-to-one composer reads the identity without importing the plugin
9
+ * whose settings page edits it.
10
+ */
11
+ /** What one call answered with. `payload` is `{}` on an unparseable body. */
12
+ export interface SendingApiResult {
13
+ response: Response;
14
+ payload: Record<string, any>;
15
+ }
16
+ /** One DNS record, exactly as the server issued it. */
17
+ export interface SendingDnsRecordView {
18
+ type: 'TXT' | 'MX';
19
+ name: string;
20
+ value: string;
21
+ priority?: number;
22
+ purpose: 'spf' | 'dkim' | 'return-path' | 'dmarc';
23
+ required: boolean;
24
+ note: string;
25
+ }
26
+ /** One org sending domain, as the domains route reports it. */
27
+ export interface SendingDomainView {
28
+ domain: string;
29
+ status: 'requested' | 'records-issued' | 'verified' | 'failed';
30
+ records?: SendingDnsRecordView[];
31
+ lines?: string[];
32
+ lastMissing?: string[] | null;
33
+ lastIssueError?: string | null;
34
+ verifiedAtMs?: number | null;
35
+ lastCheckedAtMs?: number | null;
36
+ dmarc?: {
37
+ policy: 'reject' | 'quarantine' | 'none' | 'absent';
38
+ record: string | null;
39
+ consequence: string;
40
+ } | null;
41
+ dmarcSuggestion?: SendingDnsRecordView | null;
42
+ pendingProvider?: boolean;
43
+ providerDetail?: string | null;
44
+ }
45
+ /**
46
+ * One sender this site holds — `hosts/{hostId}/senders/{senderId}`.
47
+ *
48
+ * `from` is the whole address, assembled by the server for the reason
49
+ * `IdentityOption.from` is: a surface that built `${localPart}@${domain}`
50
+ * itself would be a second derivation of the address, and the two would
51
+ * disagree the first time either moved. It is `null` for a row whose mailbox
52
+ * is not in effect — a site on the pooled Aglyn address has one fixed mailbox,
53
+ * shared with the other sites on it.
54
+ */
55
+ export interface HostSenderView {
56
+ id: string;
57
+ localPart: string;
58
+ fromName: string | null;
59
+ replyTo: string | null;
60
+ /** Whether an email that names no sender goes out as this one. */
61
+ isDefault: boolean;
62
+ from: string | null;
63
+ }
64
+ /** What this site sends as, and what it is allowed to send as. */
65
+ export interface SendingIdentityView {
66
+ orgId: string | null;
67
+ selected: string;
68
+ /**
69
+ * The site's own provisioned domain inside the platform mail apex, or `''`
70
+ * when it has none and its transactional mail rides the shared pool.
71
+ */
72
+ platformDomain: string;
73
+ /**
74
+ * The plan that carries sending as a domain the CUSTOMER owns, and the plan
75
+ * that carries a domain the platform provisions for the site — as names
76
+ * ready to print, `null` when no plan carries that gate.
77
+ *
78
+ * Both come from the server because both are derived from the entitlement
79
+ * tables there. A tier name written into this component would be pricing
80
+ * copy that keeps rendering after the gate beneath it moves.
81
+ */
82
+ customDomainPlan: string | null;
83
+ dedicatedDomainPlan: string | null;
84
+ /** The mailbox this site sends as — the part before the `@`. */
85
+ localPart: string;
86
+ /**
87
+ * Whether that mailbox is the one actually in use.
88
+ *
89
+ * False for a site on the pooled Aglyn address, whose mailbox is fixed. The
90
+ * stored value is real and is kept; it takes effect when the site has a
91
+ * domain of its own, and until then the card says so rather than showing a
92
+ * name that no recipient will see.
93
+ */
94
+ localPartInUse: boolean;
95
+ /** The site's default sender name, or null. Composers start from it. */
96
+ fromName: string | null;
97
+ /** The site's default reply address, or null. */
98
+ replyTo: string | null;
99
+ /**
100
+ * Every sender this site may send as, the default among them.
101
+ *
102
+ * Never empty: a site with no `senders` subcollection has exactly one
103
+ * sender, and the server synthesizes it from the three fields the host
104
+ * document has always carried rather than reporting an absence.
105
+ */
106
+ senders: HostSenderView[];
107
+ identity: string;
108
+ /**
109
+ * `'custom'` for a domain this site has verified, `'shared'` for the pooled
110
+ * Aglyn address a site uses until it has one, `null` when the send is
111
+ * refused. `'platform'` is in the union because the resolver can return it,
112
+ * and never reaches this view: it is Aglyn's own `aglyn.com` identity, which
113
+ * a host-scoped resolution cannot produce.
114
+ */
115
+ identitySource: 'custom' | 'shared' | 'platform' | null;
116
+ refusal: {
117
+ code: string;
118
+ domain: string | null;
119
+ message: string;
120
+ missing: string[];
121
+ } | null;
122
+ options: {
123
+ value: string;
124
+ from: string | null;
125
+ selectable: boolean;
126
+ status: string;
127
+ }[];
128
+ domains: {
129
+ domain: string;
130
+ status: SendingDomainView['status'];
131
+ verifiedAtMs: number | null;
132
+ lastCheckedAtMs: number | null;
133
+ /**
134
+ * Why the last attempt to issue a key produced none, or `null`.
135
+ *
136
+ * Carried in the LIST and not only on the domain's own page, because one
137
+ * of its values is not a fault: a claim held at the mail provider's domain
138
+ * allowance is `requested` with a reason, and a list that could not see
139
+ * the reason would label it "Waiting on a signing key" — a state whose
140
+ * whole instruction is to press a button that will not move it.
141
+ */
142
+ lastIssueError?: string | null;
143
+ }[];
144
+ canManage: boolean;
145
+ entitled: boolean;
146
+ /**
147
+ * The OFFER of a platform sending domain — never the fact of having one,
148
+ * which `platformDomain` above answers.
149
+ *
150
+ * A dedicated subdomain is no longer issued on upgrade: it costs a provider
151
+ * domain slot and three records in Aglyn's own zone, so it is requested by
152
+ * somebody who wants it rather than handed to every paying site.
153
+ *
154
+ * Optional because a console deployment can be older than the field. A
155
+ * surface reading it must treat its absence as "no offer to show", never as
156
+ * "the offer was refused".
157
+ */
158
+ dedicated?: {
159
+ /** The plan carries one and this site has none. */
160
+ available: boolean;
161
+ /** The name a request would most likely take. Not a reservation. */
162
+ proposed: string | null;
163
+ };
164
+ }
165
+ /**
166
+ * ONE AUTHORIZED CALL TO THE SENDING-IDENTITY AND SENDING-DOMAIN ROUTES.
167
+ *
168
+ * The same shape as `useCampaignSendApi`, including why the user is held in a
169
+ * ref: this callback is a dependency of effects that load on open, and the
170
+ * user object is a new object on most renders, so depending on it directly
171
+ * would re-issue the request on every render of the surface. The token is
172
+ * fetched at call time either way.
173
+ *
174
+ * The two paths are fixed here rather than taken as an argument, for the
175
+ * reason the campaign hook gives: a hook that accepted a URL from a component
176
+ * would be a way to post the console's credentials somewhere the console does
177
+ * not own.
178
+ *
179
+ * The token is obtained under a deadline, for the reason `useCampaignApi`
180
+ * gives: it is awaited in front of the request, so an unbounded refresh is a
181
+ * call that never reaches `fetch` and never reports anything.
182
+ *
183
+ * ## An unobtainable token ANSWERS here rather than throwing
184
+ *
185
+ * Unlike the campaign hook, whose callers must tell a send that never left
186
+ * the browser apart from one the route refused. Everything on this side reads
187
+ * or edits a configuration surface, where both are the same news to the
188
+ * reader — and every caller already renders `payload.error` on a refusal, so
189
+ * answering in that shape is what puts the real reason on screen and leaves
190
+ * the button that started the call usable.
191
+ */
192
+ export declare function useSendingApi(): (options: {
193
+ path: "sending-identity" | "sending-domains";
194
+ method: "GET" | "POST" | "DELETE";
195
+ query?: Record<string, string>;
196
+ body?: Record<string, unknown>;
197
+ }) => Promise<SendingApiResult>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { useUser } from "./firebase/firebase-services.js";
19
+ import { useCallback, useRef } from "react";
20
+ import { describeCallFailure, resolveIdToken } from "@aglyn/shared-util-http/authorized-token";
21
+ /**
22
+ * A refusal the caller reads the same way it reads the route's own.
23
+ *
24
+ * `401` because that is what the request would have been answered with had it
25
+ * gone out, and the reason travels in `payload.error`, which is the field
26
+ * every caller already renders.
27
+ */ const refusedForCaller = (message)=>({
28
+ response: {
29
+ ok: false,
30
+ status: 401,
31
+ json: async ()=>({
32
+ error: message
33
+ })
34
+ },
35
+ payload: {
36
+ error: message
37
+ }
38
+ });
39
+ /**
40
+ * ONE AUTHORIZED CALL TO THE SENDING-IDENTITY AND SENDING-DOMAIN ROUTES.
41
+ *
42
+ * The same shape as `useCampaignSendApi`, including why the user is held in a
43
+ * ref: this callback is a dependency of effects that load on open, and the
44
+ * user object is a new object on most renders, so depending on it directly
45
+ * would re-issue the request on every render of the surface. The token is
46
+ * fetched at call time either way.
47
+ *
48
+ * The two paths are fixed here rather than taken as an argument, for the
49
+ * reason the campaign hook gives: a hook that accepted a URL from a component
50
+ * would be a way to post the console's credentials somewhere the console does
51
+ * not own.
52
+ *
53
+ * The token is obtained under a deadline, for the reason `useCampaignApi`
54
+ * gives: it is awaited in front of the request, so an unbounded refresh is a
55
+ * call that never reaches `fetch` and never reports anything.
56
+ *
57
+ * ## An unobtainable token ANSWERS here rather than throwing
58
+ *
59
+ * Unlike the campaign hook, whose callers must tell a send that never left
60
+ * the browser apart from one the route refused. Everything on this side reads
61
+ * or edits a configuration surface, where both are the same news to the
62
+ * reader — and every caller already renders `payload.error` on a refusal, so
63
+ * answering in that shape is what puts the real reason on screen and leaves
64
+ * the button that started the call usable.
65
+ */ export function useSendingApi() {
66
+ const { data: user } = useUser();
67
+ const userRef = useRef(user);
68
+ userRef.current = user;
69
+ return useCallback(async (options)=>{
70
+ var _options_query;
71
+ let idToken;
72
+ try {
73
+ idToken = await resolveIdToken(userRef.current);
74
+ } catch (error) {
75
+ return refusedForCaller(describeCallFailure(error, 'Your sign-in could not be confirmed'));
76
+ }
77
+ const search = new URLSearchParams((_options_query = options.query) != null ? _options_query : {}).toString();
78
+ const response = await fetch(`/api/email/${options.path}${search ? `?${search}` : ''}`, _extends({
79
+ method: options.method,
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ Authorization: `Bearer ${idToken}`
83
+ }
84
+ }, options.body ? {
85
+ body: JSON.stringify(options.body)
86
+ } : {}));
87
+ const json = await response.json().catch(()=>({}));
88
+ return {
89
+ response,
90
+ payload: json
91
+ };
92
+ }, []);
93
+ }
94
+
95
+ //# sourceMappingURL=use-sending-identity-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/feature/instance/src/lib/hooks/use-sending-identity-api.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 { useUser } from './firebase/firebase-services'\nimport { useCallback, useRef } from 'react'\nimport {\n describeCallFailure,\n resolveIdToken,\n type TokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\n\n/**\n * The client of the console's sending-identity routes (`/api/email/*`).\n *\n * Which address a site's mail leaves from, and the domains and senders behind\n * it, are the platform's mail rail: the console serves the routes, and every\n * message goes out under that identity whichever plugin wrote it. So the hook\n * that calls them is here, beside the other client hooks a plugin may import,\n * and a one-to-one composer reads the identity without importing the plugin\n * whose settings page edits it.\n */\n\n/** What one call answered with. `payload` is `{}` on an unparseable body. */\nexport interface SendingApiResult {\n response: Response\n payload: Record<string, any>\n}\n\n/**\n * A refusal the caller reads the same way it reads the route's own.\n *\n * `401` because that is what the request would have been answered with had it\n * gone out, and the reason travels in `payload.error`, which is the field\n * every caller already renders.\n */\nconst refusedForCaller = (message: string): SendingApiResult => ({\n response: {\n ok: false,\n status: 401,\n json: async () => ({ error: message }),\n } as unknown as Response,\n payload: { error: message },\n})\n\n/** One DNS record, exactly as the server issued it. */\nexport interface SendingDnsRecordView {\n type: 'TXT' | 'MX'\n name: string\n value: string\n priority?: number\n purpose: 'spf' | 'dkim' | 'return-path' | 'dmarc'\n required: boolean\n note: string\n}\n\n/** One org sending domain, as the domains route reports it. */\nexport interface SendingDomainView {\n domain: string\n status: 'requested' | 'records-issued' | 'verified' | 'failed'\n records?: SendingDnsRecordView[]\n lines?: string[]\n lastMissing?: string[] | null\n lastIssueError?: string | null\n verifiedAtMs?: number | null\n lastCheckedAtMs?: number | null\n dmarc?: {\n policy: 'reject' | 'quarantine' | 'none' | 'absent'\n record: string | null\n consequence: string\n } | null\n dmarcSuggestion?: SendingDnsRecordView | null\n pendingProvider?: boolean\n providerDetail?: string | null\n}\n\n/**\n * One sender this site holds — `hosts/{hostId}/senders/{senderId}`.\n *\n * `from` is the whole address, assembled by the server for the reason\n * `IdentityOption.from` is: a surface that built `${localPart}@${domain}`\n * itself would be a second derivation of the address, and the two would\n * disagree the first time either moved. It is `null` for a row whose mailbox\n * is not in effect — a site on the pooled Aglyn address has one fixed mailbox,\n * shared with the other sites on it.\n */\nexport interface HostSenderView {\n id: string\n localPart: string\n fromName: string | null\n replyTo: string | null\n /** Whether an email that names no sender goes out as this one. */\n isDefault: boolean\n from: string | null\n}\n\n/** What this site sends as, and what it is allowed to send as. */\nexport interface SendingIdentityView {\n orgId: string | null\n selected: string\n /**\n * The site's own provisioned domain inside the platform mail apex, or `''`\n * when it has none and its transactional mail rides the shared pool.\n */\n platformDomain: string\n /**\n * The plan that carries sending as a domain the CUSTOMER owns, and the plan\n * that carries a domain the platform provisions for the site — as names\n * ready to print, `null` when no plan carries that gate.\n *\n * Both come from the server because both are derived from the entitlement\n * tables there. A tier name written into this component would be pricing\n * copy that keeps rendering after the gate beneath it moves.\n */\n customDomainPlan: string | null\n dedicatedDomainPlan: string | null\n /** The mailbox this site sends as — the part before the `@`. */\n localPart: string\n /**\n * Whether that mailbox is the one actually in use.\n *\n * False for a site on the pooled Aglyn address, whose mailbox is fixed. The\n * stored value is real and is kept; it takes effect when the site has a\n * domain of its own, and until then the card says so rather than showing a\n * name that no recipient will see.\n */\n localPartInUse: boolean\n /** The site's default sender name, or null. Composers start from it. */\n fromName: string | null\n /** The site's default reply address, or null. */\n replyTo: string | null\n /**\n * Every sender this site may send as, the default among them.\n *\n * Never empty: a site with no `senders` subcollection has exactly one\n * sender, and the server synthesizes it from the three fields the host\n * document has always carried rather than reporting an absence.\n */\n senders: HostSenderView[]\n identity: string\n /**\n * `'custom'` for a domain this site has verified, `'shared'` for the pooled\n * Aglyn address a site uses until it has one, `null` when the send is\n * refused. `'platform'` is in the union because the resolver can return it,\n * and never reaches this view: it is Aglyn's own `aglyn.com` identity, which\n * a host-scoped resolution cannot produce.\n */\n identitySource: 'custom' | 'shared' | 'platform' | null\n refusal: {\n code: string\n domain: string | null\n message: string\n missing: string[]\n } | null\n options: {\n value: string\n from: string | null\n selectable: boolean\n status: string\n }[]\n domains: {\n domain: string\n status: SendingDomainView['status']\n verifiedAtMs: number | null\n lastCheckedAtMs: number | null\n /**\n * Why the last attempt to issue a key produced none, or `null`.\n *\n * Carried in the LIST and not only on the domain's own page, because one\n * of its values is not a fault: a claim held at the mail provider's domain\n * allowance is `requested` with a reason, and a list that could not see\n * the reason would label it \"Waiting on a signing key\" — a state whose\n * whole instruction is to press a button that will not move it.\n */\n lastIssueError?: string | null\n }[]\n canManage: boolean\n entitled: boolean\n /**\n * The OFFER of a platform sending domain — never the fact of having one,\n * which `platformDomain` above answers.\n *\n * A dedicated subdomain is no longer issued on upgrade: it costs a provider\n * domain slot and three records in Aglyn's own zone, so it is requested by\n * somebody who wants it rather than handed to every paying site.\n *\n * Optional because a console deployment can be older than the field. A\n * surface reading it must treat its absence as \"no offer to show\", never as\n * \"the offer was refused\".\n */\n dedicated?: {\n /** The plan carries one and this site has none. */\n available: boolean\n /** The name a request would most likely take. Not a reservation. */\n proposed: string | null\n }\n}\n\n/**\n * ONE AUTHORIZED CALL TO THE SENDING-IDENTITY AND SENDING-DOMAIN ROUTES.\n *\n * The same shape as `useCampaignSendApi`, including why the user is held in a\n * ref: this callback is a dependency of effects that load on open, and the\n * user object is a new object on most renders, so depending on it directly\n * would re-issue the request on every render of the surface. The token is\n * fetched at call time either way.\n *\n * The two paths are fixed here rather than taken as an argument, for the\n * reason the campaign hook gives: a hook that accepted a URL from a component\n * would be a way to post the console's credentials somewhere the console does\n * not own.\n *\n * The token is obtained under a deadline, for the reason `useCampaignApi`\n * gives: it is awaited in front of the request, so an unbounded refresh is a\n * call that never reaches `fetch` and never reports anything.\n *\n * ## An unobtainable token ANSWERS here rather than throwing\n *\n * Unlike the campaign hook, whose callers must tell a send that never left\n * the browser apart from one the route refused. Everything on this side reads\n * or edits a configuration surface, where both are the same news to the\n * reader — and every caller already renders `payload.error` on a refusal, so\n * answering in that shape is what puts the real reason on screen and leaves\n * the button that started the call usable.\n */\nexport function useSendingApi() {\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n\n return useCallback(\n async (options: {\n path: 'sending-identity' | 'sending-domains'\n method: 'GET' | 'POST' | 'DELETE'\n query?: Record<string, string>\n body?: Record<string, unknown>\n }): Promise<SendingApiResult> => {\n let idToken: string\n try {\n idToken = await resolveIdToken(\n userRef.current as TokenSource | null | undefined,\n )\n } catch (error) {\n return refusedForCaller(\n describeCallFailure(error, 'Your sign-in could not be confirmed'),\n )\n }\n const search = new URLSearchParams(options.query ?? {}).toString()\n const response = await fetch(\n `/api/email/${options.path}${search ? `?${search}` : ''}`,\n {\n method: options.method,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${idToken}`,\n },\n ...(options.body ? { body: JSON.stringify(options.body) } : {}),\n },\n )\n const json = await response.json().catch(() => ({}))\n return { response, payload: json as Record<string, any> }\n },\n [],\n )\n}\n"],"names":["useUser","useCallback","useRef","describeCallFailure","resolveIdToken","refusedForCaller","message","response","ok","status","json","error","payload","useSendingApi","data","user","userRef","current","options","idToken","search","URLSearchParams","query","toString","fetch","path","method","headers","Authorization","body","JSON","stringify","catch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,OAAO,QAAQ,kCAA8B;AACtD,SAASC,WAAW,EAAEC,MAAM,QAAQ,QAAO;AAC3C,SACEC,mBAAmB,EACnBC,cAAc,QAET,2CAA0C;AAmBjD;;;;;;CAMC,GACD,MAAMC,mBAAmB,CAACC,UAAuC,CAAA;QAC/DC,UAAU;YACRC,IAAI;YACJC,QAAQ;YACRC,MAAM,UAAa,CAAA;oBAAEC,OAAOL;gBAAQ,CAAA;QACtC;QACAM,SAAS;YAAED,OAAOL;QAAQ;IAC5B,CAAA;AA2JA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASO;IACd,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGf;IACvB,MAAMgB,UAAUd,OAAOa;IACvBC,QAAQC,OAAO,GAAGF;IAElB,OAAOd,YACL,OAAOiB;YAgB8BA;QAVnC,IAAIC;QACJ,IAAI;YACFA,UAAU,MAAMf,eACdY,QAAQC,OAAO;QAEnB,EAAE,OAAON,OAAO;YACd,OAAON,iBACLF,oBAAoBQ,OAAO;QAE/B;QACA,MAAMS,SAAS,IAAIC,iBAAgBH,iBAAAA,QAAQI,KAAK,YAAbJ,iBAAiB,CAAC,GAAGK,QAAQ;QAChE,MAAMhB,WAAW,MAAMiB,MACrB,CAAC,WAAW,EAAEN,QAAQO,IAAI,GAAGL,SAAS,CAAC,CAAC,EAAEA,QAAQ,GAAG,IAAI,EACzD;YACEM,QAAQR,QAAQQ,MAAM;YACtBC,SAAS;gBACP,gBAAgB;gBAChBC,eAAe,CAAC,OAAO,EAAET,SAAS;YACpC;WACID,QAAQW,IAAI,GAAG;YAAEA,MAAMC,KAAKC,SAAS,CAACb,QAAQW,IAAI;QAAE,IAAI,CAAC;QAGjE,MAAMnB,OAAO,MAAMH,SAASG,IAAI,GAAGsB,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACjD,OAAO;YAAEzB;YAAUK,SAASF;QAA4B;IAC1D,GACA,EAAE;AAEN"}
@@ -0,0 +1,48 @@
1
+ import type { DocumentData, Query } from 'firebase/firestore';
2
+ import { type DependencyList } from 'react';
3
+ import { type CollectionSort } from './sorted-collection-window';
4
+ import { type UseFirestoreCollectionOptions } from './use-firestore-collection';
5
+ import type { UsePagedCollectionResult } from './use-paged-collection';
6
+ export interface UseSortedPagedCollectionOptions extends UseFirestoreCollectionOptions {
7
+ /**
8
+ * Required: the keyed and unkeyed segments are two listeners, and a
9
+ * document moving between them is recognized by its id.
10
+ */
11
+ idField: string;
12
+ /** Rows per page. Defaults to the console-wide smallest option. */
13
+ pageSize?: number;
14
+ /**
15
+ * The fields `buildBase` pins by EQUALITY. A sort on one of them needs no
16
+ * unkeyed segment — see `sortFieldIsTotal`.
17
+ */
18
+ equalityFields?: readonly string[];
19
+ }
20
+ /**
21
+ * A paged window over a live collection SORTED BY A FIELD, which still drops
22
+ * no document that lacks the field (AGL-2853).
23
+ *
24
+ * The same contract as `usePagedCollection` — the window is the query, one
25
+ * page plus a probe row, `hasMore` a fact — over the two-segment walk in
26
+ * `sorted-collection-window.ts`: the documents that carry the sort field in
27
+ * its order, then the ones that do not, in name order. See that module for why
28
+ * both segments are needed and what the second one costs.
29
+ *
30
+ * `buildBase` returns the collection with any EQUALITY predicates and nothing
31
+ * else; the ordering and the limits are this hook's. `deps` identify the
32
+ * subject as they do for `useFirestoreCollection`. A new subject, sort or
33
+ * filter starts again from page one.
34
+ *
35
+ * ```ts
36
+ * useSortedPagedCollection<Entry>(
37
+ * () => query(
38
+ * collection(firestore, 'hosts', hostId, 'collections', collectionId, 'entries'),
39
+ * where('status', '==', status),
40
+ * ),
41
+ * { field: 'publishedAt', direction: 'desc' },
42
+ * [firestore, hostId, collectionId, status],
43
+ * { idField: '$id', equalityFields: ['status'] },
44
+ * )
45
+ * ```
46
+ */
47
+ export declare function useSortedPagedCollection<T = DocumentData>(buildBase: () => Query<DocumentData> | null | undefined, sort: CollectionSort, deps: DependencyList, options: UseSortedPagedCollectionOptions): UsePagedCollectionResult<T>;
48
+ export default useSortedPagedCollection;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
19
+ import { useCallback, useEffect, useRef, useState } from "react";
20
+ import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
21
+ import { planKeyedSegment, planSortedWindow, sortedKeyedQuery, sortedScanLimit, sortedUnkeyedQuery, sortFieldIsTotal } from "./sorted-collection-window.js";
22
+ import { useFirestoreCollection } from "./use-firestore-collection.js";
23
+ /** `Object.is` over two dependency lists, the comparison React itself uses. */ const sameDeps = (a, b)=>a.length === b.length && a.every((value, index)=>Object.is(value, b[index]));
24
+ /**
25
+ * The request a read's CURRENT rows were delivered for.
26
+ *
27
+ * `useFirestoreCollection` re-subscribes in an effect, so the render in which
28
+ * its inputs change still returns the previous subscription's rows — rows for
29
+ * another limit, sort or subject. Planning on them would judge a window by a
30
+ * read that was never made for it: page two's window measured against page
31
+ * one's eleven rows looks like a walk that ran out.
32
+ *
33
+ * So rows are labeled with the request that was current when they ARRIVED,
34
+ * which is the render their array first appears in, and a render whose request
35
+ * has since moved on treats them as unread. Held as state and adjusted during
36
+ * render — the pattern React documents for state that follows a prop — so no
37
+ * render ever pairs a request with rows read for another.
38
+ */ function useCurrentRows(rows, request) {
39
+ const [delivered, setDelivered] = useState(()=>({
40
+ rows,
41
+ request
42
+ }));
43
+ if (delivered.rows !== rows) {
44
+ setDelivered({
45
+ rows,
46
+ request
47
+ });
48
+ return true;
49
+ }
50
+ return sameDeps(delivered.request, request);
51
+ }
52
+ /**
53
+ * A paged window over a live collection SORTED BY A FIELD, which still drops
54
+ * no document that lacks the field (AGL-2853).
55
+ *
56
+ * The same contract as `usePagedCollection` — the window is the query, one
57
+ * page plus a probe row, `hasMore` a fact — over the two-segment walk in
58
+ * `sorted-collection-window.ts`: the documents that carry the sort field in
59
+ * its order, then the ones that do not, in name order. See that module for why
60
+ * both segments are needed and what the second one costs.
61
+ *
62
+ * `buildBase` returns the collection with any EQUALITY predicates and nothing
63
+ * else; the ordering and the limits are this hook's. `deps` identify the
64
+ * subject as they do for `useFirestoreCollection`. A new subject, sort or
65
+ * filter starts again from page one.
66
+ *
67
+ * ```ts
68
+ * useSortedPagedCollection<Entry>(
69
+ * () => query(
70
+ * collection(firestore, 'hosts', hostId, 'collections', collectionId, 'entries'),
71
+ * where('status', '==', status),
72
+ * ),
73
+ * { field: 'publishedAt', direction: 'desc' },
74
+ * [firestore, hostId, collectionId, status],
75
+ * { idField: '$id', equalityFields: ['status'] },
76
+ * )
77
+ * ```
78
+ */ export function useSortedPagedCollection(buildBase, sort, deps, options) {
79
+ var _keyedRead_error;
80
+ const { pageSize: initialPageSize = TABLE_PAGE_SIZE_DEFAULT, equalityFields = [] } = options, collectionOptions = _object_without_properties_loose(options, [
81
+ "pageSize",
82
+ "equalityFields"
83
+ ]);
84
+ const idField = options.idField;
85
+ const [pageSize, setPageSizeState] = useState(initialPageSize);
86
+ const buildBaseRef = useRef(buildBase);
87
+ buildBaseRef.current = buildBase;
88
+ const equalityKey = equalityFields.join('\n');
89
+ const subject = [
90
+ ...deps,
91
+ sort.field,
92
+ sort.direction,
93
+ equalityKey
94
+ ];
95
+ /*
96
+ * The reader's position belongs to ONE subject. It resets in the render the
97
+ * subject changes in, not in an effect after it: an effect would let that
98
+ * render subscribe the new sort at the old page's limit, and a sort change
99
+ * on page four would bill forty-one documents to show ten.
100
+ */ const [cursor, setCursor] = useState(()=>({
101
+ subject,
102
+ page: 0,
103
+ widenedTo: 0
104
+ }));
105
+ const sameSubject = sameDeps(cursor.subject, subject);
106
+ const position = sameSubject ? cursor : {
107
+ subject,
108
+ page: 0,
109
+ widenedTo: 0
110
+ };
111
+ if (!sameSubject) setCursor(position);
112
+ const { page } = position;
113
+ const keyedIsTotal = sortFieldIsTotal(sort, equalityFields);
114
+ const { keyedLimit } = planKeyedSegment({
115
+ page,
116
+ pageSize,
117
+ keyed: undefined,
118
+ keyedSettled: false,
119
+ keyedIsTotal
120
+ });
121
+ const keyedRequest = [
122
+ ...subject,
123
+ keyedLimit
124
+ ];
125
+ const keyedRead = useFirestoreCollection(()=>{
126
+ const base = buildBaseRef.current();
127
+ return base ? sortedKeyedQuery(base, sort, keyedLimit, equalityFields) : null;
128
+ }, keyedRequest, collectionOptions);
129
+ const keyedCurrent = useCurrentRows(keyedRead.data, keyedRequest);
130
+ const keyed = keyedCurrent && keyedRead.status !== 'loading' ? keyedRead.data : undefined;
131
+ const keyedSettled = keyedCurrent && keyedRead.status === 'success' && !keyedRead.fromCache;
132
+ const keyedPlan = planKeyedSegment({
133
+ page,
134
+ pageSize,
135
+ keyed,
136
+ keyedSettled,
137
+ keyedIsTotal
138
+ });
139
+ const scanLimit = sortedScanLimit(keyedPlan, position.widenedTo);
140
+ const scanRequest = [
141
+ ...subject,
142
+ scanLimit
143
+ ];
144
+ const scanRead = useFirestoreCollection(()=>{
145
+ if (scanLimit <= 0) return null;
146
+ const base = buildBaseRef.current();
147
+ return base ? sortedUnkeyedQuery(base, scanLimit) : null;
148
+ }, scanRequest, collectionOptions);
149
+ const scanCurrent = useCurrentRows(scanRead.data, scanRequest);
150
+ const scanOpen = scanLimit > 0;
151
+ const sorted = planSortedWindow({
152
+ page,
153
+ pageSize,
154
+ sort,
155
+ keyedIsTotal,
156
+ keyed,
157
+ keyedSettled,
158
+ scan: scanOpen && scanCurrent && scanRead.status !== 'loading' ? scanRead.data : undefined,
159
+ scanSettled: scanOpen && scanCurrent && scanRead.status === 'success' && !scanRead.fromCache,
160
+ scanLimit,
161
+ idOf: (row)=>String(row[idField])
162
+ });
163
+ /*
164
+ * Widening is remembered for the subject, so paging back to where the scan
165
+ * is not needed and forward again re-opens it at the width it reached
166
+ * instead of doubling its way back up.
167
+ */ const widenTo = sorted.scanLimit;
168
+ useEffect(()=>{
169
+ if (widenTo <= position.widenedTo) return;
170
+ setCursor((previous)=>sameDeps(previous.subject, position.subject) && widenTo > previous.widenedTo ? _extends({}, previous, {
171
+ widenedTo: widenTo
172
+ }) : previous);
173
+ }, [
174
+ widenTo,
175
+ position.widenedTo,
176
+ position.subject
177
+ ]);
178
+ const setPage = useCallback((next)=>{
179
+ setCursor((previous)=>_extends({}, previous, {
180
+ page: next
181
+ }));
182
+ }, []);
183
+ const setPageSize = useCallback((next)=>{
184
+ setPageSizeState(next);
185
+ // Page four of a ten-row list does not exist once the reader asks for
186
+ // fifty at a time, and an out-of-range page renders as an empty list.
187
+ setCursor((previous)=>_extends({}, previous, {
188
+ page: 0
189
+ }));
190
+ }, []);
191
+ /*
192
+ * A short window is a claim that the list ENDS here, so it is `loading`
193
+ * until the server has confirmed both segments. A window holding its probe
194
+ * row claims only that more exists, which a cached read can say.
195
+ */ const errored = keyedRead.status === 'error' || scanOpen && scanRead.status === 'error';
196
+ const status = errored ? 'error' : keyed === undefined ? 'loading' : sorted.hasMore || sorted.settled ? 'success' : 'loading';
197
+ return {
198
+ data: sorted.ordered,
199
+ rows: sorted.rows,
200
+ hasMore: sorted.hasMore,
201
+ page,
202
+ setPage,
203
+ pageSize,
204
+ setPageSize,
205
+ status,
206
+ error: (_keyedRead_error = keyedRead.error) != null ? _keyedRead_error : scanOpen ? scanRead.error : undefined,
207
+ fromCache: keyedRead.fromCache || scanOpen && scanRead.fromCache,
208
+ serverDenied: keyedRead.serverDenied || scanOpen && scanRead.serverDenied
209
+ };
210
+ }
211
+ export default useSortedPagedCollection;
212
+
213
+ //# sourceMappingURL=use-sorted-paged-collection.js.map