@ai-matrx/sharing 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +33 -2
- package/dist/access-react-cant-remove-dialog.cjs +52 -0
- package/dist/access-react-cant-remove-dialog.cjs.map +1 -0
- package/dist/access-react-cant-remove-dialog.d.cts +6 -0
- package/dist/access-react-cant-remove-dialog.d.ts +6 -0
- package/dist/access-react-cant-remove-dialog.js +32 -0
- package/dist/access-react-cant-remove-dialog.js.map +1 -0
- package/dist/access-react.cjs +2816 -0
- package/dist/access-react.cjs.map +1 -0
- package/dist/access-react.d.cts +682 -0
- package/dist/access-react.d.ts +682 -0
- package/dist/access-react.js +2904 -0
- package/dist/access-react.js.map +1 -0
- package/dist/core.cjs +5396 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +4740 -0
- package/dist/core.d.ts +4740 -0
- package/dist/core.js +5372 -0
- package/dist/core.js.map +1 -0
- package/dist/react.cjs +4574 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +779 -0
- package/dist/react.d.ts +779 -0
- package/dist/react.js +4701 -0
- package/dist/react.js.map +1 -0
- package/package.json +133 -4
|
@@ -0,0 +1,2904 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/access/react/env.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useContext,
|
|
8
|
+
useMemo
|
|
9
|
+
} from "react";
|
|
10
|
+
import { Avatar, AvatarFallback, AvatarImage, Textarea } from "@ai-matrx/design-system";
|
|
11
|
+
import { getInitials } from "@ai-matrx/kit/format";
|
|
12
|
+
import { confirm as kitConfirm } from "@ai-matrx/kit/confirm-opener";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
var REQUEST_ACCESS_MANUAL_ACTION = "request_access.manual";
|
|
15
|
+
var DEFAULT_NAV_ITEM_CLASSES = {
|
|
16
|
+
selected: "bg-[var(--shell-nav-selected-bg)] text-[var(--shell-nav-selected-text)] shadow-[var(--shell-nav-selected-shadow)] font-semibold",
|
|
17
|
+
unselected: "text-[var(--shell-nav-unselected-text)] hover:text-[var(--shell-nav-text-hover)] hover:bg-[var(--matrx-glass-bg-hover)]"
|
|
18
|
+
};
|
|
19
|
+
var DefaultLink = forwardRef(
|
|
20
|
+
function DefaultLink2(props, ref) {
|
|
21
|
+
return /* @__PURE__ */ jsx("a", { ref, ...props });
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
var AccessLink = forwardRef(
|
|
25
|
+
function AccessLink2(props, ref) {
|
|
26
|
+
const { Link } = useAccessUi();
|
|
27
|
+
return /* @__PURE__ */ jsx(Link, { ref, ...props });
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
var DEFAULT_ROUTER = {
|
|
31
|
+
push: (href) => {
|
|
32
|
+
if (typeof window !== "undefined") window.location.assign(href);
|
|
33
|
+
},
|
|
34
|
+
back: () => {
|
|
35
|
+
if (typeof window !== "undefined") window.history.back();
|
|
36
|
+
},
|
|
37
|
+
refresh: () => {
|
|
38
|
+
if (typeof window !== "undefined") window.location.reload();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function defaultSearchParam(name) {
|
|
42
|
+
if (typeof window === "undefined") return null;
|
|
43
|
+
return new URLSearchParams(window.location.search).get(name);
|
|
44
|
+
}
|
|
45
|
+
function DefaultTextarea(props) {
|
|
46
|
+
return /* @__PURE__ */ jsx(Textarea, { ...props });
|
|
47
|
+
}
|
|
48
|
+
function DefaultUserIdentity({ user, size = "md", avatarOnly }) {
|
|
49
|
+
const name = user.displayName ?? "Someone";
|
|
50
|
+
const box = size === "sm" ? "h-7 w-7" : "h-9 w-9";
|
|
51
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-2", children: [
|
|
52
|
+
/* @__PURE__ */ jsxs(Avatar, { className: box, children: [
|
|
53
|
+
user.avatarUrl ? /* @__PURE__ */ jsx(AvatarImage, { src: user.avatarUrl, alt: name }) : null,
|
|
54
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "text-[11px]", children: getInitials(user.displayName) })
|
|
55
|
+
] }),
|
|
56
|
+
avatarOnly ? null : /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-sm", children: name })
|
|
57
|
+
] });
|
|
58
|
+
}
|
|
59
|
+
function DefaultEntityRef({ name, className }) {
|
|
60
|
+
return /* @__PURE__ */ jsx("span", { className, children: name });
|
|
61
|
+
}
|
|
62
|
+
var moduleEnv = {};
|
|
63
|
+
var moduleVersion = 0;
|
|
64
|
+
function configureAccessUi(env) {
|
|
65
|
+
moduleEnv = { ...moduleEnv, ...env };
|
|
66
|
+
moduleVersion += 1;
|
|
67
|
+
}
|
|
68
|
+
function isAccessUiConfigured() {
|
|
69
|
+
return Boolean(moduleEnv.useLoginHref && moduleEnv.useCurrentUser && moduleEnv.toast);
|
|
70
|
+
}
|
|
71
|
+
function notConfigured(field) {
|
|
72
|
+
return new Error(
|
|
73
|
+
`[@ai-matrx/sharing/access/react] \`${field}\` was not supplied: add it to configureAccessUi({ ${field} }) once at app boot.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function resolveAccessUi(base, override) {
|
|
77
|
+
const env = { ...base, ...override ?? {} };
|
|
78
|
+
const required = (key) => {
|
|
79
|
+
const value = env[key];
|
|
80
|
+
if (value === void 0 || value === null) throw notConfigured(key);
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
const ui = {
|
|
84
|
+
// Required fields are getters so a screen that never reads one still renders.
|
|
85
|
+
get useLoginHref() {
|
|
86
|
+
return required("useLoginHref");
|
|
87
|
+
},
|
|
88
|
+
get useCurrentUser() {
|
|
89
|
+
return required("useCurrentUser");
|
|
90
|
+
},
|
|
91
|
+
get toast() {
|
|
92
|
+
return required("toast");
|
|
93
|
+
},
|
|
94
|
+
Link: env.Link ?? DefaultLink,
|
|
95
|
+
router: env.router ?? DEFAULT_ROUTER,
|
|
96
|
+
searchParam: env.searchParam ?? defaultSearchParam,
|
|
97
|
+
confirm: env.confirm ?? kitConfirm,
|
|
98
|
+
Textarea: env.Textarea ?? DefaultTextarea,
|
|
99
|
+
renderUserIdentity: env.renderUserIdentity ?? ((p) => /* @__PURE__ */ jsx(DefaultUserIdentity, { ...p })),
|
|
100
|
+
renderEntityRef: env.renderEntityRef ?? ((p) => /* @__PURE__ */ jsx(DefaultEntityRef, { ...p })),
|
|
101
|
+
renderErrorMenu: env.renderErrorMenu ?? (() => null),
|
|
102
|
+
navItemClasses: env.navItemClasses ?? DEFAULT_NAV_ITEM_CLASSES,
|
|
103
|
+
manualRequestActionKey: env.manualRequestActionKey ?? REQUEST_ACCESS_MANUAL_ACTION,
|
|
104
|
+
tryGetEntityInfo: (token) => env.entityRegistry?.tryGetEntityInfo(token) ?? null,
|
|
105
|
+
restoreFromTrash: async (token, id) => {
|
|
106
|
+
if (!env.trash) throw notConfigured("trash");
|
|
107
|
+
await env.trash.restore(token, id);
|
|
108
|
+
},
|
|
109
|
+
resolveRecordUnavailableCapture: (error, resolution) => env.recordUnavailableCapture?.resolve(error, resolution),
|
|
110
|
+
getSettingRequestAction: (key) => env.settingRequestActions?.get(key),
|
|
111
|
+
getResourceActionRequestAction: (key) => env.resourceActionRequests?.get(key),
|
|
112
|
+
createCmsAccessRequest: async (args) => {
|
|
113
|
+
if (!env.cms) throw notConfigured("cms");
|
|
114
|
+
return env.cms.createAccessRequest(args);
|
|
115
|
+
},
|
|
116
|
+
settingDoorHref: (target) => {
|
|
117
|
+
if (!env.settingDoorHref) throw notConfigured("settingDoorHref");
|
|
118
|
+
return env.settingDoorHref(target);
|
|
119
|
+
},
|
|
120
|
+
reportFeedback: async (input) => {
|
|
121
|
+
if (!env.reportFeedback) throw notConfigured("reportFeedback");
|
|
122
|
+
return env.reportFeedback(input);
|
|
123
|
+
},
|
|
124
|
+
presentOrganizationRefusal: (error, options) => env.presentOrganizationRefusal?.(error, options) ?? false,
|
|
125
|
+
openOverlay: (overlayId, instanceId, data) => {
|
|
126
|
+
if (!env.overlays) throw notConfigured("overlays");
|
|
127
|
+
env.overlays.open(overlayId, instanceId, data);
|
|
128
|
+
},
|
|
129
|
+
closeOverlay: (overlayId, instanceId) => env.overlays?.close(overlayId, instanceId)
|
|
130
|
+
};
|
|
131
|
+
return ui;
|
|
132
|
+
}
|
|
133
|
+
function getAccessUi() {
|
|
134
|
+
return resolveAccessUi(moduleEnv, null);
|
|
135
|
+
}
|
|
136
|
+
var AccessUiContext = createContext(null);
|
|
137
|
+
function AccessUiProvider({
|
|
138
|
+
env,
|
|
139
|
+
children
|
|
140
|
+
}) {
|
|
141
|
+
return /* @__PURE__ */ jsx(AccessUiContext.Provider, { value: env, children });
|
|
142
|
+
}
|
|
143
|
+
function useAccessUi() {
|
|
144
|
+
const override = useContext(AccessUiContext);
|
|
145
|
+
return useMemo(
|
|
146
|
+
() => resolveAccessUi(moduleEnv, override),
|
|
147
|
+
// `moduleVersion` is read on purpose: configuring after first render is picked up by the next mount.
|
|
148
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
|
+
[override, moduleVersion]
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/access/react/AccessDenied.tsx
|
|
154
|
+
import { useState as useState4 } from "react";
|
|
155
|
+
import {
|
|
156
|
+
ArrowLeft,
|
|
157
|
+
Building2,
|
|
158
|
+
Lock,
|
|
159
|
+
LogIn,
|
|
160
|
+
RefreshCw,
|
|
161
|
+
RotateCcw,
|
|
162
|
+
SearchX,
|
|
163
|
+
Trash2,
|
|
164
|
+
TriangleAlert
|
|
165
|
+
} from "lucide-react";
|
|
166
|
+
import { Avatar as Avatar2, AvatarFallback as AvatarFallback2, AvatarImage as AvatarImage2, Button as Button3 } from "@ai-matrx/design-system";
|
|
167
|
+
|
|
168
|
+
// src/access/react/RequestAccessPanel.tsx
|
|
169
|
+
import { useState } from "react";
|
|
170
|
+
import { Check, Loader2, Send, Undo2 } from "lucide-react";
|
|
171
|
+
import { Button } from "@ai-matrx/design-system";
|
|
172
|
+
import { getAccessEnv } from "@ai-matrx/data/access";
|
|
173
|
+
import {
|
|
174
|
+
createAccessRequest,
|
|
175
|
+
withdrawAccessRequest
|
|
176
|
+
} from "@ai-matrx/data/access";
|
|
177
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
178
|
+
function ownerName(context) {
|
|
179
|
+
return context.owner?.displayName ?? "the owner";
|
|
180
|
+
}
|
|
181
|
+
function RequestAccessPanel({
|
|
182
|
+
context,
|
|
183
|
+
resourceId,
|
|
184
|
+
href,
|
|
185
|
+
onChanged
|
|
186
|
+
}) {
|
|
187
|
+
const ui = useAccessUi();
|
|
188
|
+
const { toast } = ui;
|
|
189
|
+
const ProTextarea = ui.Textarea;
|
|
190
|
+
const currentUserId = ui.useCurrentUser().id;
|
|
191
|
+
const [level, setLevel] = useState("viewer");
|
|
192
|
+
const [note, setNote] = useState("");
|
|
193
|
+
const [busy, setBusy] = useState(false);
|
|
194
|
+
const [delivered, setDelivered] = useState(null);
|
|
195
|
+
const [reasking, setReasking] = useState(false);
|
|
196
|
+
const request = context.request;
|
|
197
|
+
const kind = context.entity.label.toLowerCase();
|
|
198
|
+
const cmsToken = getAccessEnv().entityTokens?.isCmsAccessGateToken(context.entity.token) ? context.entity.token : null;
|
|
199
|
+
const hrToken = context.entity.token === "hr_review_cycle";
|
|
200
|
+
const requestRecipientName = cmsToken ? context.organization?.name ?? "the organization\u2019s admins" : hrToken ? "HR" : ownerName(context);
|
|
201
|
+
async function send() {
|
|
202
|
+
setBusy(true);
|
|
203
|
+
try {
|
|
204
|
+
const result = cmsToken ? await ui.createCmsAccessRequest({
|
|
205
|
+
token: cmsToken,
|
|
206
|
+
resourceId,
|
|
207
|
+
message: note,
|
|
208
|
+
currentUserId
|
|
209
|
+
}) : await createAccessRequest({
|
|
210
|
+
resourceType: context.entity.token,
|
|
211
|
+
resourceId,
|
|
212
|
+
level,
|
|
213
|
+
message: note,
|
|
214
|
+
currentUserId,
|
|
215
|
+
href: href ?? null
|
|
216
|
+
});
|
|
217
|
+
if (result.blindAnswer) {
|
|
218
|
+
toast.success(result.blindAnswer);
|
|
219
|
+
} else if (result.already) {
|
|
220
|
+
toast.success("You've already asked \u2014 they haven't answered yet.");
|
|
221
|
+
} else if (result.delivered === 0) {
|
|
222
|
+
toast.warning(
|
|
223
|
+
"Your request is saved, but we couldn't message them just now."
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
toast.success(
|
|
227
|
+
`Request sent to ${(hrToken ? null : result.recipients[0]?.displayName) ?? requestRecipientName}.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
setDelivered(result.already ? null : result.delivered ?? null);
|
|
231
|
+
onChanged();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
toast.error(
|
|
234
|
+
error instanceof Error ? error.message : "We couldn't send that."
|
|
235
|
+
);
|
|
236
|
+
} finally {
|
|
237
|
+
setBusy(false);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function withdraw() {
|
|
241
|
+
if (!request) return;
|
|
242
|
+
setBusy(true);
|
|
243
|
+
try {
|
|
244
|
+
await withdrawAccessRequest(request.id);
|
|
245
|
+
toast.success("Request withdrawn.");
|
|
246
|
+
onChanged();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
toast.error(
|
|
249
|
+
error instanceof Error ? error.message : "We couldn't withdraw that."
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
setBusy(false);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (request?.status === "pending") {
|
|
256
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border border-border bg-muted/40 p-4", children: [
|
|
257
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm text-foreground", children: [
|
|
258
|
+
/* @__PURE__ */ jsx2(Check, { className: "mr-1.5 inline h-4 w-4 text-primary", "aria-hidden": true }),
|
|
259
|
+
"Waiting on ",
|
|
260
|
+
requestRecipientName,
|
|
261
|
+
"."
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsxs2("p", { className: "mt-1 text-xs text-muted-foreground", children: [
|
|
264
|
+
cmsToken ? "Your request is saved. Once an owner adds you to the organization, this site will open." : delivered === 0 ? "We couldn't reach them with a message, but your request is saved and they'll see it." : "They\u2019ve been messaged. You\u2019ll get a message back the moment they answer, and this page will open.",
|
|
265
|
+
ui.renderErrorMenu({})
|
|
266
|
+
] }),
|
|
267
|
+
/* @__PURE__ */ jsxs2(
|
|
268
|
+
Button,
|
|
269
|
+
{
|
|
270
|
+
className: "mt-3 h-8",
|
|
271
|
+
size: "sm",
|
|
272
|
+
variant: "outline",
|
|
273
|
+
disabled: busy,
|
|
274
|
+
onClick: () => void withdraw(),
|
|
275
|
+
children: [
|
|
276
|
+
busy ? /* @__PURE__ */ jsx2(Loader2, { className: "mr-1.5 h-3.5 w-3.5 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx2(Undo2, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": true }),
|
|
277
|
+
"Withdraw request"
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
] });
|
|
282
|
+
}
|
|
283
|
+
if (request?.status === "declined" && !reasking) {
|
|
284
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border border-border bg-muted/40 p-4", children: [
|
|
285
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm text-foreground", children: [
|
|
286
|
+
requestRecipientName,
|
|
287
|
+
" declined this request."
|
|
288
|
+
] }),
|
|
289
|
+
request.decisionNote ? /* @__PURE__ */ jsxs2("p", { className: "mt-1 text-xs text-muted-foreground", children: [
|
|
290
|
+
"\u201C",
|
|
291
|
+
request.decisionNote,
|
|
292
|
+
"\u201D"
|
|
293
|
+
] }) : null,
|
|
294
|
+
/* @__PURE__ */ jsxs2(
|
|
295
|
+
Button,
|
|
296
|
+
{
|
|
297
|
+
className: "mt-3 h-8",
|
|
298
|
+
size: "sm",
|
|
299
|
+
variant: "outline",
|
|
300
|
+
onClick: () => setReasking(true),
|
|
301
|
+
children: [
|
|
302
|
+
/* @__PURE__ */ jsx2(Send, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": true }),
|
|
303
|
+
"Ask again"
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
] });
|
|
308
|
+
}
|
|
309
|
+
if (request?.status === "reported" || !context.canRequest) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border border-border bg-card p-4", children: [
|
|
313
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm font-medium text-foreground", children: [
|
|
314
|
+
"Ask ",
|
|
315
|
+
requestRecipientName,
|
|
316
|
+
" for access"
|
|
317
|
+
] }),
|
|
318
|
+
!cmsToken ? /* @__PURE__ */ jsxs2("div", { className: "mt-3 flex flex-wrap gap-2", children: [
|
|
319
|
+
/* @__PURE__ */ jsx2(
|
|
320
|
+
LevelChoice,
|
|
321
|
+
{
|
|
322
|
+
active: level === "viewer",
|
|
323
|
+
label: "Just to view",
|
|
324
|
+
onClick: () => setLevel("viewer")
|
|
325
|
+
}
|
|
326
|
+
),
|
|
327
|
+
/* @__PURE__ */ jsx2(
|
|
328
|
+
LevelChoice,
|
|
329
|
+
{
|
|
330
|
+
active: level === "editor",
|
|
331
|
+
label: "To view and make changes",
|
|
332
|
+
onClick: () => setLevel("editor")
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
] }) : null,
|
|
336
|
+
/* @__PURE__ */ jsx2(
|
|
337
|
+
ProTextarea,
|
|
338
|
+
{
|
|
339
|
+
className: "mt-3 min-h-16 text-base md:text-sm",
|
|
340
|
+
placeholder: `Why you need this ${kind} (optional)`,
|
|
341
|
+
value: note,
|
|
342
|
+
maxLength: 500,
|
|
343
|
+
onChange: (event) => setNote(event.target.value)
|
|
344
|
+
}
|
|
345
|
+
),
|
|
346
|
+
/* @__PURE__ */ jsxs2(
|
|
347
|
+
Button,
|
|
348
|
+
{
|
|
349
|
+
className: "mt-3",
|
|
350
|
+
size: "sm",
|
|
351
|
+
disabled: busy,
|
|
352
|
+
onClick: () => void send(),
|
|
353
|
+
children: [
|
|
354
|
+
busy ? /* @__PURE__ */ jsx2(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx2(Send, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
355
|
+
"Request access"
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
] });
|
|
360
|
+
}
|
|
361
|
+
function LevelChoice({
|
|
362
|
+
active,
|
|
363
|
+
label,
|
|
364
|
+
onClick
|
|
365
|
+
}) {
|
|
366
|
+
return /* @__PURE__ */ jsx2(
|
|
367
|
+
"button",
|
|
368
|
+
{
|
|
369
|
+
type: "button",
|
|
370
|
+
"aria-pressed": active,
|
|
371
|
+
onClick,
|
|
372
|
+
className: [
|
|
373
|
+
"rounded-full border px-3 py-1 text-xs font-medium transition-colors",
|
|
374
|
+
active ? "border-primary bg-primary/10 text-primary-ink" : "border-border bg-background text-muted-foreground hover:bg-accent"
|
|
375
|
+
].join(" "),
|
|
376
|
+
children: label
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/access/react/BlindAccessAsk.tsx
|
|
382
|
+
import { useState as useState2 } from "react";
|
|
383
|
+
import { KeyRound } from "lucide-react";
|
|
384
|
+
import { Button as Button2 } from "@ai-matrx/design-system";
|
|
385
|
+
import { askForAccessBlind, BLIND_ASK_ANSWER } from "@ai-matrx/data/access";
|
|
386
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
387
|
+
function BlindAccessAsk({
|
|
388
|
+
token,
|
|
389
|
+
id,
|
|
390
|
+
href
|
|
391
|
+
}) {
|
|
392
|
+
const ui = useAccessUi();
|
|
393
|
+
const [state, setState] = useState2({ kind: "idle" });
|
|
394
|
+
if (state.kind === "asked") {
|
|
395
|
+
return /* @__PURE__ */ jsx3("p", { className: "text-sm text-foreground", role: "status", "data-blind-ask": "asked", children: state.says || BLIND_ASK_ANSWER });
|
|
396
|
+
}
|
|
397
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-start gap-2", "data-blind-ask": state.kind, children: [
|
|
398
|
+
/* @__PURE__ */ jsxs3(
|
|
399
|
+
Button2,
|
|
400
|
+
{
|
|
401
|
+
size: "sm",
|
|
402
|
+
disabled: state.kind === "asking",
|
|
403
|
+
"aria-busy": state.kind === "asking",
|
|
404
|
+
onClick: () => {
|
|
405
|
+
setState({ kind: "asking" });
|
|
406
|
+
void askForAccessBlind({
|
|
407
|
+
resourceType: token,
|
|
408
|
+
resourceId: id,
|
|
409
|
+
href: href ?? (typeof window !== "undefined" ? window.location.pathname : null)
|
|
410
|
+
}).then(
|
|
411
|
+
(says) => setState({ kind: "asked", says }),
|
|
412
|
+
(e) => setState({
|
|
413
|
+
kind: "failed",
|
|
414
|
+
why: e instanceof Error ? e.message : "We could not send that just now."
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
"data-tap-target": true,
|
|
419
|
+
children: [
|
|
420
|
+
/* @__PURE__ */ jsx3(KeyRound, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
421
|
+
state.kind === "asking" ? "Asking\u2026" : "Ask for access"
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
),
|
|
425
|
+
state.kind === "failed" ? /* @__PURE__ */ jsxs3("p", { className: "text-xs text-destructive", role: "alert", children: [
|
|
426
|
+
state.why,
|
|
427
|
+
" Try again.",
|
|
428
|
+
ui.renderErrorMenu({ className: "ml-auto" })
|
|
429
|
+
] }) : null
|
|
430
|
+
] });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/access/react/useAccessGate.ts
|
|
434
|
+
import { useCallback, useEffect, useState as useState3 } from "react";
|
|
435
|
+
import { fetchAccessDeniedContext } from "@ai-matrx/data/access";
|
|
436
|
+
import { classifyDataError } from "@ai-matrx/data/access";
|
|
437
|
+
function useAccessGate(token, id, options = {}) {
|
|
438
|
+
const resolveRecordUnavailableCapture = useAccessUi().resolveRecordUnavailableCapture;
|
|
439
|
+
const [nonce, setNonce] = useState3(0);
|
|
440
|
+
const enabled = options.enabled !== false && Boolean(token && id);
|
|
441
|
+
const key = enabled ? `${token}:${id}:${nonce}` : null;
|
|
442
|
+
const [resolved, setResolved] = useState3(null);
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
if (!key || !token || !id) return;
|
|
445
|
+
let active = true;
|
|
446
|
+
const read = classifyDataError(options.readError) === "fault" ? "fault" : "access-question";
|
|
447
|
+
void fetchAccessDeniedContext(token, id, read).then((next) => {
|
|
448
|
+
if (!active) return;
|
|
449
|
+
const resolution = next.status === "anonymous" ? "signed-out" : next.status === "denied" || next.status === "deleted" || next.status === "missing" || next.status === "ok" ? next.status : null;
|
|
450
|
+
if (resolution) {
|
|
451
|
+
resolveRecordUnavailableCapture(options.readError, resolution);
|
|
452
|
+
}
|
|
453
|
+
setResolved({ key, context: next });
|
|
454
|
+
});
|
|
455
|
+
return () => {
|
|
456
|
+
active = false;
|
|
457
|
+
};
|
|
458
|
+
}, [key, token, id, options.readError, resolveRecordUnavailableCapture]);
|
|
459
|
+
const context = resolved && resolved.key === key ? resolved.context : null;
|
|
460
|
+
const isLoading = key !== null && context === null;
|
|
461
|
+
const refresh = useCallback(() => setNonce((n) => n + 1), []);
|
|
462
|
+
return {
|
|
463
|
+
context,
|
|
464
|
+
status: isLoading ? "loading" : context?.status ?? "error",
|
|
465
|
+
isLoading,
|
|
466
|
+
refresh
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/access/react/AccessDenied.tsx
|
|
471
|
+
import { getInitials as getInitials2 } from "@ai-matrx/kit/format";
|
|
472
|
+
import { ErrorNotice } from "@ai-matrx/design-system";
|
|
473
|
+
import {
|
|
474
|
+
archivedExplanation,
|
|
475
|
+
archivedHeadline,
|
|
476
|
+
mayRestoreArchived
|
|
477
|
+
} from "@ai-matrx/data/access";
|
|
478
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
479
|
+
function RestoreArchived({
|
|
480
|
+
token,
|
|
481
|
+
id,
|
|
482
|
+
kind,
|
|
483
|
+
onRestored
|
|
484
|
+
}) {
|
|
485
|
+
const ui = useAccessUi();
|
|
486
|
+
const [busy, setBusy] = useState4(false);
|
|
487
|
+
const [failure, setFailure] = useState4(null);
|
|
488
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
489
|
+
/* @__PURE__ */ jsxs4(
|
|
490
|
+
Button3,
|
|
491
|
+
{
|
|
492
|
+
size: "sm",
|
|
493
|
+
disabled: busy,
|
|
494
|
+
onClick: async () => {
|
|
495
|
+
setBusy(true);
|
|
496
|
+
setFailure(null);
|
|
497
|
+
try {
|
|
498
|
+
await ui.restoreFromTrash(token, id);
|
|
499
|
+
ui.toast.success(`Restored the ${kind}.`);
|
|
500
|
+
onRestored();
|
|
501
|
+
} catch (error) {
|
|
502
|
+
setFailure(error);
|
|
503
|
+
} finally {
|
|
504
|
+
setBusy(false);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
children: [
|
|
508
|
+
/* @__PURE__ */ jsx4(RotateCcw, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
509
|
+
busy ? "Restoring\u2026" : "Restore"
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
),
|
|
513
|
+
failure ? /* @__PURE__ */ jsx4(
|
|
514
|
+
ErrorNotice,
|
|
515
|
+
{
|
|
516
|
+
size: "inline",
|
|
517
|
+
className: "basis-full text-sm",
|
|
518
|
+
title: "Not restored",
|
|
519
|
+
error: failure,
|
|
520
|
+
operation: `Restore this ${kind}`,
|
|
521
|
+
records: [{ type: token, id }]
|
|
522
|
+
}
|
|
523
|
+
) : null
|
|
524
|
+
] });
|
|
525
|
+
}
|
|
526
|
+
function headline(context) {
|
|
527
|
+
const kind = context.entity.label.toLowerCase();
|
|
528
|
+
switch (context.status) {
|
|
529
|
+
case "denied":
|
|
530
|
+
return `You don't have access to this ${kind}`;
|
|
531
|
+
case "deleted":
|
|
532
|
+
return archivedHeadline(context);
|
|
533
|
+
case "missing":
|
|
534
|
+
return `We couldn't find this ${kind}`;
|
|
535
|
+
case "anonymous":
|
|
536
|
+
return `Sign in to open this ${kind}`;
|
|
537
|
+
case "ok":
|
|
538
|
+
return `We couldn't load this ${kind}`;
|
|
539
|
+
default:
|
|
540
|
+
return "Something went wrong";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function explanation(context) {
|
|
544
|
+
const kind = context.entity.label.toLowerCase();
|
|
545
|
+
switch (context.status) {
|
|
546
|
+
case "denied": {
|
|
547
|
+
const owner = context.organization ? context.organization.name : context.owner?.displayName;
|
|
548
|
+
if (!context.canRequest && context.request?.status !== "pending") {
|
|
549
|
+
return owner ? `It belongs to ${owner}, and there's no one for us to pass a request to.` : `It belongs to someone else, and there's no one for us to pass a request to.`;
|
|
550
|
+
}
|
|
551
|
+
return owner ? `It belongs to ${owner}. You can ask for access below.` : `It belongs to someone else. You can ask for access below.`;
|
|
552
|
+
}
|
|
553
|
+
case "deleted":
|
|
554
|
+
return archivedExplanation(context);
|
|
555
|
+
case "missing":
|
|
556
|
+
return `If someone shared a link with you, you can ask for access.`;
|
|
557
|
+
case "anonymous":
|
|
558
|
+
return `We can't tell you anything about it until we know who you are.`;
|
|
559
|
+
case "ok":
|
|
560
|
+
return `You do have access to it \u2014 something went wrong on our side. Try again.`;
|
|
561
|
+
default:
|
|
562
|
+
return `We couldn't work out what happened. Try again in a moment.`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function StatusIcon({ status }) {
|
|
566
|
+
const className = "h-7 w-7 text-muted-foreground";
|
|
567
|
+
if (status === "denied") return /* @__PURE__ */ jsx4(Lock, { className, "aria-hidden": true });
|
|
568
|
+
if (status === "deleted") return /* @__PURE__ */ jsx4(Trash2, { className, "aria-hidden": true });
|
|
569
|
+
if (status === "missing")
|
|
570
|
+
return /* @__PURE__ */ jsx4(SearchX, { className, "aria-hidden": true });
|
|
571
|
+
if (status === "anonymous")
|
|
572
|
+
return /* @__PURE__ */ jsx4(LogIn, { className, "aria-hidden": true });
|
|
573
|
+
return /* @__PURE__ */ jsx4(TriangleAlert, { className, "aria-hidden": true });
|
|
574
|
+
}
|
|
575
|
+
function IdentityBlock({
|
|
576
|
+
label,
|
|
577
|
+
name,
|
|
578
|
+
href,
|
|
579
|
+
avatar
|
|
580
|
+
}) {
|
|
581
|
+
const body = /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
582
|
+
avatar,
|
|
583
|
+
/* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
|
|
584
|
+
/* @__PURE__ */ jsx4("span", { className: "block text-[11px] uppercase tracking-wide text-muted-foreground", children: label }),
|
|
585
|
+
/* @__PURE__ */ jsx4("span", { className: "block truncate text-foreground", children: name })
|
|
586
|
+
] })
|
|
587
|
+
] });
|
|
588
|
+
if (!href) {
|
|
589
|
+
return /* @__PURE__ */ jsx4("span", { className: "flex min-w-0 items-center gap-2 text-sm", children: body });
|
|
590
|
+
}
|
|
591
|
+
return /* @__PURE__ */ jsx4(
|
|
592
|
+
AccessLink,
|
|
593
|
+
{
|
|
594
|
+
href,
|
|
595
|
+
className: "flex min-w-0 items-center gap-2 text-sm hover:underline",
|
|
596
|
+
children: body
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
function AccessDeniedView({
|
|
601
|
+
context,
|
|
602
|
+
id,
|
|
603
|
+
fallbackHref,
|
|
604
|
+
fallbackLabel,
|
|
605
|
+
onRetry,
|
|
606
|
+
onChanged,
|
|
607
|
+
suggestions = [],
|
|
608
|
+
requestability = "requestable",
|
|
609
|
+
reason,
|
|
610
|
+
headline: headlineOverride,
|
|
611
|
+
footer
|
|
612
|
+
}) {
|
|
613
|
+
const ui = useAccessUi();
|
|
614
|
+
const router = ui.router;
|
|
615
|
+
const signInHref = ui.useLoginHref();
|
|
616
|
+
const tryGetEntityInfo = ui.tryGetEntityInfo;
|
|
617
|
+
const showOrg = Boolean(context.organization);
|
|
618
|
+
const ancestor = context.ancestor;
|
|
619
|
+
const ancestorInfo = ancestor ? tryGetEntityInfo(ancestor.token) : null;
|
|
620
|
+
const ancestorHref = ancestorInfo?.hrefFor?.(ancestor?.id ?? "") ?? null;
|
|
621
|
+
const selfInfo = tryGetEntityInfo(context.entity.token);
|
|
622
|
+
const selfHref = selfInfo?.hrefFor?.(id) ?? null;
|
|
623
|
+
const absolute = requestability === "absolute" && context.status === "denied";
|
|
624
|
+
const title = headlineOverride?.trim() || (absolute ? "You can't open this here" : headline(context));
|
|
625
|
+
const body = reason?.trim() || (absolute ? "" : explanation(context));
|
|
626
|
+
return /* @__PURE__ */ jsx4("div", { className: "matrx-touch-targets flex h-full min-h-64 w-full items-center justify-center p-6", children: /* @__PURE__ */ jsxs4("div", { className: "w-full max-w-xl", children: [
|
|
627
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-4", children: [
|
|
628
|
+
/* @__PURE__ */ jsx4("div", { className: "flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-border bg-muted", children: /* @__PURE__ */ jsx4(StatusIcon, { status: context.status }) }),
|
|
629
|
+
/* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
|
|
630
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-2", children: [
|
|
631
|
+
/* @__PURE__ */ jsx4("h1", { className: "min-w-0 flex-1 text-xl font-semibold tracking-tight text-foreground", children: title }),
|
|
632
|
+
ui.renderErrorMenu({
|
|
633
|
+
size: "icon",
|
|
634
|
+
input: {
|
|
635
|
+
title,
|
|
636
|
+
message: body || title,
|
|
637
|
+
operation: absolute || context.status === "missing" ? "Open this record" : `Open this ${context.entity.label.toLowerCase()}`,
|
|
638
|
+
...absolute || context.status === "missing" ? {} : { records: [{ type: context.entity.token, id }] },
|
|
639
|
+
details: { access_status: context.status },
|
|
640
|
+
source: "route-boundary"
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
] }),
|
|
644
|
+
context.entity.title ? /* @__PURE__ */ jsxs4("p", { className: "mt-1 truncate text-sm text-muted-foreground", children: [
|
|
645
|
+
context.entity.label,
|
|
646
|
+
" \xB7",
|
|
647
|
+
" ",
|
|
648
|
+
/* @__PURE__ */ jsx4("span", { className: "text-foreground", children: context.entity.title })
|
|
649
|
+
] }) : null,
|
|
650
|
+
body ? /* @__PURE__ */ jsx4("p", { className: "mt-2 text-sm text-muted-foreground", children: body }) : null
|
|
651
|
+
] })
|
|
652
|
+
] }),
|
|
653
|
+
!absolute && (context.owner || showOrg) ? /* @__PURE__ */ jsxs4("div", { className: "mt-5 flex flex-wrap items-center gap-4 rounded-lg border border-border bg-muted/30 px-4 py-3", children: [
|
|
654
|
+
context.owner ? /* @__PURE__ */ jsx4(
|
|
655
|
+
IdentityBlock,
|
|
656
|
+
{
|
|
657
|
+
label: "Owner",
|
|
658
|
+
name: context.owner.displayName ?? "Someone else",
|
|
659
|
+
href: context.owner.creatorHandle ? `/c/${context.owner.creatorHandle}` : null,
|
|
660
|
+
avatar: /* @__PURE__ */ jsxs4(Avatar2, { className: "h-7 w-7", children: [
|
|
661
|
+
context.owner.avatarUrl ? /* @__PURE__ */ jsx4(
|
|
662
|
+
AvatarImage2,
|
|
663
|
+
{
|
|
664
|
+
src: context.owner.avatarUrl,
|
|
665
|
+
alt: context.owner.displayName ?? "Owner"
|
|
666
|
+
}
|
|
667
|
+
) : null,
|
|
668
|
+
/* @__PURE__ */ jsx4(AvatarFallback2, { className: "text-[11px]", children: getInitials2(context.owner.displayName) })
|
|
669
|
+
] })
|
|
670
|
+
}
|
|
671
|
+
) : null,
|
|
672
|
+
showOrg && context.organization ? /* @__PURE__ */ jsx4(
|
|
673
|
+
IdentityBlock,
|
|
674
|
+
{
|
|
675
|
+
label: "Organization",
|
|
676
|
+
name: context.organization.name ?? "An organization",
|
|
677
|
+
href: context.organization.viewerIsMember ? `/organizations/${context.organization.id}` : null,
|
|
678
|
+
avatar: /* @__PURE__ */ jsx4("span", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-background", children: /* @__PURE__ */ jsx4(
|
|
679
|
+
Building2,
|
|
680
|
+
{
|
|
681
|
+
className: "h-3.5 w-3.5 text-muted-foreground",
|
|
682
|
+
"aria-hidden": true
|
|
683
|
+
}
|
|
684
|
+
) })
|
|
685
|
+
}
|
|
686
|
+
) : null
|
|
687
|
+
] }) : null,
|
|
688
|
+
context.status === "denied" && !absolute ? /* @__PURE__ */ jsx4("div", { className: "mt-4", children: /* @__PURE__ */ jsx4(
|
|
689
|
+
RequestAccessPanel,
|
|
690
|
+
{
|
|
691
|
+
context,
|
|
692
|
+
resourceId: id,
|
|
693
|
+
href: selfHref,
|
|
694
|
+
onChanged
|
|
695
|
+
}
|
|
696
|
+
) }) : null,
|
|
697
|
+
context.status === "missing" && requestability !== "absolute" ? /* @__PURE__ */ jsx4("div", { className: "mt-4", children: /* @__PURE__ */ jsx4(BlindAccessAsk, { token: context.entity.token, id, href: selfHref }) }) : null,
|
|
698
|
+
suggestions.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-5 grid gap-2 sm:grid-cols-2", children: suggestions.map((s) => {
|
|
699
|
+
const Icon = s.icon;
|
|
700
|
+
return /* @__PURE__ */ jsxs4(
|
|
701
|
+
AccessLink,
|
|
702
|
+
{
|
|
703
|
+
href: s.href,
|
|
704
|
+
"data-tap-target": true,
|
|
705
|
+
className: "group flex items-start gap-3 rounded-lg border border-border bg-card px-4 py-3 transition-colors hover:border-foreground/20 hover:bg-accent",
|
|
706
|
+
children: [
|
|
707
|
+
Icon ? /* @__PURE__ */ jsx4("span", { className: "mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground group-hover:text-foreground", children: /* @__PURE__ */ jsx4(Icon, { className: "h-4 w-4" }) }) : null,
|
|
708
|
+
/* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
|
|
709
|
+
/* @__PURE__ */ jsx4("span", { className: "block text-sm font-medium text-foreground", children: s.title }),
|
|
710
|
+
s.description ? /* @__PURE__ */ jsx4("span", { className: "mt-0.5 block text-xs leading-snug text-muted-foreground", children: s.description }) : null
|
|
711
|
+
] })
|
|
712
|
+
]
|
|
713
|
+
},
|
|
714
|
+
s.href + s.title
|
|
715
|
+
);
|
|
716
|
+
}) }) : null,
|
|
717
|
+
footer ? /* @__PURE__ */ jsx4("div", { className: "mt-4", children: footer }) : null,
|
|
718
|
+
/* @__PURE__ */ jsxs4("div", { className: "mt-5 flex flex-wrap items-center gap-2", children: [
|
|
719
|
+
context.status === "anonymous" ? /* @__PURE__ */ jsx4(Button3, { asChild: true, size: "sm", children: /* @__PURE__ */ jsxs4(AccessLink, { href: signInHref, "data-tap-target": true, children: [
|
|
720
|
+
/* @__PURE__ */ jsx4(LogIn, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
721
|
+
"Sign in"
|
|
722
|
+
] }) }) : null,
|
|
723
|
+
mayRestoreArchived(context) ? /* @__PURE__ */ jsx4(
|
|
724
|
+
RestoreArchived,
|
|
725
|
+
{
|
|
726
|
+
token: context.entity.token,
|
|
727
|
+
id,
|
|
728
|
+
kind: context.entity.label.toLowerCase(),
|
|
729
|
+
onRestored: () => {
|
|
730
|
+
onChanged();
|
|
731
|
+
if (onRetry) onRetry();
|
|
732
|
+
else router.refresh();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
) : null,
|
|
736
|
+
(context.status === "ok" || context.status === "error") && onRetry ? /* @__PURE__ */ jsxs4(Button3, { size: "sm", onClick: onRetry, children: [
|
|
737
|
+
/* @__PURE__ */ jsx4(RefreshCw, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
738
|
+
"Try again"
|
|
739
|
+
] }) : null,
|
|
740
|
+
ancestor && ancestorHref ? /* @__PURE__ */ jsx4(Button3, { asChild: true, size: "sm", variant: "outline", children: /* @__PURE__ */ jsxs4(AccessLink, { href: ancestorHref, "data-tap-target": true, children: [
|
|
741
|
+
/* @__PURE__ */ jsx4(ArrowLeft, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
742
|
+
"Open ",
|
|
743
|
+
ancestor.title ?? ancestor.label.toLowerCase()
|
|
744
|
+
] }) }) : fallbackHref ? /* @__PURE__ */ jsx4(Button3, { asChild: true, size: "sm", variant: "outline", children: /* @__PURE__ */ jsxs4(AccessLink, { href: fallbackHref, "data-tap-target": true, children: [
|
|
745
|
+
/* @__PURE__ */ jsx4(ArrowLeft, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
746
|
+
fallbackLabel ?? "Back to what you can see"
|
|
747
|
+
] }) }) : /* @__PURE__ */ jsxs4(Button3, { size: "sm", variant: "outline", onClick: () => router.back(), children: [
|
|
748
|
+
/* @__PURE__ */ jsx4(ArrowLeft, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
749
|
+
"Go back"
|
|
750
|
+
] })
|
|
751
|
+
] })
|
|
752
|
+
] }) });
|
|
753
|
+
}
|
|
754
|
+
function AccessDenied({
|
|
755
|
+
token,
|
|
756
|
+
id,
|
|
757
|
+
readError,
|
|
758
|
+
fallbackHref,
|
|
759
|
+
fallbackLabel,
|
|
760
|
+
onRetry,
|
|
761
|
+
suggestions,
|
|
762
|
+
requestability,
|
|
763
|
+
reason,
|
|
764
|
+
headline: headline2,
|
|
765
|
+
footer
|
|
766
|
+
}) {
|
|
767
|
+
const { context, isLoading, refresh } = useAccessGate(token, id, {
|
|
768
|
+
readError
|
|
769
|
+
});
|
|
770
|
+
if (isLoading || !context) {
|
|
771
|
+
return /* @__PURE__ */ jsx4("div", { className: "flex h-full min-h-64 items-center justify-center p-6", children: /* @__PURE__ */ jsx4("div", { className: "h-8 w-8 animate-pulse rounded-full bg-muted" }) });
|
|
772
|
+
}
|
|
773
|
+
return /* @__PURE__ */ jsx4(
|
|
774
|
+
AccessDeniedView,
|
|
775
|
+
{
|
|
776
|
+
context,
|
|
777
|
+
id,
|
|
778
|
+
fallbackHref,
|
|
779
|
+
fallbackLabel,
|
|
780
|
+
onRetry,
|
|
781
|
+
onChanged: refresh,
|
|
782
|
+
suggestions,
|
|
783
|
+
requestability,
|
|
784
|
+
reason,
|
|
785
|
+
headline: headline2,
|
|
786
|
+
footer
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/access/react/AccessGate.tsx
|
|
792
|
+
import { classifyDataError as classifyDataError2 } from "@ai-matrx/data/access";
|
|
793
|
+
import { readServerRefusal } from "@ai-matrx/data/access";
|
|
794
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
795
|
+
function AccessGate({
|
|
796
|
+
token,
|
|
797
|
+
id,
|
|
798
|
+
error,
|
|
799
|
+
onRetry,
|
|
800
|
+
fallbackHref,
|
|
801
|
+
fallbackLabel,
|
|
802
|
+
relatedReads = [],
|
|
803
|
+
renderFault,
|
|
804
|
+
suggestions,
|
|
805
|
+
requestability,
|
|
806
|
+
reason,
|
|
807
|
+
headline: headline2,
|
|
808
|
+
footer
|
|
809
|
+
}) {
|
|
810
|
+
if (error && classifyDataError2(error) === "fault" && renderFault) {
|
|
811
|
+
return /* @__PURE__ */ jsx5(Fragment2, { children: renderFault(error) });
|
|
812
|
+
}
|
|
813
|
+
const refusal = readServerRefusal(error);
|
|
814
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
815
|
+
relatedReads.map((read) => /* @__PURE__ */ jsx5(
|
|
816
|
+
AccessGateCaptureResolver,
|
|
817
|
+
{
|
|
818
|
+
token: read.token,
|
|
819
|
+
id: read.id,
|
|
820
|
+
error: read.error
|
|
821
|
+
},
|
|
822
|
+
`${read.token}:${read.id}`
|
|
823
|
+
)),
|
|
824
|
+
/* @__PURE__ */ jsx5(
|
|
825
|
+
AccessDenied,
|
|
826
|
+
{
|
|
827
|
+
token,
|
|
828
|
+
id,
|
|
829
|
+
readError: error,
|
|
830
|
+
onRetry: refusal ? void 0 : onRetry,
|
|
831
|
+
fallbackHref,
|
|
832
|
+
fallbackLabel,
|
|
833
|
+
suggestions,
|
|
834
|
+
requestability,
|
|
835
|
+
reason: refusal?.message ?? reason,
|
|
836
|
+
headline: headline2,
|
|
837
|
+
footer: refusal?.code ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
838
|
+
footer,
|
|
839
|
+
/* @__PURE__ */ jsx5(ServerRefusalCode, { code: refusal.code, issues: refusal.issues })
|
|
840
|
+
] }) : footer
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
] });
|
|
844
|
+
}
|
|
845
|
+
function AccessGateCaptureResolver({
|
|
846
|
+
token,
|
|
847
|
+
id,
|
|
848
|
+
error
|
|
849
|
+
}) {
|
|
850
|
+
useAccessGate(token, id, { readError: error });
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
function ServerRefusalCode({
|
|
854
|
+
code,
|
|
855
|
+
issues
|
|
856
|
+
}) {
|
|
857
|
+
return /* @__PURE__ */ jsxs5("details", { className: "text-[11px] text-muted-foreground", children: [
|
|
858
|
+
/* @__PURE__ */ jsx5("summary", { className: "cursor-pointer select-none", children: "What the server said" }),
|
|
859
|
+
/* @__PURE__ */ jsx5("p", { className: "mt-1 font-mono", children: code }),
|
|
860
|
+
issues.length > 0 ? /* @__PURE__ */ jsx5("ul", { className: "mt-1 list-disc pl-4", children: issues.map((issue) => /* @__PURE__ */ jsx5("li", { children: issue }, issue)) }) : null
|
|
861
|
+
] });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/access/react/SlugAccessGate.tsx
|
|
865
|
+
import { useEffect as useEffect2, useState as useState5 } from "react";
|
|
866
|
+
import { ArrowLeft as ArrowLeft2, Link2Off } from "lucide-react";
|
|
867
|
+
import { isUuidShape } from "@ai-matrx/kit/uuid";
|
|
868
|
+
import { Button as Button4 } from "@ai-matrx/design-system/controls";
|
|
869
|
+
import {
|
|
870
|
+
fetchAccessDeniedContext as fetchAccessDeniedContext2,
|
|
871
|
+
resolveAccessGateSlug
|
|
872
|
+
} from "@ai-matrx/data/access";
|
|
873
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
874
|
+
function SlugAccessGate({
|
|
875
|
+
tokens,
|
|
876
|
+
slug,
|
|
877
|
+
noun,
|
|
878
|
+
fallbackHref,
|
|
879
|
+
fallbackLabel,
|
|
880
|
+
publishFiltered = false
|
|
881
|
+
}) {
|
|
882
|
+
const tokenKey = tokens.join("|");
|
|
883
|
+
const [resolved, setResolved] = useState5(null);
|
|
884
|
+
const key = `${tokenKey}::${slug}`;
|
|
885
|
+
useEffect2(() => {
|
|
886
|
+
let active = true;
|
|
887
|
+
const list = tokenKey ? tokenKey.split("|") : [];
|
|
888
|
+
void (async () => {
|
|
889
|
+
for (const token of list) {
|
|
890
|
+
const id = slug ? await resolveAccessGateSlug(token, slug) : null;
|
|
891
|
+
if (!active) return;
|
|
892
|
+
if (id) {
|
|
893
|
+
const readable = publishFiltered ? (await fetchAccessDeniedContext2(token, id, "fault")).status === "ok" : false;
|
|
894
|
+
if (active) setResolved({ key, token, id, readable });
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (active && list.length && isUuidShape(slug)) {
|
|
899
|
+
setResolved({ key, token: list[0] ?? null, id: slug, readable: false });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (active) setResolved({ key, token: null, id: null, readable: false });
|
|
903
|
+
})();
|
|
904
|
+
return () => {
|
|
905
|
+
active = false;
|
|
906
|
+
};
|
|
907
|
+
}, [key, tokenKey, slug, publishFiltered]);
|
|
908
|
+
if (!resolved || resolved.key !== key) return null;
|
|
909
|
+
if (resolved.token && resolved.id && resolved.readable) {
|
|
910
|
+
return /* @__PURE__ */ jsx6(
|
|
911
|
+
Notice,
|
|
912
|
+
{
|
|
913
|
+
title: `This ${noun} isn't published`,
|
|
914
|
+
body: `It exists and you can open it, but it has no public page until it's published.`,
|
|
915
|
+
fallbackHref,
|
|
916
|
+
fallbackLabel
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (resolved.token && resolved.id) {
|
|
921
|
+
return /* @__PURE__ */ jsx6(
|
|
922
|
+
AccessGate,
|
|
923
|
+
{
|
|
924
|
+
token: resolved.token,
|
|
925
|
+
id: resolved.id,
|
|
926
|
+
fallbackHref,
|
|
927
|
+
fallbackLabel
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
return /* @__PURE__ */ jsx6(
|
|
932
|
+
Notice,
|
|
933
|
+
{
|
|
934
|
+
title: `This address doesn't match ${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun} you can open`,
|
|
935
|
+
body: "Check the link, or sign in with the account it was shared with.",
|
|
936
|
+
fallbackHref,
|
|
937
|
+
fallbackLabel
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
function Notice({
|
|
942
|
+
title,
|
|
943
|
+
body,
|
|
944
|
+
fallbackHref,
|
|
945
|
+
fallbackLabel
|
|
946
|
+
}) {
|
|
947
|
+
return /* @__PURE__ */ jsx6("div", { className: "flex h-full min-h-64 w-full items-center justify-center p-6", children: /* @__PURE__ */ jsxs6("div", { className: "w-full max-w-lg", children: [
|
|
948
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-start gap-4", children: [
|
|
949
|
+
/* @__PURE__ */ jsx6("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx6(Link2Off, { className: "h-6 w-6 text-muted-foreground", "aria-hidden": true }) }),
|
|
950
|
+
/* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
|
|
951
|
+
/* @__PURE__ */ jsx6("h1", { className: "text-lg font-semibold text-foreground", children: title }),
|
|
952
|
+
/* @__PURE__ */ jsx6("p", { className: "mt-2 text-sm text-muted-foreground", children: body })
|
|
953
|
+
] })
|
|
954
|
+
] }),
|
|
955
|
+
/* @__PURE__ */ jsx6("div", { className: "mt-5", children: /* @__PURE__ */ jsx6(Button4, { asChild: true, variant: "outline", children: /* @__PURE__ */ jsxs6(AccessLink, { href: fallbackHref, children: [
|
|
956
|
+
/* @__PURE__ */ jsx6(ArrowLeft2, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
957
|
+
fallbackLabel
|
|
958
|
+
] }) }) })
|
|
959
|
+
] }) });
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/access/react/SettingAccessGate.tsx
|
|
963
|
+
import { useState as useState6 } from "react";
|
|
964
|
+
import { Check as Check2, Loader2 as Loader22, Lock as Lock2, Send as Send2 } from "lucide-react";
|
|
965
|
+
import { Button as Button5 } from "@ai-matrx/design-system/controls";
|
|
966
|
+
import { createSettingAccessRequest } from "@ai-matrx/data/access";
|
|
967
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
968
|
+
function SettingAccessGate({
|
|
969
|
+
canManage,
|
|
970
|
+
organizationId,
|
|
971
|
+
organizationSlugOrId,
|
|
972
|
+
settingKey,
|
|
973
|
+
settingLabel,
|
|
974
|
+
actionKey,
|
|
975
|
+
actionPayload,
|
|
976
|
+
defaultMessage,
|
|
977
|
+
requestReady = true,
|
|
978
|
+
children
|
|
979
|
+
}) {
|
|
980
|
+
const ui = useAccessUi();
|
|
981
|
+
const currentUserId = ui.useCurrentUser().id;
|
|
982
|
+
const ProTextarea = ui.Textarea;
|
|
983
|
+
const toast = ui.toast;
|
|
984
|
+
const [customMessage, setCustomMessage] = useState6(null);
|
|
985
|
+
const [busy, setBusy] = useState6(false);
|
|
986
|
+
const [pending, setPending] = useState6(false);
|
|
987
|
+
const message = customMessage ?? defaultMessage;
|
|
988
|
+
if (canManage) return /* @__PURE__ */ jsx7(Fragment3, { children });
|
|
989
|
+
async function send() {
|
|
990
|
+
setBusy(true);
|
|
991
|
+
try {
|
|
992
|
+
const result = await createSettingAccessRequest({
|
|
993
|
+
organizationId,
|
|
994
|
+
settingKey,
|
|
995
|
+
settingLabel,
|
|
996
|
+
href: ui.settingDoorHref({
|
|
997
|
+
scope: "organization",
|
|
998
|
+
organizationSlugOrId,
|
|
999
|
+
controlId: settingKey
|
|
1000
|
+
}),
|
|
1001
|
+
actionKey,
|
|
1002
|
+
actionPayload,
|
|
1003
|
+
message,
|
|
1004
|
+
currentUserId
|
|
1005
|
+
});
|
|
1006
|
+
setPending(true);
|
|
1007
|
+
if (result.already) {
|
|
1008
|
+
toast.success("Your request is already waiting for an admin.");
|
|
1009
|
+
} else if (result.delivered === 0) {
|
|
1010
|
+
toast.warning(
|
|
1011
|
+
"Your request is saved, but we couldn't message an admin just now."
|
|
1012
|
+
);
|
|
1013
|
+
} else {
|
|
1014
|
+
toast.success("Request sent to your organization admins.");
|
|
1015
|
+
}
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
toast.error(
|
|
1018
|
+
error instanceof Error ? error.message : "We couldn't send that request."
|
|
1019
|
+
);
|
|
1020
|
+
} finally {
|
|
1021
|
+
setBusy(false);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return /* @__PURE__ */ jsxs7("div", { className: "rounded-lg border border-border bg-muted/30 p-4", children: [
|
|
1025
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-start gap-3", children: [
|
|
1026
|
+
/* @__PURE__ */ jsx7("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-background", children: pending ? /* @__PURE__ */ jsx7(Check2, { className: "h-4 w-4 text-primary", "aria-hidden": true }) : /* @__PURE__ */ jsx7(Lock2, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": true }) }),
|
|
1027
|
+
/* @__PURE__ */ jsxs7("div", { className: "min-w-0 flex-1", children: [
|
|
1028
|
+
/* @__PURE__ */ jsx7("p", { className: "text-sm font-medium text-foreground", children: pending ? "Your request is waiting" : "An organization admin controls this setting" }),
|
|
1029
|
+
/* @__PURE__ */ jsx7("p", { className: "mt-1 text-xs leading-5 text-muted-foreground", children: pending ? "It is saved in Access requests, even if the direct message could not be delivered." : "Tell them what you need. The request is filled with this setting and includes an action they can complete inside the message." })
|
|
1030
|
+
] })
|
|
1031
|
+
] }),
|
|
1032
|
+
!pending ? /* @__PURE__ */ jsxs7("div", { className: "mt-3 space-y-2", children: [
|
|
1033
|
+
/* @__PURE__ */ jsx7(
|
|
1034
|
+
ProTextarea,
|
|
1035
|
+
{
|
|
1036
|
+
value: message,
|
|
1037
|
+
onChange: (event) => setCustomMessage(event.target.value),
|
|
1038
|
+
rows: 3,
|
|
1039
|
+
maxLength: 1e3,
|
|
1040
|
+
"aria-label": "Message to organization admins"
|
|
1041
|
+
}
|
|
1042
|
+
),
|
|
1043
|
+
/* @__PURE__ */ jsx7(
|
|
1044
|
+
Button5,
|
|
1045
|
+
{
|
|
1046
|
+
icon: busy ? /* @__PURE__ */ jsx7(Loader22, { className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx7(Send2, { "aria-hidden": true }),
|
|
1047
|
+
variant: "primary",
|
|
1048
|
+
disabled: busy || !message.trim() || !requestReady,
|
|
1049
|
+
onClick: () => void send(),
|
|
1050
|
+
children: "Request this change"
|
|
1051
|
+
}
|
|
1052
|
+
)
|
|
1053
|
+
] }) : null
|
|
1054
|
+
] });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/access/react/ForbiddenSurface.tsx
|
|
1058
|
+
import { ArrowLeft as ArrowLeft3, Lock as Lock3, LogIn as LogIn2, RotateCw } from "lucide-react";
|
|
1059
|
+
import { Button as Button6 } from "@ai-matrx/design-system/controls";
|
|
1060
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1061
|
+
function ForbiddenSurfaceView({
|
|
1062
|
+
state,
|
|
1063
|
+
isAuthenticated,
|
|
1064
|
+
pathname,
|
|
1065
|
+
search = "",
|
|
1066
|
+
signInHref,
|
|
1067
|
+
homeHref = "/dashboard"
|
|
1068
|
+
}) {
|
|
1069
|
+
const unverified = state === "unverified";
|
|
1070
|
+
return /* @__PURE__ */ jsx8("div", { className: "flex h-full min-h-64 w-full items-center justify-center p-6", children: /* @__PURE__ */ jsxs8("div", { className: "w-full max-w-lg", children: [
|
|
1071
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-start gap-4", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx8("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx8(Lock3, { className: "h-6 w-6 text-muted-foreground", "aria-hidden": true }) }),
|
|
1073
|
+
/* @__PURE__ */ jsxs8("div", { className: "min-w-0 flex-1", children: [
|
|
1074
|
+
/* @__PURE__ */ jsx8("h1", { className: "text-lg font-semibold text-foreground", children: unverified ? "We could not confirm who you are just now" : isAuthenticated ? "You don't have access to this page" : "Sign in to open this page" }),
|
|
1075
|
+
/* @__PURE__ */ jsx8("p", { className: "mt-2 text-sm text-muted-foreground", children: unverified ? "You have not been signed out. This usually clears in a few seconds \u2014 open the page again." : isAuthenticated ? "Your account can't open it. If someone shared it with you, ask them to share it with this account." : "We can't tell you anything about it until we know who you are." })
|
|
1076
|
+
] })
|
|
1077
|
+
] }),
|
|
1078
|
+
/* @__PURE__ */ jsxs8("div", { className: "mt-5 flex flex-wrap items-center gap-2", children: [
|
|
1079
|
+
unverified ? /* @__PURE__ */ jsx8(Button6, { variant: "primary", asChild: true, children: /* @__PURE__ */ jsxs8(AccessLink, { href: `${pathname}${search}`, children: [
|
|
1080
|
+
/* @__PURE__ */ jsx8(RotateCw, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
1081
|
+
"Open it again"
|
|
1082
|
+
] }) }) : isAuthenticated ? null : /* @__PURE__ */ jsx8(Button6, { variant: "primary", asChild: true, children: /* @__PURE__ */ jsxs8(AccessLink, { href: signInHref, children: [
|
|
1083
|
+
/* @__PURE__ */ jsx8(LogIn2, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
1084
|
+
"Sign in"
|
|
1085
|
+
] }) }),
|
|
1086
|
+
/* @__PURE__ */ jsx8(Button6, { asChild: true, variant: "outline", children: /* @__PURE__ */ jsxs8(AccessLink, { href: homeHref, children: [
|
|
1087
|
+
/* @__PURE__ */ jsx8(ArrowLeft3, { className: "mr-1.5 h-4 w-4", "aria-hidden": true }),
|
|
1088
|
+
"Back to what you can see"
|
|
1089
|
+
] }) })
|
|
1090
|
+
] })
|
|
1091
|
+
] }) });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/access/react/RequestAccess.tsx
|
|
1095
|
+
import { useState as useState8 } from "react";
|
|
1096
|
+
import { Check as Check3, KeyRound as KeyRound2, Loader2 as Loader23, Send as Send3 } from "lucide-react";
|
|
1097
|
+
import { Button as Button7 } from "@ai-matrx/design-system/controls";
|
|
1098
|
+
import {
|
|
1099
|
+
Dialog,
|
|
1100
|
+
DialogContent,
|
|
1101
|
+
DialogDescription,
|
|
1102
|
+
DialogFooter,
|
|
1103
|
+
DialogHeader,
|
|
1104
|
+
DialogTitle
|
|
1105
|
+
} from "@ai-matrx/design-system";
|
|
1106
|
+
import { cn } from "@ai-matrx/design-system";
|
|
1107
|
+
|
|
1108
|
+
// src/access/react/useRequestAccess.ts
|
|
1109
|
+
import { useState as useState7 } from "react";
|
|
1110
|
+
import { requireAccessEnvField } from "@ai-matrx/data/access";
|
|
1111
|
+
import { createSettingAccessRequest as createSettingAccessRequest2 } from "@ai-matrx/data/access";
|
|
1112
|
+
import {
|
|
1113
|
+
destinationLabel,
|
|
1114
|
+
feedbackMetadata,
|
|
1115
|
+
orgActionPayload,
|
|
1116
|
+
orgManageHref,
|
|
1117
|
+
orgSettingKey,
|
|
1118
|
+
requestBody,
|
|
1119
|
+
requestTitle,
|
|
1120
|
+
resolveRequestOwner
|
|
1121
|
+
} from "@ai-matrx/data/access";
|
|
1122
|
+
function useRequestAccess(target) {
|
|
1123
|
+
const ui = useAccessUi();
|
|
1124
|
+
const { id: userId, displayName, email } = ui.useCurrentUser();
|
|
1125
|
+
const owner = resolveRequestOwner(target.owner);
|
|
1126
|
+
const [sending, setSending] = useState7(false);
|
|
1127
|
+
const [sent, setSent] = useState7(false);
|
|
1128
|
+
const context = () => ({
|
|
1129
|
+
pageUrl: typeof window === "undefined" ? "" : window.location.href,
|
|
1130
|
+
requesterName: displayName,
|
|
1131
|
+
requesterEmail: email
|
|
1132
|
+
});
|
|
1133
|
+
async function send(note) {
|
|
1134
|
+
setSending(true);
|
|
1135
|
+
try {
|
|
1136
|
+
const ctx = context();
|
|
1137
|
+
if (owner.kind === "system") {
|
|
1138
|
+
const organizationId = await requireAccessEnvField("ensureOrgId", "useRequestAccess")(null);
|
|
1139
|
+
const result2 = await ui.reportFeedback({
|
|
1140
|
+
// The feedback vocabulary's access-request type; `metadata` carries
|
|
1141
|
+
// the structured target (FEATURE.md, RequestAccess).
|
|
1142
|
+
feedback_type: "request",
|
|
1143
|
+
route: typeof window === "undefined" ? "" : window.location.pathname,
|
|
1144
|
+
organization_id: organizationId,
|
|
1145
|
+
description: `Access request: ${requestTitle(target)}
|
|
1146
|
+
|
|
1147
|
+
${requestBody(target, ctx, note)}`,
|
|
1148
|
+
metadata: feedbackMetadata(target, ctx)
|
|
1149
|
+
});
|
|
1150
|
+
if (!result2.success) {
|
|
1151
|
+
throw new Error(result2.error ?? "We couldn't send that request.");
|
|
1152
|
+
}
|
|
1153
|
+
setSent(true);
|
|
1154
|
+
return {
|
|
1155
|
+
already: false,
|
|
1156
|
+
undelivered: false,
|
|
1157
|
+
message: `Request sent to ${destinationLabel(owner)}.`
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
const result = await createSettingAccessRequest2({
|
|
1161
|
+
organizationId: owner.organizationId,
|
|
1162
|
+
settingKey: orgSettingKey(target),
|
|
1163
|
+
settingLabel: requestTitle(target),
|
|
1164
|
+
href: orgManageHref(target, owner),
|
|
1165
|
+
actionKey: ui.manualRequestActionKey,
|
|
1166
|
+
actionPayload: orgActionPayload(target, owner),
|
|
1167
|
+
message: requestBody(target, ctx, note),
|
|
1168
|
+
currentUserId: userId
|
|
1169
|
+
});
|
|
1170
|
+
setSent(true);
|
|
1171
|
+
if (result.already) {
|
|
1172
|
+
return {
|
|
1173
|
+
already: true,
|
|
1174
|
+
undelivered: false,
|
|
1175
|
+
message: "Your request is already waiting for an answer."
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
if (result.delivered === 0) {
|
|
1179
|
+
return {
|
|
1180
|
+
already: false,
|
|
1181
|
+
undelivered: true,
|
|
1182
|
+
message: "Your request is saved in Access requests, but we couldn't message an admin just now."
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
already: false,
|
|
1187
|
+
undelivered: false,
|
|
1188
|
+
message: `Request sent to ${destinationLabel(owner)}.`
|
|
1189
|
+
};
|
|
1190
|
+
} finally {
|
|
1191
|
+
setSending(false);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
owner,
|
|
1196
|
+
destination: destinationLabel(owner),
|
|
1197
|
+
context,
|
|
1198
|
+
send,
|
|
1199
|
+
sending,
|
|
1200
|
+
sent
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/access/react/RequestAccess.tsx
|
|
1205
|
+
import {
|
|
1206
|
+
contextLines
|
|
1207
|
+
} from "@ai-matrx/data/access";
|
|
1208
|
+
import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1209
|
+
function RequestAccess({
|
|
1210
|
+
target,
|
|
1211
|
+
variant = "inline",
|
|
1212
|
+
reason,
|
|
1213
|
+
className
|
|
1214
|
+
}) {
|
|
1215
|
+
const [open, setOpen] = useState8(false);
|
|
1216
|
+
const request = useRequestAccess(target);
|
|
1217
|
+
const label = `Ask for access to ${target.action.toLowerCase()}`;
|
|
1218
|
+
const why = reason ?? (request.owner.kind === "system" ? "Only the platform team can change this." : `Only ${request.destination} can change this.`);
|
|
1219
|
+
return /* @__PURE__ */ jsxs9(Fragment4, { children: [
|
|
1220
|
+
variant === "icon" ? /* @__PURE__ */ jsx9(
|
|
1221
|
+
"button",
|
|
1222
|
+
{
|
|
1223
|
+
type: "button",
|
|
1224
|
+
"aria-label": label,
|
|
1225
|
+
title: label,
|
|
1226
|
+
onClick: () => setOpen(true),
|
|
1227
|
+
className: cn(
|
|
1228
|
+
"inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1229
|
+
className
|
|
1230
|
+
),
|
|
1231
|
+
children: request.sent ? /* @__PURE__ */ jsx9(Check3, { className: "size-3.5 text-primary", "aria-hidden": "true" }) : /* @__PURE__ */ jsx9(KeyRound2, { className: "size-3.5", "aria-hidden": "true" })
|
|
1232
|
+
}
|
|
1233
|
+
) : /* @__PURE__ */ jsxs9(
|
|
1234
|
+
"div",
|
|
1235
|
+
{
|
|
1236
|
+
className: cn(
|
|
1237
|
+
"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground",
|
|
1238
|
+
className
|
|
1239
|
+
),
|
|
1240
|
+
children: [
|
|
1241
|
+
/* @__PURE__ */ jsx9("span", { children: request.sent ? "Request sent." : why }),
|
|
1242
|
+
request.sent ? null : /* @__PURE__ */ jsx9(
|
|
1243
|
+
Button7,
|
|
1244
|
+
{
|
|
1245
|
+
icon: /* @__PURE__ */ jsx9(KeyRound2, { "aria-hidden": "true" }),
|
|
1246
|
+
variant: "outline",
|
|
1247
|
+
onClick: () => setOpen(true),
|
|
1248
|
+
children: "Ask for access"
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1251
|
+
]
|
|
1252
|
+
}
|
|
1253
|
+
),
|
|
1254
|
+
open ? /* @__PURE__ */ jsx9(
|
|
1255
|
+
RequestAccessDialog,
|
|
1256
|
+
{
|
|
1257
|
+
target,
|
|
1258
|
+
request,
|
|
1259
|
+
onOpenChange: setOpen
|
|
1260
|
+
}
|
|
1261
|
+
) : null
|
|
1262
|
+
] });
|
|
1263
|
+
}
|
|
1264
|
+
function RequestAccessDialog({
|
|
1265
|
+
target,
|
|
1266
|
+
request,
|
|
1267
|
+
onOpenChange
|
|
1268
|
+
}) {
|
|
1269
|
+
const ui = useAccessUi();
|
|
1270
|
+
const { toast } = ui;
|
|
1271
|
+
const ProTextarea = ui.Textarea;
|
|
1272
|
+
const [note, setNote] = useState8("");
|
|
1273
|
+
const lines = contextLines(target, request.context());
|
|
1274
|
+
async function send() {
|
|
1275
|
+
try {
|
|
1276
|
+
const result = await request.send(note);
|
|
1277
|
+
if (result.undelivered) toast.warning(result.message);
|
|
1278
|
+
else toast.success(result.message);
|
|
1279
|
+
onOpenChange(false);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (ui.presentOrganizationRefusal(error, { act: "sent", subject: "Your request" })) return;
|
|
1282
|
+
toast.error(
|
|
1283
|
+
error instanceof Error ? error.message : "We couldn't send that request."
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return /* @__PURE__ */ jsx9(Dialog, { open: true, onOpenChange, children: /* @__PURE__ */ jsxs9(DialogContent, { className: "matrx-touch-targets sm:max-w-md", children: [
|
|
1288
|
+
/* @__PURE__ */ jsxs9(DialogHeader, { children: [
|
|
1289
|
+
/* @__PURE__ */ jsx9(DialogTitle, { children: "Ask for access" }),
|
|
1290
|
+
/* @__PURE__ */ jsxs9(DialogDescription, { children: [
|
|
1291
|
+
"Goes to ",
|
|
1292
|
+
request.destination,
|
|
1293
|
+
"."
|
|
1294
|
+
] })
|
|
1295
|
+
] }),
|
|
1296
|
+
/* @__PURE__ */ jsx9("dl", { className: "grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-md border border-border bg-muted/30 p-3 text-xs", children: lines.map(([label, value]) => /* @__PURE__ */ jsxs9("div", { className: "contents", children: [
|
|
1297
|
+
/* @__PURE__ */ jsx9("dt", { className: "text-muted-foreground", children: label }),
|
|
1298
|
+
/* @__PURE__ */ jsx9("dd", { className: "min-w-0 text-foreground [overflow-wrap:anywhere]", children: value })
|
|
1299
|
+
] }, label)) }),
|
|
1300
|
+
/* @__PURE__ */ jsx9(
|
|
1301
|
+
ProTextarea,
|
|
1302
|
+
{
|
|
1303
|
+
className: "min-h-20 text-base md:text-sm",
|
|
1304
|
+
value: note,
|
|
1305
|
+
maxLength: 1e3,
|
|
1306
|
+
autoFocus: true,
|
|
1307
|
+
placeholder: "What do you need, and why? (optional)",
|
|
1308
|
+
"aria-label": "Note",
|
|
1309
|
+
onChange: (event) => setNote(event.target.value)
|
|
1310
|
+
}
|
|
1311
|
+
),
|
|
1312
|
+
/* @__PURE__ */ jsxs9(DialogFooter, { children: [
|
|
1313
|
+
/* @__PURE__ */ jsx9(Button7, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
|
|
1314
|
+
/* @__PURE__ */ jsx9(Button7, { icon: request.sending ? /* @__PURE__ */ jsx9(Loader23, { className: "animate-spin", "aria-hidden": "true" }) : /* @__PURE__ */ jsx9(Send3, { "aria-hidden": "true" }), variant: "primary", disabled: request.sending, onClick: () => void send(), children: "Send request" })
|
|
1315
|
+
] })
|
|
1316
|
+
] }) });
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/access/react/UnresolvedEntityRef.tsx
|
|
1320
|
+
import { useState as useState9 } from "react";
|
|
1321
|
+
import {
|
|
1322
|
+
Ban,
|
|
1323
|
+
HelpCircle,
|
|
1324
|
+
Loader2 as Loader24,
|
|
1325
|
+
LogIn as LogIn3,
|
|
1326
|
+
RefreshCw as RefreshCw2,
|
|
1327
|
+
Trash2 as Trash22,
|
|
1328
|
+
TriangleAlert as TriangleAlert2
|
|
1329
|
+
} from "lucide-react";
|
|
1330
|
+
import {
|
|
1331
|
+
Popover,
|
|
1332
|
+
PopoverContent,
|
|
1333
|
+
PopoverTrigger
|
|
1334
|
+
} from "@ai-matrx/design-system";
|
|
1335
|
+
import { Button as Button8 } from "@ai-matrx/design-system/controls";
|
|
1336
|
+
import { cn as cn2 } from "@ai-matrx/design-system";
|
|
1337
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1338
|
+
function chipLabel(context) {
|
|
1339
|
+
const kind = context.entity.label.toLowerCase();
|
|
1340
|
+
switch (context.status) {
|
|
1341
|
+
// The state has to be readable WITHOUT opening the popover — a bare name in
|
|
1342
|
+
// a roster reads as an ordinary row, which is the lie all over again.
|
|
1343
|
+
case "denied":
|
|
1344
|
+
return context.entity.title ? `${context.entity.title} \u2014 no access` : `A ${kind} you can't open`;
|
|
1345
|
+
case "deleted":
|
|
1346
|
+
return context.entity.title ? `${context.entity.title} \u2014 deleted` : `Deleted ${kind}`;
|
|
1347
|
+
case "missing":
|
|
1348
|
+
return `This ${kind} no longer exists`;
|
|
1349
|
+
case "anonymous":
|
|
1350
|
+
return "Sign in to see this";
|
|
1351
|
+
case "ok":
|
|
1352
|
+
return context.entity.title ?? `This ${kind} didn't load`;
|
|
1353
|
+
default:
|
|
1354
|
+
return "Couldn't check this record";
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function explanation2(context) {
|
|
1358
|
+
const kind = context.entity.label.toLowerCase();
|
|
1359
|
+
switch (context.status) {
|
|
1360
|
+
case "denied":
|
|
1361
|
+
return `This ${kind} exists, but it isn't shared with you. You can ask for access \u2014 the row stays in the list either way.`;
|
|
1362
|
+
case "deleted":
|
|
1363
|
+
return `This ${kind} was moved to the trash. Whoever deleted it can restore it; until then there is nothing here to open.`;
|
|
1364
|
+
case "missing":
|
|
1365
|
+
return `There is no ${kind} with this id. The reference is stale.`;
|
|
1366
|
+
case "anonymous":
|
|
1367
|
+
return "Your session ended, so we can't tell you anything about this record. Sign in and it will load.";
|
|
1368
|
+
case "ok":
|
|
1369
|
+
return `You DO have access to this ${kind} \u2014 the read failed for some other reason. Trying again should work.`;
|
|
1370
|
+
default:
|
|
1371
|
+
return "We couldn't work out why this didn't load. That's a fault on our side, not a permission problem.";
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function StatusIcon2({ status }) {
|
|
1375
|
+
const className = "h-3.5 w-3.5 shrink-0";
|
|
1376
|
+
switch (status) {
|
|
1377
|
+
case "denied":
|
|
1378
|
+
return /* @__PURE__ */ jsx10(Ban, { className, "aria-hidden": true });
|
|
1379
|
+
case "deleted":
|
|
1380
|
+
return /* @__PURE__ */ jsx10(Trash22, { className, "aria-hidden": true });
|
|
1381
|
+
case "missing":
|
|
1382
|
+
return /* @__PURE__ */ jsx10(HelpCircle, { className, "aria-hidden": true });
|
|
1383
|
+
case "anonymous":
|
|
1384
|
+
return /* @__PURE__ */ jsx10(LogIn3, { className, "aria-hidden": true });
|
|
1385
|
+
case "ok":
|
|
1386
|
+
return /* @__PURE__ */ jsx10(RefreshCw2, { className, "aria-hidden": true });
|
|
1387
|
+
default:
|
|
1388
|
+
return /* @__PURE__ */ jsx10(TriangleAlert2, { className, "aria-hidden": true });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function UnresolvedEntityRef({
|
|
1392
|
+
id,
|
|
1393
|
+
context,
|
|
1394
|
+
onChanged,
|
|
1395
|
+
repairAction,
|
|
1396
|
+
className
|
|
1397
|
+
}) {
|
|
1398
|
+
const [open, setOpen] = useState9(false);
|
|
1399
|
+
const signInHref = useAccessUi().useLoginHref();
|
|
1400
|
+
if (!context) {
|
|
1401
|
+
return /* @__PURE__ */ jsxs10(
|
|
1402
|
+
"span",
|
|
1403
|
+
{
|
|
1404
|
+
className: cn2(
|
|
1405
|
+
"inline-flex items-center gap-1.5 text-sm text-muted-foreground",
|
|
1406
|
+
className
|
|
1407
|
+
),
|
|
1408
|
+
children: [
|
|
1409
|
+
/* @__PURE__ */ jsx10(Loader24, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true }),
|
|
1410
|
+
"Checking\u2026"
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
const label = chipLabel(context);
|
|
1416
|
+
const tone = context.status === "error" || context.status === "missing" ? "text-amber-700 dark:text-amber-400" : "text-muted-foreground";
|
|
1417
|
+
const showOrg = Boolean(context.organization);
|
|
1418
|
+
return /* @__PURE__ */ jsxs10(Popover, { open, onOpenChange: setOpen, children: [
|
|
1419
|
+
/* @__PURE__ */ jsx10(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs10(
|
|
1420
|
+
"button",
|
|
1421
|
+
{
|
|
1422
|
+
type: "button",
|
|
1423
|
+
onClick: (e) => e.stopPropagation(),
|
|
1424
|
+
className: cn2(
|
|
1425
|
+
"inline-flex min-w-0 items-center gap-1.5 rounded-md border border-dashed border-border px-1.5 py-0.5 text-sm italic transition-colors hover:bg-muted",
|
|
1426
|
+
tone,
|
|
1427
|
+
className
|
|
1428
|
+
),
|
|
1429
|
+
title: "Why can't I see this?",
|
|
1430
|
+
children: [
|
|
1431
|
+
/* @__PURE__ */ jsx10(StatusIcon2, { status: context.status }),
|
|
1432
|
+
/* @__PURE__ */ jsx10("span", { className: "truncate", children: label })
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
) }),
|
|
1436
|
+
/* @__PURE__ */ jsxs10(
|
|
1437
|
+
PopoverContent,
|
|
1438
|
+
{
|
|
1439
|
+
sizing: "content",
|
|
1440
|
+
align: "start",
|
|
1441
|
+
className: "p-3",
|
|
1442
|
+
onClick: (e) => e.stopPropagation(),
|
|
1443
|
+
children: [
|
|
1444
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm font-medium text-foreground", children: context.entity.title ?? context.entity.label }),
|
|
1445
|
+
/* @__PURE__ */ jsx10("p", { className: "mt-1 text-xs text-muted-foreground", children: explanation2(context) }),
|
|
1446
|
+
context.owner || showOrg ? /* @__PURE__ */ jsxs10("dl", { className: "mt-2 space-y-0.5 rounded-md bg-muted/50 px-2 py-1.5 text-xs", children: [
|
|
1447
|
+
context.owner ? /* @__PURE__ */ jsxs10("div", { className: "flex gap-1.5", children: [
|
|
1448
|
+
/* @__PURE__ */ jsx10("dt", { className: "text-muted-foreground", children: "Owner" }),
|
|
1449
|
+
/* @__PURE__ */ jsx10("dd", { className: "min-w-0 truncate text-foreground", children: context.owner.creatorHandle ? /* @__PURE__ */ jsx10(
|
|
1450
|
+
AccessLink,
|
|
1451
|
+
{
|
|
1452
|
+
href: `/c/${context.owner.creatorHandle}`,
|
|
1453
|
+
className: "underline underline-offset-2",
|
|
1454
|
+
children: context.owner.displayName ?? "Someone else"
|
|
1455
|
+
}
|
|
1456
|
+
) : context.owner.displayName ?? "Someone else" })
|
|
1457
|
+
] }) : null,
|
|
1458
|
+
showOrg && context.organization ? /* @__PURE__ */ jsxs10("div", { className: "flex gap-1.5", children: [
|
|
1459
|
+
/* @__PURE__ */ jsx10("dt", { className: "text-muted-foreground", children: "Organization" }),
|
|
1460
|
+
/* @__PURE__ */ jsx10("dd", { className: "min-w-0 truncate text-foreground", children: context.organization.viewerIsMember ? /* @__PURE__ */ jsx10(
|
|
1461
|
+
AccessLink,
|
|
1462
|
+
{
|
|
1463
|
+
href: `/organizations/${context.organization.id}`,
|
|
1464
|
+
className: "underline underline-offset-2",
|
|
1465
|
+
children: context.organization.name ?? "An organization"
|
|
1466
|
+
}
|
|
1467
|
+
) : context.organization.name ?? "An organization" })
|
|
1468
|
+
] }) : null
|
|
1469
|
+
] }) : null,
|
|
1470
|
+
context.status === "denied" ? /* @__PURE__ */ jsx10("div", { className: "mt-2", children: /* @__PURE__ */ jsx10(
|
|
1471
|
+
RequestAccessPanel,
|
|
1472
|
+
{
|
|
1473
|
+
context,
|
|
1474
|
+
resourceId: id,
|
|
1475
|
+
onChanged: () => onChanged?.()
|
|
1476
|
+
}
|
|
1477
|
+
) }) : null,
|
|
1478
|
+
context.status === "anonymous" ? /* @__PURE__ */ jsx10(Button8, { variant: "primary", asChild: true, className: "mt-2 w-full", children: /* @__PURE__ */ jsxs10(AccessLink, { href: signInHref, children: [
|
|
1479
|
+
/* @__PURE__ */ jsx10(LogIn3, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": true }),
|
|
1480
|
+
"Sign in"
|
|
1481
|
+
] }) }) : null,
|
|
1482
|
+
(context.status === "ok" || context.status === "error") && onChanged ? /* @__PURE__ */ jsx10(
|
|
1483
|
+
Button8,
|
|
1484
|
+
{
|
|
1485
|
+
icon: /* @__PURE__ */ jsx10(RefreshCw2, { "aria-hidden": true }),
|
|
1486
|
+
variant: "primary",
|
|
1487
|
+
className: "mt-2 w-full",
|
|
1488
|
+
onClick: () => onChanged(),
|
|
1489
|
+
children: "Try again"
|
|
1490
|
+
}
|
|
1491
|
+
) : null,
|
|
1492
|
+
repairAction ? /* @__PURE__ */ jsx10("div", { className: "mt-2", children: repairAction }) : null
|
|
1493
|
+
]
|
|
1494
|
+
}
|
|
1495
|
+
)
|
|
1496
|
+
] });
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/access/react/ServerReadRecheck.tsx
|
|
1500
|
+
import { useEffect as useEffect3, useState as useState10 } from "react";
|
|
1501
|
+
import { getClaimsUser } from "@ai-matrx/auth/claims";
|
|
1502
|
+
import { getAccessEnv as getAccessEnv2 } from "@ai-matrx/data/access";
|
|
1503
|
+
import { decideServerReadRecheck } from "@ai-matrx/data/access";
|
|
1504
|
+
import { Fragment as Fragment5, jsx as jsx11 } from "react/jsx-runtime";
|
|
1505
|
+
var ROW_READERS = {
|
|
1506
|
+
conversation: async (supabase, id) => {
|
|
1507
|
+
const { data, error } = await supabase.schema("chat").from("conversation").select("id").eq("id", id).is("deleted_at", null).maybeSingle();
|
|
1508
|
+
return !error && Boolean(data);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
var refreshedOnce = /* @__PURE__ */ new Set();
|
|
1512
|
+
function ServerReadRecheck({
|
|
1513
|
+
token,
|
|
1514
|
+
id,
|
|
1515
|
+
children,
|
|
1516
|
+
pending
|
|
1517
|
+
}) {
|
|
1518
|
+
const router = useAccessUi().router;
|
|
1519
|
+
const key = `${token}:${id}`;
|
|
1520
|
+
const [showGate, setShowGate] = useState10(false);
|
|
1521
|
+
useEffect3(() => {
|
|
1522
|
+
let active = true;
|
|
1523
|
+
void (async () => {
|
|
1524
|
+
let signedIn = false;
|
|
1525
|
+
let rowReadable = false;
|
|
1526
|
+
try {
|
|
1527
|
+
const supabase = getAccessEnv2().getClient();
|
|
1528
|
+
const { data } = await getClaimsUser(supabase);
|
|
1529
|
+
signedIn = Boolean(data.user);
|
|
1530
|
+
if (signedIn) rowReadable = await ROW_READERS[token](supabase, id);
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
console.error(`[ServerReadRecheck] ${key}: browser re-read failed`, error);
|
|
1533
|
+
}
|
|
1534
|
+
if (!active) return;
|
|
1535
|
+
const verdict = decideServerReadRecheck({
|
|
1536
|
+
signedIn,
|
|
1537
|
+
rowReadable,
|
|
1538
|
+
alreadyRefreshed: refreshedOnce.has(key)
|
|
1539
|
+
});
|
|
1540
|
+
if (verdict === "refresh") {
|
|
1541
|
+
console.warn(
|
|
1542
|
+
`[ServerReadRecheck] ${key}: the server read came back empty but this browser can read it \u2014 re-rendering with the refreshed session`
|
|
1543
|
+
);
|
|
1544
|
+
refreshedOnce.add(key);
|
|
1545
|
+
router.refresh();
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
setShowGate(true);
|
|
1549
|
+
})();
|
|
1550
|
+
return () => {
|
|
1551
|
+
active = false;
|
|
1552
|
+
};
|
|
1553
|
+
}, [key, token, id, router]);
|
|
1554
|
+
return /* @__PURE__ */ jsx11(Fragment5, { children: showGate ? children : pending ?? null });
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/access/react/GovernedActionDialog.tsx
|
|
1558
|
+
import { useState as useState12 } from "react";
|
|
1559
|
+
import {
|
|
1560
|
+
Archive,
|
|
1561
|
+
Check as Check4,
|
|
1562
|
+
EyeOff,
|
|
1563
|
+
KeyRound as KeyRound3,
|
|
1564
|
+
Loader2 as Loader25,
|
|
1565
|
+
Send as Send4,
|
|
1566
|
+
ShieldCheck,
|
|
1567
|
+
Trash2 as Trash23
|
|
1568
|
+
} from "lucide-react";
|
|
1569
|
+
import { Button as Button9 } from "@ai-matrx/design-system/controls";
|
|
1570
|
+
import {
|
|
1571
|
+
Dialog as Dialog2,
|
|
1572
|
+
DialogContent as DialogContent2,
|
|
1573
|
+
DialogDescription as DialogDescription2,
|
|
1574
|
+
DialogFooter as DialogFooter2,
|
|
1575
|
+
DialogHeader as DialogHeader2,
|
|
1576
|
+
DialogTitle as DialogTitle2
|
|
1577
|
+
} from "@ai-matrx/design-system";
|
|
1578
|
+
import {
|
|
1579
|
+
createAccessRequest as createAccessRequest2,
|
|
1580
|
+
createDeleteRequest
|
|
1581
|
+
} from "@ai-matrx/data/access";
|
|
1582
|
+
|
|
1583
|
+
// src/access/react/useHiddenByMe.ts
|
|
1584
|
+
import { useEffect as useEffect4, useState as useState11, useSyncExternalStore } from "react";
|
|
1585
|
+
import {
|
|
1586
|
+
fetchRemovalRights,
|
|
1587
|
+
hideForMe,
|
|
1588
|
+
listHiddenItems,
|
|
1589
|
+
unhideForMe
|
|
1590
|
+
} from "@ai-matrx/data/access";
|
|
1591
|
+
import { getAccessEnv as getAccessEnv3 } from "@ai-matrx/data/access";
|
|
1592
|
+
var slots = /* @__PURE__ */ new Map();
|
|
1593
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
1594
|
+
var version = 0;
|
|
1595
|
+
var watchingAuth = false;
|
|
1596
|
+
function emit() {
|
|
1597
|
+
version += 1;
|
|
1598
|
+
listeners.forEach((listener) => listener());
|
|
1599
|
+
}
|
|
1600
|
+
function watchAuth() {
|
|
1601
|
+
if (watchingAuth || typeof window === "undefined") return;
|
|
1602
|
+
watchingAuth = true;
|
|
1603
|
+
try {
|
|
1604
|
+
getAccessEnv3().getClient().auth.onAuthStateChange((event) => {
|
|
1605
|
+
if (event === "SIGNED_IN" || event === "SIGNED_OUT" || event === "USER_UPDATED") {
|
|
1606
|
+
slots.clear();
|
|
1607
|
+
emit();
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function slotFor(token) {
|
|
1614
|
+
let slot = slots.get(token);
|
|
1615
|
+
if (!slot) {
|
|
1616
|
+
slot = { items: void 0, ids: void 0, loading: null, failed: false };
|
|
1617
|
+
slots.set(token, slot);
|
|
1618
|
+
}
|
|
1619
|
+
return slot;
|
|
1620
|
+
}
|
|
1621
|
+
function settle(slot, items, failed) {
|
|
1622
|
+
slot.items = items;
|
|
1623
|
+
slot.ids = items.map((item) => item.id);
|
|
1624
|
+
slot.failed = failed;
|
|
1625
|
+
}
|
|
1626
|
+
function refreshHiddenByMe(token) {
|
|
1627
|
+
watchAuth();
|
|
1628
|
+
const slot = slotFor(token);
|
|
1629
|
+
const loading = (async () => {
|
|
1630
|
+
try {
|
|
1631
|
+
const { data } = await getAccessEnv3().getClient().auth.getSession();
|
|
1632
|
+
if (!data.session) {
|
|
1633
|
+
settle(slot, [], true);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
settle(slot, await listHiddenItems(token), false);
|
|
1637
|
+
} catch {
|
|
1638
|
+
settle(slot, slot.items ?? [], true);
|
|
1639
|
+
}
|
|
1640
|
+
})().finally(() => {
|
|
1641
|
+
slot.loading = null;
|
|
1642
|
+
emit();
|
|
1643
|
+
});
|
|
1644
|
+
slot.loading = loading;
|
|
1645
|
+
return loading;
|
|
1646
|
+
}
|
|
1647
|
+
function useHiddenSlot(token, enabled) {
|
|
1648
|
+
useSyncExternalStore(
|
|
1649
|
+
(listener) => {
|
|
1650
|
+
listeners.add(listener);
|
|
1651
|
+
return () => listeners.delete(listener);
|
|
1652
|
+
},
|
|
1653
|
+
() => version,
|
|
1654
|
+
() => version
|
|
1655
|
+
);
|
|
1656
|
+
const active = enabled && Boolean(token);
|
|
1657
|
+
const slot = active ? slotFor(token) : void 0;
|
|
1658
|
+
useEffect4(() => {
|
|
1659
|
+
if (!active) return;
|
|
1660
|
+
const current = slotFor(token);
|
|
1661
|
+
if ((current.items === void 0 || current.failed) && !current.loading) void refreshHiddenByMe(token);
|
|
1662
|
+
}, [active, token]);
|
|
1663
|
+
return slot;
|
|
1664
|
+
}
|
|
1665
|
+
function useHiddenByMe(token, enabled = true) {
|
|
1666
|
+
const slot = useHiddenSlot(token, enabled);
|
|
1667
|
+
return {
|
|
1668
|
+
data: slot?.ids,
|
|
1669
|
+
isPending: enabled && Boolean(token) && slot?.ids === void 0
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function useHiddenItems(token, enabled = true) {
|
|
1673
|
+
const slot = useHiddenSlot(token, enabled);
|
|
1674
|
+
return { data: slot?.items, isPending: enabled && slot?.items === void 0 };
|
|
1675
|
+
}
|
|
1676
|
+
function useRemovalRights(token, id) {
|
|
1677
|
+
const [nonce, setNonce] = useState11(0);
|
|
1678
|
+
const key = id ? `${token}:${id}:${nonce}` : null;
|
|
1679
|
+
const [settled, setSettled] = useState11(null);
|
|
1680
|
+
useEffect4(() => {
|
|
1681
|
+
if (!key || !id) return;
|
|
1682
|
+
let active = true;
|
|
1683
|
+
fetchRemovalRights(token, id).then(
|
|
1684
|
+
(data) => active && setSettled({ key, data }),
|
|
1685
|
+
(error) => active && setSettled({ key, error })
|
|
1686
|
+
);
|
|
1687
|
+
return () => {
|
|
1688
|
+
active = false;
|
|
1689
|
+
};
|
|
1690
|
+
}, [key, token, id]);
|
|
1691
|
+
const current = key && settled?.key === key ? settled : null;
|
|
1692
|
+
const isLoading = key !== null && current === null;
|
|
1693
|
+
return {
|
|
1694
|
+
data: current?.data,
|
|
1695
|
+
error: current?.error,
|
|
1696
|
+
isLoading,
|
|
1697
|
+
isPending: isLoading,
|
|
1698
|
+
isError: current?.error !== void 0,
|
|
1699
|
+
refetch: () => setNonce((n) => n + 1)
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function useHideForMe(token, onChanged) {
|
|
1703
|
+
const [busy, setBusy] = useState11(false);
|
|
1704
|
+
const run = async (work) => {
|
|
1705
|
+
setBusy(true);
|
|
1706
|
+
try {
|
|
1707
|
+
await work();
|
|
1708
|
+
await refreshHiddenByMe(token);
|
|
1709
|
+
await onChanged?.();
|
|
1710
|
+
} finally {
|
|
1711
|
+
setBusy(false);
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
return {
|
|
1715
|
+
hide: {
|
|
1716
|
+
isPending: busy,
|
|
1717
|
+
mutateAsync: (args) => run(
|
|
1718
|
+
() => typeof args === "string" ? hideForMe(token, args) : hideForMe(token, args.id, args.label)
|
|
1719
|
+
)
|
|
1720
|
+
},
|
|
1721
|
+
unhide: {
|
|
1722
|
+
isPending: busy,
|
|
1723
|
+
mutateAsync: (id) => run(() => unhideForMe(token, id))
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/access/react/GovernedActionDialog.tsx
|
|
1729
|
+
import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1730
|
+
function GovernedActionDialog({
|
|
1731
|
+
open,
|
|
1732
|
+
onOpenChange,
|
|
1733
|
+
resourceType,
|
|
1734
|
+
resourceId,
|
|
1735
|
+
itemName,
|
|
1736
|
+
itemLabel = "site",
|
|
1737
|
+
href,
|
|
1738
|
+
action = "delete",
|
|
1739
|
+
onHidden
|
|
1740
|
+
}) {
|
|
1741
|
+
const ui = useAccessUi();
|
|
1742
|
+
const { toast } = ui;
|
|
1743
|
+
const ProTextarea = ui.Textarea;
|
|
1744
|
+
const currentUserId = ui.useCurrentUser().id;
|
|
1745
|
+
const { context, isLoading } = useAccessGate(resourceType, resourceId, {
|
|
1746
|
+
enabled: open
|
|
1747
|
+
});
|
|
1748
|
+
const [choice, setChoice] = useState12("delete");
|
|
1749
|
+
const [note, setNote] = useState12("");
|
|
1750
|
+
const [busy, setBusy] = useState12(false);
|
|
1751
|
+
const [sent, setSent] = useState12(null);
|
|
1752
|
+
const { hide } = useHideForMe(resourceType);
|
|
1753
|
+
const verb = action === "archive" ? "archive" : "delete";
|
|
1754
|
+
const ActionIcon = action === "archive" ? Archive : Trash23;
|
|
1755
|
+
const ownerName2 = context?.owner?.displayName ?? (context?.organization ? context.organization.name : null) ?? "the owner";
|
|
1756
|
+
async function hideIt() {
|
|
1757
|
+
setBusy(true);
|
|
1758
|
+
try {
|
|
1759
|
+
await hide.mutateAsync({ id: resourceId, label: itemName });
|
|
1760
|
+
toast.success(`Hidden for you. Find ${itemName} under Hidden.`);
|
|
1761
|
+
onHidden?.();
|
|
1762
|
+
onOpenChange(false);
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
toast.error(
|
|
1765
|
+
error instanceof Error ? error.message : "We couldn't hide that."
|
|
1766
|
+
);
|
|
1767
|
+
} finally {
|
|
1768
|
+
setBusy(false);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
async function sendRequest() {
|
|
1772
|
+
if (choice === "hide") return hideIt();
|
|
1773
|
+
setBusy(true);
|
|
1774
|
+
try {
|
|
1775
|
+
const result = choice === "delete" ? await createDeleteRequest({
|
|
1776
|
+
resourceType,
|
|
1777
|
+
resourceId,
|
|
1778
|
+
message: note,
|
|
1779
|
+
currentUserId,
|
|
1780
|
+
href: href ?? null
|
|
1781
|
+
}) : await createAccessRequest2({
|
|
1782
|
+
resourceType,
|
|
1783
|
+
resourceId,
|
|
1784
|
+
level: "admin",
|
|
1785
|
+
message: note,
|
|
1786
|
+
currentUserId,
|
|
1787
|
+
href: href ?? null
|
|
1788
|
+
});
|
|
1789
|
+
setSent(choice);
|
|
1790
|
+
if (result.blindAnswer) {
|
|
1791
|
+
toast.success(result.blindAnswer);
|
|
1792
|
+
} else if (result.already) {
|
|
1793
|
+
toast.success("Your request is already waiting for an answer.");
|
|
1794
|
+
} else if (result.delivered === 0) {
|
|
1795
|
+
toast.warning(
|
|
1796
|
+
"Your request is saved, but we couldn't message them just now."
|
|
1797
|
+
);
|
|
1798
|
+
} else {
|
|
1799
|
+
toast.success(
|
|
1800
|
+
`Request sent to ${result.recipients[0]?.displayName ?? ownerName2}.`
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
toast.error(
|
|
1805
|
+
error instanceof Error ? error.message : "We couldn't send that request."
|
|
1806
|
+
);
|
|
1807
|
+
} finally {
|
|
1808
|
+
setBusy(false);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return /* @__PURE__ */ jsx12(Dialog2, { open, onOpenChange, children: /* @__PURE__ */ jsxs11(DialogContent2, { className: "matrx-touch-targets sm:max-w-xl", children: [
|
|
1812
|
+
/* @__PURE__ */ jsxs11(DialogHeader2, { children: [
|
|
1813
|
+
/* @__PURE__ */ jsx12("div", { className: "mb-2 flex h-11 w-11 items-center justify-center rounded-full bg-destructive/10", children: /* @__PURE__ */ jsx12(ActionIcon, { className: "h-5 w-5 text-destructive", "aria-hidden": true }) }),
|
|
1814
|
+
/* @__PURE__ */ jsxs11(DialogTitle2, { children: [
|
|
1815
|
+
"Ask ",
|
|
1816
|
+
ownerName2,
|
|
1817
|
+
" to ",
|
|
1818
|
+
verb,
|
|
1819
|
+
" this ",
|
|
1820
|
+
itemLabel
|
|
1821
|
+
] }),
|
|
1822
|
+
/* @__PURE__ */ jsxs11(DialogDescription2, { children: [
|
|
1823
|
+
"You can work on",
|
|
1824
|
+
" ",
|
|
1825
|
+
/* @__PURE__ */ jsx12("span", { className: "font-medium text-foreground", children: itemName }),
|
|
1826
|
+
", but removing it is up to them. Ask them, or hide it just for you."
|
|
1827
|
+
] })
|
|
1828
|
+
] }),
|
|
1829
|
+
isLoading ? /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 rounded-lg border border-border bg-muted/30 p-4 text-sm text-muted-foreground", children: [
|
|
1830
|
+
/* @__PURE__ */ jsx12(Loader25, { className: "h-4 w-4 animate-spin", "aria-hidden": true }),
|
|
1831
|
+
"Finding the person who can help\u2026"
|
|
1832
|
+
] }) : context?.status === "error" || context?.status === "anonymous" ? /* @__PURE__ */ jsxs11("div", { className: "rounded-lg border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive-ink", children: [
|
|
1833
|
+
"We couldn't find the right person to contact. Try again after refreshing.",
|
|
1834
|
+
ui.renderErrorMenu({})
|
|
1835
|
+
] }) : sent ? /* @__PURE__ */ jsxs11("div", { className: "rounded-lg border border-primary/30 bg-primary/5 p-4", children: [
|
|
1836
|
+
/* @__PURE__ */ jsxs11("p", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
1837
|
+
/* @__PURE__ */ jsx12(Check4, { className: "h-4 w-4 text-primary", "aria-hidden": true }),
|
|
1838
|
+
sent === "delete" ? action === "archive" ? "Archive requested" : "Deletion requested" : "Full access requested"
|
|
1839
|
+
] }),
|
|
1840
|
+
/* @__PURE__ */ jsxs11("p", { className: "mt-1 text-xs text-muted-foreground", children: [
|
|
1841
|
+
"The request is saved in Access requests and sent to ",
|
|
1842
|
+
ownerName2,
|
|
1843
|
+
" in Messages."
|
|
1844
|
+
] })
|
|
1845
|
+
] }) : /* @__PURE__ */ jsxs11(Fragment6, { children: [
|
|
1846
|
+
/* @__PURE__ */ jsxs11("div", { className: "grid gap-2 sm:grid-cols-3", children: [
|
|
1847
|
+
/* @__PURE__ */ jsx12(
|
|
1848
|
+
RequestChoiceButton,
|
|
1849
|
+
{
|
|
1850
|
+
active: choice === "delete",
|
|
1851
|
+
icon: /* @__PURE__ */ jsx12(ActionIcon, { className: "h-4 w-4", "aria-hidden": true }),
|
|
1852
|
+
title: `Ask them to ${verb} it`,
|
|
1853
|
+
detail: "They approve it from your message.",
|
|
1854
|
+
onClick: () => setChoice("delete")
|
|
1855
|
+
}
|
|
1856
|
+
),
|
|
1857
|
+
/* @__PURE__ */ jsx12(
|
|
1858
|
+
RequestChoiceButton,
|
|
1859
|
+
{
|
|
1860
|
+
active: choice === "hide",
|
|
1861
|
+
icon: /* @__PURE__ */ jsx12(EyeOff, { className: "h-4 w-4", "aria-hidden": true }),
|
|
1862
|
+
title: "Hide it for me",
|
|
1863
|
+
detail: "Only you stop seeing it; undo under Hidden.",
|
|
1864
|
+
onClick: () => setChoice("hide")
|
|
1865
|
+
}
|
|
1866
|
+
),
|
|
1867
|
+
/* @__PURE__ */ jsx12(
|
|
1868
|
+
RequestChoiceButton,
|
|
1869
|
+
{
|
|
1870
|
+
active: choice === "full",
|
|
1871
|
+
icon: /* @__PURE__ */ jsx12(ShieldCheck, { className: "h-4 w-4", "aria-hidden": true }),
|
|
1872
|
+
title: "Request full access",
|
|
1873
|
+
detail: `Then you can ${verb} it yourself.`,
|
|
1874
|
+
onClick: () => setChoice("full")
|
|
1875
|
+
}
|
|
1876
|
+
)
|
|
1877
|
+
] }),
|
|
1878
|
+
choice === "hide" ? null : /* @__PURE__ */ jsx12(
|
|
1879
|
+
ProTextarea,
|
|
1880
|
+
{
|
|
1881
|
+
className: "min-h-20 text-base md:text-sm",
|
|
1882
|
+
value: note,
|
|
1883
|
+
maxLength: 500,
|
|
1884
|
+
placeholder: "Add a note for the owner (optional)",
|
|
1885
|
+
onChange: (event) => setNote(event.target.value)
|
|
1886
|
+
}
|
|
1887
|
+
)
|
|
1888
|
+
] }),
|
|
1889
|
+
/* @__PURE__ */ jsxs11(DialogFooter2, { children: [
|
|
1890
|
+
/* @__PURE__ */ jsx12(Button9, { variant: "outline", onClick: () => onOpenChange(false), children: sent ? "Done" : "Cancel" }),
|
|
1891
|
+
!sent && context?.status !== "error" && context?.status !== "anonymous" ? /* @__PURE__ */ jsx12(
|
|
1892
|
+
Button9,
|
|
1893
|
+
{
|
|
1894
|
+
icon: busy ? /* @__PURE__ */ jsx12(Loader25, { className: "animate-spin", "aria-hidden": true }) : choice === "hide" ? /* @__PURE__ */ jsx12(EyeOff, { "aria-hidden": true }) : choice === "delete" ? /* @__PURE__ */ jsx12(Send4, { "aria-hidden": true }) : /* @__PURE__ */ jsx12(KeyRound3, { "aria-hidden": true }),
|
|
1895
|
+
variant: "primary",
|
|
1896
|
+
disabled: busy || isLoading,
|
|
1897
|
+
onClick: () => void sendRequest(),
|
|
1898
|
+
children: choice === "hide" ? "Hide for me" : "Send request"
|
|
1899
|
+
}
|
|
1900
|
+
) : null
|
|
1901
|
+
] })
|
|
1902
|
+
] }) });
|
|
1903
|
+
}
|
|
1904
|
+
function RequestChoiceButton({
|
|
1905
|
+
active,
|
|
1906
|
+
icon,
|
|
1907
|
+
title,
|
|
1908
|
+
detail,
|
|
1909
|
+
onClick
|
|
1910
|
+
}) {
|
|
1911
|
+
return /* @__PURE__ */ jsxs11(
|
|
1912
|
+
"button",
|
|
1913
|
+
{
|
|
1914
|
+
type: "button",
|
|
1915
|
+
"aria-pressed": active,
|
|
1916
|
+
onClick,
|
|
1917
|
+
className: `rounded-lg border p-3 text-left transition-colors ${active ? "border-primary bg-primary/5" : "border-border hover:bg-muted/40"}`,
|
|
1918
|
+
children: [
|
|
1919
|
+
/* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [
|
|
1920
|
+
icon,
|
|
1921
|
+
title
|
|
1922
|
+
] }),
|
|
1923
|
+
/* @__PURE__ */ jsx12("span", { className: "mt-1 block text-xs text-muted-foreground", children: detail })
|
|
1924
|
+
]
|
|
1925
|
+
}
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// src/access/react/HiddenByMeButton.tsx
|
|
1930
|
+
import { useState as useState13 } from "react";
|
|
1931
|
+
import { Eye, EyeOff as EyeOff2 } from "lucide-react";
|
|
1932
|
+
import { Button as Button10 } from "@ai-matrx/design-system/controls";
|
|
1933
|
+
import {
|
|
1934
|
+
Dialog as Dialog3,
|
|
1935
|
+
DialogContent as DialogContent3,
|
|
1936
|
+
DialogDescription as DialogDescription3,
|
|
1937
|
+
DialogHeader as DialogHeader3,
|
|
1938
|
+
DialogTitle as DialogTitle3
|
|
1939
|
+
} from "@ai-matrx/design-system";
|
|
1940
|
+
import { Fragment as Fragment7, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1941
|
+
function HiddenByMeButton({
|
|
1942
|
+
token,
|
|
1943
|
+
pluralLabel
|
|
1944
|
+
}) {
|
|
1945
|
+
const ui = useAccessUi();
|
|
1946
|
+
const { toast } = ui;
|
|
1947
|
+
const [open, setOpen] = useState13(false);
|
|
1948
|
+
const ids = useHiddenByMe(token);
|
|
1949
|
+
const items = useHiddenItems(token, open);
|
|
1950
|
+
const { unhide } = useHideForMe(token);
|
|
1951
|
+
const count = ids.data?.length ?? 0;
|
|
1952
|
+
if (count === 0 && !open) return null;
|
|
1953
|
+
const showAgain = async (id, name) => {
|
|
1954
|
+
try {
|
|
1955
|
+
await unhide.mutateAsync(id);
|
|
1956
|
+
toast.success(`${name} is back in your list`);
|
|
1957
|
+
if (count <= 1) setOpen(false);
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
toast.error(error instanceof Error ? error.message : "We couldn't show that again.");
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
return /* @__PURE__ */ jsxs12(Fragment7, { children: [
|
|
1963
|
+
/* @__PURE__ */ jsx13(
|
|
1964
|
+
Button10,
|
|
1965
|
+
{
|
|
1966
|
+
type: "button",
|
|
1967
|
+
variant: "quiet",
|
|
1968
|
+
icon: /* @__PURE__ */ jsx13(EyeOff2, {}),
|
|
1969
|
+
title: `${pluralLabel} you hid`,
|
|
1970
|
+
onClick: () => setOpen(true),
|
|
1971
|
+
children: `Hidden ${count}`
|
|
1972
|
+
}
|
|
1973
|
+
),
|
|
1974
|
+
/* @__PURE__ */ jsx13(Dialog3, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs12(DialogContent3, { className: "matrx-touch-targets sm:max-w-lg", children: [
|
|
1975
|
+
/* @__PURE__ */ jsxs12(DialogHeader3, { children: [
|
|
1976
|
+
/* @__PURE__ */ jsx13(DialogTitle3, { children: "Hidden by me" }),
|
|
1977
|
+
/* @__PURE__ */ jsxs12(DialogDescription3, { children: [
|
|
1978
|
+
"Only you stopped seeing these ",
|
|
1979
|
+
pluralLabel,
|
|
1980
|
+
"."
|
|
1981
|
+
] })
|
|
1982
|
+
] }),
|
|
1983
|
+
/* @__PURE__ */ jsx13("ul", { className: "flex max-h-[60dvh] flex-col divide-y divide-border overflow-y-auto", children: (items.data ?? []).map((item) => {
|
|
1984
|
+
const name = item.label ?? "Untitled";
|
|
1985
|
+
return /* @__PURE__ */ jsxs12("li", { className: "flex items-center gap-2 py-2", children: [
|
|
1986
|
+
/* @__PURE__ */ jsx13("div", { className: "min-w-0 flex-1", children: ui.renderEntityRef({ token: item.token, id: item.id, name }) }),
|
|
1987
|
+
/* @__PURE__ */ jsx13(
|
|
1988
|
+
Button10,
|
|
1989
|
+
{
|
|
1990
|
+
type: "button",
|
|
1991
|
+
variant: "outline",
|
|
1992
|
+
icon: /* @__PURE__ */ jsx13(Eye, {}),
|
|
1993
|
+
disabled: unhide.isPending,
|
|
1994
|
+
onClick: () => void showAgain(item.id, name),
|
|
1995
|
+
children: "Show again"
|
|
1996
|
+
}
|
|
1997
|
+
)
|
|
1998
|
+
] }, item.id);
|
|
1999
|
+
}) })
|
|
2000
|
+
] }) })
|
|
2001
|
+
] });
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// src/access/react/HiddenFilter.tsx
|
|
2005
|
+
import { EyeOff as EyeOff3 } from "lucide-react";
|
|
2006
|
+
import { Button as Button11 } from "@ai-matrx/design-system/controls";
|
|
2007
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
2008
|
+
function HiddenFilter({
|
|
2009
|
+
count,
|
|
2010
|
+
value,
|
|
2011
|
+
onValueChange
|
|
2012
|
+
}) {
|
|
2013
|
+
if (count === 0 && value === "visible") return null;
|
|
2014
|
+
const showing = value === "hidden";
|
|
2015
|
+
return /* @__PURE__ */ jsx14(
|
|
2016
|
+
Button11,
|
|
2017
|
+
{
|
|
2018
|
+
type: "button",
|
|
2019
|
+
variant: showing ? "outline" : "quiet",
|
|
2020
|
+
icon: /* @__PURE__ */ jsx14(EyeOff3, {}),
|
|
2021
|
+
"aria-pressed": showing,
|
|
2022
|
+
title: showing ? "Back to the list" : "Items you hid",
|
|
2023
|
+
onClick: () => onValueChange(showing ? "visible" : "hidden"),
|
|
2024
|
+
children: `Hidden ${count}`
|
|
2025
|
+
}
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/access/react/ResourceActionRequestButtons.tsx
|
|
2030
|
+
import { useState as useState14 } from "react";
|
|
2031
|
+
import {
|
|
2032
|
+
Check as Check5,
|
|
2033
|
+
CircleSlash,
|
|
2034
|
+
ExternalLink,
|
|
2035
|
+
Flag,
|
|
2036
|
+
KeyRound as KeyRound4,
|
|
2037
|
+
Loader2 as Loader26,
|
|
2038
|
+
Trash2 as Trash24
|
|
2039
|
+
} from "lucide-react";
|
|
2040
|
+
import { Button as Button12 } from "@ai-matrx/design-system/controls";
|
|
2041
|
+
import {
|
|
2042
|
+
decideAccessRequest,
|
|
2043
|
+
getResourceActionRequestForDecision,
|
|
2044
|
+
reportAccessRequest
|
|
2045
|
+
} from "@ai-matrx/data/access";
|
|
2046
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2047
|
+
function ResourceActionRequestButtons({
|
|
2048
|
+
requestId,
|
|
2049
|
+
actionKey,
|
|
2050
|
+
href,
|
|
2051
|
+
itemName,
|
|
2052
|
+
isOwn = false,
|
|
2053
|
+
compact = false,
|
|
2054
|
+
onDone
|
|
2055
|
+
}) {
|
|
2056
|
+
const ui = useAccessUi();
|
|
2057
|
+
const { toast, confirm, getResourceActionRequestAction } = ui;
|
|
2058
|
+
const currentUserId = ui.useCurrentUser().id;
|
|
2059
|
+
const [busy, setBusy] = useState14(false);
|
|
2060
|
+
const [done, setDone] = useState14(null);
|
|
2061
|
+
const action = getResourceActionRequestAction(actionKey);
|
|
2062
|
+
const safeHref = href?.startsWith("/") && !href.startsWith("//") ? href : null;
|
|
2063
|
+
const size = compact ? "h-7 px-2 text-[11px]" : "h-11 sm:h-8";
|
|
2064
|
+
if (isOwn) {
|
|
2065
|
+
return /* @__PURE__ */ jsxs13("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 text-[11px] text-inherit", children: [
|
|
2066
|
+
/* @__PURE__ */ jsx15(Trash24, { className: "h-3 w-3", "aria-hidden": true }),
|
|
2067
|
+
"Deletion requested"
|
|
2068
|
+
] });
|
|
2069
|
+
}
|
|
2070
|
+
if (done) {
|
|
2071
|
+
return /* @__PURE__ */ jsxs13("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 text-[11px] text-muted-foreground", children: [
|
|
2072
|
+
/* @__PURE__ */ jsx15(Check5, { className: "h-3 w-3", "aria-hidden": true }),
|
|
2073
|
+
done === "granted" ? "Completed" : done === "reported" ? "Reported" : "Declined"
|
|
2074
|
+
] });
|
|
2075
|
+
}
|
|
2076
|
+
async function completeAction() {
|
|
2077
|
+
if (!action) {
|
|
2078
|
+
toast.error("This request uses an action the app does not recognize.");
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const ok = await confirm({
|
|
2082
|
+
title: action.confirmTitle(itemName),
|
|
2083
|
+
description: action.confirmDescription,
|
|
2084
|
+
confirmLabel: action.label,
|
|
2085
|
+
variant: "destructive"
|
|
2086
|
+
});
|
|
2087
|
+
if (!ok) return;
|
|
2088
|
+
setBusy(true);
|
|
2089
|
+
try {
|
|
2090
|
+
const request = await getResourceActionRequestForDecision(requestId);
|
|
2091
|
+
const durableAction = getResourceActionRequestAction(
|
|
2092
|
+
request.resourceAction.actionKey
|
|
2093
|
+
);
|
|
2094
|
+
if (!durableAction) {
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
"This request uses an action the app does not recognize."
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
await durableAction.execute({
|
|
2100
|
+
resourceType: request.resourceType,
|
|
2101
|
+
resourceId: request.resourceId
|
|
2102
|
+
});
|
|
2103
|
+
const result = await decideAccessRequest({
|
|
2104
|
+
requestId,
|
|
2105
|
+
decision: "complete",
|
|
2106
|
+
currentUserId
|
|
2107
|
+
});
|
|
2108
|
+
setDone(result.status);
|
|
2109
|
+
onDone?.(result.status);
|
|
2110
|
+
toast.success(durableAction.completedLabel);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
toast.error(
|
|
2113
|
+
error instanceof Error ? error.message : "We couldn't complete that action."
|
|
2114
|
+
);
|
|
2115
|
+
} finally {
|
|
2116
|
+
setBusy(false);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
async function grantFullAccess() {
|
|
2120
|
+
setBusy(true);
|
|
2121
|
+
try {
|
|
2122
|
+
const result = await decideAccessRequest({
|
|
2123
|
+
requestId,
|
|
2124
|
+
decision: "grant",
|
|
2125
|
+
level: "admin",
|
|
2126
|
+
currentUserId
|
|
2127
|
+
});
|
|
2128
|
+
setDone(result.status);
|
|
2129
|
+
onDone?.(result.status);
|
|
2130
|
+
toast.success("Full access granted.");
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
toast.error(
|
|
2133
|
+
error instanceof Error ? error.message : "We couldn't grant access."
|
|
2134
|
+
);
|
|
2135
|
+
} finally {
|
|
2136
|
+
setBusy(false);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
async function decline() {
|
|
2140
|
+
setBusy(true);
|
|
2141
|
+
try {
|
|
2142
|
+
const result = await decideAccessRequest({
|
|
2143
|
+
requestId,
|
|
2144
|
+
decision: "decline",
|
|
2145
|
+
currentUserId
|
|
2146
|
+
});
|
|
2147
|
+
setDone(result.status);
|
|
2148
|
+
onDone?.(result.status);
|
|
2149
|
+
toast.success("Request declined.");
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
toast.error(
|
|
2152
|
+
error instanceof Error ? error.message : "We couldn't decline that."
|
|
2153
|
+
);
|
|
2154
|
+
} finally {
|
|
2155
|
+
setBusy(false);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
async function report() {
|
|
2159
|
+
const ok = await confirm({
|
|
2160
|
+
title: "Report this request?",
|
|
2161
|
+
description: "This ends the conversation for good. They won't be able to ask about this item again.",
|
|
2162
|
+
confirmLabel: "Report",
|
|
2163
|
+
variant: "destructive"
|
|
2164
|
+
});
|
|
2165
|
+
if (!ok) return;
|
|
2166
|
+
setBusy(true);
|
|
2167
|
+
try {
|
|
2168
|
+
await reportAccessRequest(requestId);
|
|
2169
|
+
setDone("reported");
|
|
2170
|
+
onDone?.("reported");
|
|
2171
|
+
toast.success("Request reported.");
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
toast.error(
|
|
2174
|
+
error instanceof Error ? error.message : "We couldn't report that."
|
|
2175
|
+
);
|
|
2176
|
+
} finally {
|
|
2177
|
+
setBusy(false);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap gap-2", children: [
|
|
2181
|
+
action ? /* @__PURE__ */ jsx15(
|
|
2182
|
+
Button12,
|
|
2183
|
+
{
|
|
2184
|
+
icon: busy ? /* @__PURE__ */ jsx15(Loader26, { className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx15(Trash24, { "aria-hidden": true }),
|
|
2185
|
+
variant: "primary",
|
|
2186
|
+
className: size,
|
|
2187
|
+
disabled: busy,
|
|
2188
|
+
onClick: () => void completeAction(),
|
|
2189
|
+
children: action.label
|
|
2190
|
+
}
|
|
2191
|
+
) : null,
|
|
2192
|
+
/* @__PURE__ */ jsx15(
|
|
2193
|
+
Button12,
|
|
2194
|
+
{
|
|
2195
|
+
icon: /* @__PURE__ */ jsx15(KeyRound4, { "aria-hidden": true }),
|
|
2196
|
+
className: size,
|
|
2197
|
+
variant: "outline",
|
|
2198
|
+
disabled: busy,
|
|
2199
|
+
onClick: () => void grantFullAccess(),
|
|
2200
|
+
children: "Give full access"
|
|
2201
|
+
}
|
|
2202
|
+
),
|
|
2203
|
+
safeHref ? /* @__PURE__ */ jsx15(Button12, { asChild: true, className: size, variant: "outline", children: /* @__PURE__ */ jsxs13(AccessLink, { href: safeHref, target: "_blank", rel: "noopener noreferrer", children: [
|
|
2204
|
+
/* @__PURE__ */ jsx15(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": true }),
|
|
2205
|
+
"Open item"
|
|
2206
|
+
] }) }) : null,
|
|
2207
|
+
/* @__PURE__ */ jsx15(
|
|
2208
|
+
Button12,
|
|
2209
|
+
{
|
|
2210
|
+
icon: /* @__PURE__ */ jsx15(CircleSlash, { "aria-hidden": true }),
|
|
2211
|
+
className: size,
|
|
2212
|
+
variant: "quiet",
|
|
2213
|
+
disabled: busy,
|
|
2214
|
+
onClick: () => void decline(),
|
|
2215
|
+
children: "Decline"
|
|
2216
|
+
}
|
|
2217
|
+
),
|
|
2218
|
+
/* @__PURE__ */ jsx15(
|
|
2219
|
+
Button12,
|
|
2220
|
+
{
|
|
2221
|
+
icon: /* @__PURE__ */ jsx15(Flag, { "aria-hidden": true }),
|
|
2222
|
+
className: size,
|
|
2223
|
+
variant: "quiet",
|
|
2224
|
+
disabled: busy,
|
|
2225
|
+
onClick: () => void report(),
|
|
2226
|
+
children: "Report"
|
|
2227
|
+
}
|
|
2228
|
+
)
|
|
2229
|
+
] });
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/access/react/SettingRequestActionButtons.tsx
|
|
2233
|
+
import { useState as useState15 } from "react";
|
|
2234
|
+
import {
|
|
2235
|
+
Check as Check6,
|
|
2236
|
+
CircleSlash as CircleSlash2,
|
|
2237
|
+
ExternalLink as ExternalLink2,
|
|
2238
|
+
Loader2 as Loader27,
|
|
2239
|
+
Settings
|
|
2240
|
+
} from "lucide-react";
|
|
2241
|
+
import { Button as Button13 } from "@ai-matrx/design-system/controls";
|
|
2242
|
+
import {
|
|
2243
|
+
decideSettingAccessRequest,
|
|
2244
|
+
getSettingAccessRequestForDecision
|
|
2245
|
+
} from "@ai-matrx/data/access";
|
|
2246
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2247
|
+
function SettingRequestActionButtons({
|
|
2248
|
+
requestId,
|
|
2249
|
+
href,
|
|
2250
|
+
actionKey,
|
|
2251
|
+
isOwn = false,
|
|
2252
|
+
compact = false,
|
|
2253
|
+
onDone
|
|
2254
|
+
}) {
|
|
2255
|
+
const ui = useAccessUi();
|
|
2256
|
+
const toast = ui.toast;
|
|
2257
|
+
const getSettingRequestAction = ui.getSettingRequestAction;
|
|
2258
|
+
const [busy, setBusy] = useState15(false);
|
|
2259
|
+
const [done, setDone] = useState15(null);
|
|
2260
|
+
const action = getSettingRequestAction(actionKey);
|
|
2261
|
+
const safeHref = href.startsWith("/") && !href.startsWith("//") ? href : null;
|
|
2262
|
+
if (isOwn) {
|
|
2263
|
+
return /* @__PURE__ */ jsxs14("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 text-[11px] text-inherit", children: [
|
|
2264
|
+
/* @__PURE__ */ jsx16(Settings, { className: "h-3 w-3", "aria-hidden": true }),
|
|
2265
|
+
"Setting change requested"
|
|
2266
|
+
] });
|
|
2267
|
+
}
|
|
2268
|
+
if (done) {
|
|
2269
|
+
return /* @__PURE__ */ jsxs14("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 text-[11px] text-muted-foreground", children: [
|
|
2270
|
+
/* @__PURE__ */ jsx16(Check6, { className: "h-3 w-3", "aria-hidden": true }),
|
|
2271
|
+
done === "granted" ? "Change completed" : "Declined"
|
|
2272
|
+
] });
|
|
2273
|
+
}
|
|
2274
|
+
async function apply() {
|
|
2275
|
+
if (!action) {
|
|
2276
|
+
toast.error("This request uses an action the app does not recognize.");
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
setBusy(true);
|
|
2280
|
+
try {
|
|
2281
|
+
const request = await getSettingAccessRequestForDecision(requestId);
|
|
2282
|
+
const durableAction = getSettingRequestAction(
|
|
2283
|
+
request.settingRequest.actionKey
|
|
2284
|
+
);
|
|
2285
|
+
if (!durableAction) {
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
"This request uses an action the app does not recognize."
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
await durableAction.execute(request.settingRequest.actionPayload, {
|
|
2291
|
+
organizationId: request.resourceId
|
|
2292
|
+
});
|
|
2293
|
+
const result = await decideSettingAccessRequest({
|
|
2294
|
+
requestId,
|
|
2295
|
+
decision: "complete"
|
|
2296
|
+
});
|
|
2297
|
+
setDone(result.status);
|
|
2298
|
+
onDone?.(result.status);
|
|
2299
|
+
toast.success(durableAction.completedLabel);
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
toast.error(
|
|
2302
|
+
error instanceof Error ? error.message : "We couldn't make that change."
|
|
2303
|
+
);
|
|
2304
|
+
} finally {
|
|
2305
|
+
setBusy(false);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async function decline() {
|
|
2309
|
+
setBusy(true);
|
|
2310
|
+
try {
|
|
2311
|
+
const result = await decideSettingAccessRequest({
|
|
2312
|
+
requestId,
|
|
2313
|
+
decision: "decline"
|
|
2314
|
+
});
|
|
2315
|
+
setDone(result.status);
|
|
2316
|
+
onDone?.(result.status);
|
|
2317
|
+
toast.success("Request declined.");
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
toast.error(
|
|
2320
|
+
error instanceof Error ? error.message : "We couldn't decline that."
|
|
2321
|
+
);
|
|
2322
|
+
} finally {
|
|
2323
|
+
setBusy(false);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
const size = compact ? "h-7 px-2 text-[11px]" : "h-11 sm:h-8";
|
|
2327
|
+
return /* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap gap-2", children: [
|
|
2328
|
+
action ? /* @__PURE__ */ jsx16(
|
|
2329
|
+
Button13,
|
|
2330
|
+
{
|
|
2331
|
+
icon: busy ? /* @__PURE__ */ jsx16(Loader27, { className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx16(Check6, { "aria-hidden": true }),
|
|
2332
|
+
variant: "primary",
|
|
2333
|
+
className: size,
|
|
2334
|
+
disabled: busy,
|
|
2335
|
+
onClick: () => void apply(),
|
|
2336
|
+
children: action.label
|
|
2337
|
+
}
|
|
2338
|
+
) : null,
|
|
2339
|
+
safeHref ? /* @__PURE__ */ jsx16(Button13, { asChild: true, className: size, variant: "outline", children: /* @__PURE__ */ jsxs14(AccessLink, { href: safeHref, target: "_blank", rel: "noopener noreferrer", children: [
|
|
2340
|
+
/* @__PURE__ */ jsx16(ExternalLink2, { className: "h-3.5 w-3.5", "aria-hidden": true }),
|
|
2341
|
+
"Open setting"
|
|
2342
|
+
] }) }) : null,
|
|
2343
|
+
/* @__PURE__ */ jsx16(
|
|
2344
|
+
Button13,
|
|
2345
|
+
{
|
|
2346
|
+
icon: /* @__PURE__ */ jsx16(CircleSlash2, { "aria-hidden": true }),
|
|
2347
|
+
className: size,
|
|
2348
|
+
variant: "quiet",
|
|
2349
|
+
disabled: busy,
|
|
2350
|
+
onClick: () => void decline(),
|
|
2351
|
+
children: "Decline"
|
|
2352
|
+
}
|
|
2353
|
+
)
|
|
2354
|
+
] });
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/access/react/AccessRequestsSurface.tsx
|
|
2358
|
+
import { useCallback as useCallback2, useEffect as useEffect5, useState as useState16 } from "react";
|
|
2359
|
+
import {
|
|
2360
|
+
CircleCheck,
|
|
2361
|
+
CircleSlash as CircleSlash3,
|
|
2362
|
+
Flag as Flag2,
|
|
2363
|
+
Inbox,
|
|
2364
|
+
KeyRound as KeyRound5,
|
|
2365
|
+
PenLine,
|
|
2366
|
+
Send as Send5,
|
|
2367
|
+
Undo2 as Undo22
|
|
2368
|
+
} from "lucide-react";
|
|
2369
|
+
import { Badge, Skeleton, cn as cn3 } from "@ai-matrx/design-system";
|
|
2370
|
+
import { Button as Button14 } from "@ai-matrx/design-system/controls";
|
|
2371
|
+
import { RefreshCwTapButton } from "@ai-matrx/design-system/tap-target/buttons";
|
|
2372
|
+
import { formatRelativeTime } from "@ai-matrx/kit/format";
|
|
2373
|
+
import {
|
|
2374
|
+
decideAccessRequest as decideAccessRequest2,
|
|
2375
|
+
listAccessRequests,
|
|
2376
|
+
reportAccessRequest as reportAccessRequest2,
|
|
2377
|
+
withdrawAccessRequest as withdrawAccessRequest2
|
|
2378
|
+
} from "@ai-matrx/data/access";
|
|
2379
|
+
import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2380
|
+
var ACCESS_REQUESTS_HREF = "/user-settings/access-requests";
|
|
2381
|
+
var STATUS_LABEL = {
|
|
2382
|
+
pending: "Pending",
|
|
2383
|
+
granted: "Granted",
|
|
2384
|
+
declined: "Declined",
|
|
2385
|
+
withdrawn: "Withdrawn",
|
|
2386
|
+
reported: "Reported"
|
|
2387
|
+
};
|
|
2388
|
+
var STATUS_TONE = {
|
|
2389
|
+
pending: "bg-muted text-muted-foreground",
|
|
2390
|
+
granted: "bg-primary/10 text-primary-ink",
|
|
2391
|
+
declined: "bg-muted text-muted-foreground",
|
|
2392
|
+
withdrawn: "bg-muted text-muted-foreground",
|
|
2393
|
+
reported: "bg-destructive/10 text-destructive-ink"
|
|
2394
|
+
};
|
|
2395
|
+
function statusLabel(row) {
|
|
2396
|
+
return row.requestKind === "resource_action" && row.status === "granted" ? "Completed" : STATUS_LABEL[row.status];
|
|
2397
|
+
}
|
|
2398
|
+
function boxFromParam(value) {
|
|
2399
|
+
return value === "sent" ? "sent" : "inbox";
|
|
2400
|
+
}
|
|
2401
|
+
async function fetchBoxes() {
|
|
2402
|
+
try {
|
|
2403
|
+
const [inbox, sent] = await Promise.all([
|
|
2404
|
+
listAccessRequests("inbox"),
|
|
2405
|
+
listAccessRequests("sent")
|
|
2406
|
+
]);
|
|
2407
|
+
return { rows: { inbox, sent }, error: null };
|
|
2408
|
+
} catch (e) {
|
|
2409
|
+
return {
|
|
2410
|
+
rows: { inbox: [], sent: [] },
|
|
2411
|
+
error: e instanceof Error ? e.message : "We couldn't load your access requests."
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function EntityCell({ row }) {
|
|
2416
|
+
const ui = useAccessUi();
|
|
2417
|
+
const label = row.entityLabel ?? "Item";
|
|
2418
|
+
if (row.requestKind === "setting" && row.settingRequest) {
|
|
2419
|
+
return /* @__PURE__ */ jsxs15("span", { className: "flex min-w-0 items-center gap-1.5 text-sm", children: [
|
|
2420
|
+
/* @__PURE__ */ jsx17("span", { className: "shrink-0 text-xs uppercase tracking-wide text-muted-foreground", children: "Setting" }),
|
|
2421
|
+
/* @__PURE__ */ jsx17(
|
|
2422
|
+
AccessLink,
|
|
2423
|
+
{
|
|
2424
|
+
href: row.settingRequest.href,
|
|
2425
|
+
className: "min-w-0 truncate font-medium text-primary hover:underline",
|
|
2426
|
+
children: row.settingRequest.settingLabel
|
|
2427
|
+
}
|
|
2428
|
+
)
|
|
2429
|
+
] });
|
|
2430
|
+
}
|
|
2431
|
+
if (!row.entityTitle) {
|
|
2432
|
+
return /* @__PURE__ */ jsxs15("span", { className: "text-sm text-muted-foreground", children: [
|
|
2433
|
+
"Untitled ",
|
|
2434
|
+
label.toLowerCase()
|
|
2435
|
+
] });
|
|
2436
|
+
}
|
|
2437
|
+
return /* @__PURE__ */ jsxs15("span", { className: "flex min-w-0 items-center gap-1.5 text-sm", children: [
|
|
2438
|
+
/* @__PURE__ */ jsx17("span", { className: "shrink-0 text-xs uppercase tracking-wide text-muted-foreground", children: label }),
|
|
2439
|
+
ui.renderEntityRef({
|
|
2440
|
+
token: row.resourceType,
|
|
2441
|
+
id: row.resourceId,
|
|
2442
|
+
name: row.entityTitle,
|
|
2443
|
+
showIcon: false,
|
|
2444
|
+
className: "min-w-0 font-medium"
|
|
2445
|
+
})
|
|
2446
|
+
] });
|
|
2447
|
+
}
|
|
2448
|
+
function RowShell({ children }) {
|
|
2449
|
+
return /* @__PURE__ */ jsx17("li", { className: "rounded-lg border border-border bg-card p-3 sm:p-4", children });
|
|
2450
|
+
}
|
|
2451
|
+
function LoadingRows() {
|
|
2452
|
+
return /* @__PURE__ */ jsx17("ul", { className: "space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx17(RowShell, { children: /* @__PURE__ */ jsxs15("div", { className: "flex items-start gap-3", children: [
|
|
2453
|
+
/* @__PURE__ */ jsx17(Skeleton, { className: "h-8 w-8 shrink-0 rounded-full" }),
|
|
2454
|
+
/* @__PURE__ */ jsxs15("div", { className: "min-w-0 flex-1 space-y-2", children: [
|
|
2455
|
+
/* @__PURE__ */ jsx17(Skeleton, { className: "h-4 w-1/3" }),
|
|
2456
|
+
/* @__PURE__ */ jsx17(Skeleton, { className: "h-3 w-2/3" })
|
|
2457
|
+
] })
|
|
2458
|
+
] }) }, i)) });
|
|
2459
|
+
}
|
|
2460
|
+
function EmptyState({ box }) {
|
|
2461
|
+
const Icon = box === "inbox" ? Inbox : Send5;
|
|
2462
|
+
return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col items-center gap-2 rounded-lg border border-dashed border-border px-6 py-12 text-center", children: [
|
|
2463
|
+
/* @__PURE__ */ jsx17(Icon, { className: "h-6 w-6 text-muted-foreground", "aria-hidden": true }),
|
|
2464
|
+
/* @__PURE__ */ jsx17("p", { className: "text-sm font-medium text-foreground", children: box === "inbox" ? "Nothing waiting on you" : "You haven't asked for anything" }),
|
|
2465
|
+
/* @__PURE__ */ jsx17("p", { className: "max-w-sm text-xs text-muted-foreground", children: box === "inbox" ? "When someone asks to open something you own, it lands here \u2014 and in your messages." : "When you open something you don't have access to yet, you can ask the owner from there. Your request shows up here." })
|
|
2466
|
+
] });
|
|
2467
|
+
}
|
|
2468
|
+
function AccessRequestsSurface() {
|
|
2469
|
+
const ui = useAccessUi();
|
|
2470
|
+
const { toast, confirm } = ui;
|
|
2471
|
+
const NAV_ITEM_SELECTED = ui.navItemClasses.selected;
|
|
2472
|
+
const NAV_ITEM_UNSELECTED = ui.navItemClasses.unselected;
|
|
2473
|
+
const [box, setBox] = useState16(() => boxFromParam(ui.searchParam("box")));
|
|
2474
|
+
const currentUserId = ui.useCurrentUser().id;
|
|
2475
|
+
const [rows, setRows] = useState16({
|
|
2476
|
+
inbox: null,
|
|
2477
|
+
sent: null
|
|
2478
|
+
});
|
|
2479
|
+
const [error, setError] = useState16(null);
|
|
2480
|
+
const [busyId, setBusyId] = useState16(null);
|
|
2481
|
+
const [refreshing, setRefreshing] = useState16(false);
|
|
2482
|
+
const refresh = useCallback2(async () => {
|
|
2483
|
+
setRefreshing(true);
|
|
2484
|
+
const result = await fetchBoxes();
|
|
2485
|
+
setRows(result.rows);
|
|
2486
|
+
setError(result.error);
|
|
2487
|
+
setRefreshing(false);
|
|
2488
|
+
}, []);
|
|
2489
|
+
useEffect5(() => {
|
|
2490
|
+
let alive = true;
|
|
2491
|
+
void (async () => {
|
|
2492
|
+
const result = await fetchBoxes();
|
|
2493
|
+
if (!alive) return;
|
|
2494
|
+
setRows(result.rows);
|
|
2495
|
+
setError(result.error);
|
|
2496
|
+
})();
|
|
2497
|
+
return () => {
|
|
2498
|
+
alive = false;
|
|
2499
|
+
};
|
|
2500
|
+
}, []);
|
|
2501
|
+
const current = rows[box];
|
|
2502
|
+
const inboxCount = rows.inbox?.length ?? 0;
|
|
2503
|
+
const sentPending = rows.sent?.filter((r) => r.status === "pending").length ?? 0;
|
|
2504
|
+
async function run(id, work, done) {
|
|
2505
|
+
setBusyId(id);
|
|
2506
|
+
try {
|
|
2507
|
+
await work();
|
|
2508
|
+
toast.success(done);
|
|
2509
|
+
await refresh();
|
|
2510
|
+
} catch (e) {
|
|
2511
|
+
toast.error(e instanceof Error ? e.message : "We couldn't do that.");
|
|
2512
|
+
} finally {
|
|
2513
|
+
setBusyId(null);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function decide(row, level) {
|
|
2517
|
+
void run(
|
|
2518
|
+
row.id,
|
|
2519
|
+
async () => {
|
|
2520
|
+
await decideAccessRequest2({
|
|
2521
|
+
requestId: row.id,
|
|
2522
|
+
decision: "grant",
|
|
2523
|
+
level,
|
|
2524
|
+
currentUserId
|
|
2525
|
+
});
|
|
2526
|
+
},
|
|
2527
|
+
`Access granted${row.entityTitle ? ` to ${row.entityTitle}` : ""}.`
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
async function decline(row) {
|
|
2531
|
+
const ok = await confirm({
|
|
2532
|
+
title: "Decline this request?",
|
|
2533
|
+
description: "They'll be told you said no. They can ask again later if something changes.",
|
|
2534
|
+
confirmLabel: "Decline"
|
|
2535
|
+
});
|
|
2536
|
+
if (!ok) return;
|
|
2537
|
+
void run(
|
|
2538
|
+
row.id,
|
|
2539
|
+
async () => {
|
|
2540
|
+
await decideAccessRequest2({
|
|
2541
|
+
requestId: row.id,
|
|
2542
|
+
decision: "decline",
|
|
2543
|
+
currentUserId
|
|
2544
|
+
});
|
|
2545
|
+
},
|
|
2546
|
+
"Request declined."
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
async function report(row) {
|
|
2550
|
+
const ok = await confirm({
|
|
2551
|
+
title: "Report this request?",
|
|
2552
|
+
description: "This ends the conversation for good \u2014 they won't be able to ask about this again.",
|
|
2553
|
+
confirmLabel: "Report",
|
|
2554
|
+
variant: "destructive"
|
|
2555
|
+
});
|
|
2556
|
+
if (!ok) return;
|
|
2557
|
+
void run(
|
|
2558
|
+
row.id,
|
|
2559
|
+
async () => {
|
|
2560
|
+
await reportAccessRequest2(row.id);
|
|
2561
|
+
},
|
|
2562
|
+
"Reported. They can't ask about this again."
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
async function withdraw(row) {
|
|
2566
|
+
const ok = await confirm({
|
|
2567
|
+
title: "Withdraw your request?",
|
|
2568
|
+
description: "The owner will no longer see it waiting for an answer.",
|
|
2569
|
+
confirmLabel: "Withdraw"
|
|
2570
|
+
});
|
|
2571
|
+
if (!ok) return;
|
|
2572
|
+
void run(
|
|
2573
|
+
row.id,
|
|
2574
|
+
async () => {
|
|
2575
|
+
await withdrawAccessRequest2(row.id);
|
|
2576
|
+
},
|
|
2577
|
+
"Request withdrawn."
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
const tab = (target, label, count) => /* @__PURE__ */ jsxs15(
|
|
2581
|
+
"button",
|
|
2582
|
+
{
|
|
2583
|
+
type: "button",
|
|
2584
|
+
onClick: () => setBox(target),
|
|
2585
|
+
"aria-current": box === target ? "page" : void 0,
|
|
2586
|
+
className: cn3(
|
|
2587
|
+
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-3 py-1 text-xs transition-colors",
|
|
2588
|
+
box === target ? NAV_ITEM_SELECTED : NAV_ITEM_UNSELECTED
|
|
2589
|
+
),
|
|
2590
|
+
children: [
|
|
2591
|
+
target === "inbox" ? /* @__PURE__ */ jsx17(Inbox, { className: "h-3.5 w-3.5", "aria-hidden": true }) : /* @__PURE__ */ jsx17(Send5, { className: "h-3.5 w-3.5", "aria-hidden": true }),
|
|
2592
|
+
label,
|
|
2593
|
+
count > 0 && /* @__PURE__ */ jsx17(Badge, { className: "pointer-events-none h-4 min-w-[16px] border-0 bg-primary px-1 text-[9px] font-semibold text-primary-foreground hover:bg-primary", children: count > 99 ? "99+" : count })
|
|
2594
|
+
]
|
|
2595
|
+
},
|
|
2596
|
+
target
|
|
2597
|
+
);
|
|
2598
|
+
return /* @__PURE__ */ jsxs15("div", { className: "mx-auto w-full max-w-3xl", children: [
|
|
2599
|
+
/* @__PURE__ */ jsxs15("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
|
|
2600
|
+
/* @__PURE__ */ jsxs15("span", { className: "hidden items-center gap-1.5 text-sm font-medium sm:inline-flex", children: [
|
|
2601
|
+
/* @__PURE__ */ jsx17(KeyRound5, { className: "h-4 w-4", "aria-hidden": true }),
|
|
2602
|
+
"Access requests"
|
|
2603
|
+
] }),
|
|
2604
|
+
/* @__PURE__ */ jsxs15("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [
|
|
2605
|
+
/* @__PURE__ */ jsxs15("div", { className: "matrx-glass-thin-border inline-flex items-center gap-1 rounded-full p-1", children: [
|
|
2606
|
+
tab("inbox", "To me", inboxCount),
|
|
2607
|
+
tab("sent", "I sent", sentPending)
|
|
2608
|
+
] }),
|
|
2609
|
+
/* @__PURE__ */ jsx17(
|
|
2610
|
+
RefreshCwTapButton,
|
|
2611
|
+
{
|
|
2612
|
+
ariaLabel: "Refresh access requests",
|
|
2613
|
+
disabled: refreshing,
|
|
2614
|
+
onClick: () => void refresh()
|
|
2615
|
+
}
|
|
2616
|
+
)
|
|
2617
|
+
] })
|
|
2618
|
+
] }),
|
|
2619
|
+
/* @__PURE__ */ jsxs15("div", { children: [
|
|
2620
|
+
error && /* @__PURE__ */ jsxs15("div", { className: "mb-3 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive-ink", children: [
|
|
2621
|
+
error,
|
|
2622
|
+
ui.renderErrorMenu({ error })
|
|
2623
|
+
] }),
|
|
2624
|
+
error ? null : current === null ? /* @__PURE__ */ jsx17(LoadingRows, {}) : current.length === 0 ? /* @__PURE__ */ jsx17(EmptyState, { box }) : /* @__PURE__ */ jsx17("ul", { className: "space-y-2", children: current.map((row) => /* @__PURE__ */ jsx17(RowShell, { children: /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 flex-col gap-3", children: [
|
|
2625
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 flex-wrap items-start justify-between gap-2", children: [
|
|
2626
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 items-start gap-3", children: [
|
|
2627
|
+
box === "inbox" && ui.renderUserIdentity({
|
|
2628
|
+
user: {
|
|
2629
|
+
id: row.requester?.userId,
|
|
2630
|
+
displayName: row.requester?.displayName,
|
|
2631
|
+
avatarUrl: row.requester?.avatarUrl
|
|
2632
|
+
},
|
|
2633
|
+
size: "sm",
|
|
2634
|
+
subtitle: false,
|
|
2635
|
+
avatarOnly: true
|
|
2636
|
+
}),
|
|
2637
|
+
/* @__PURE__ */ jsxs15("div", { className: "min-w-0 space-y-1", children: [
|
|
2638
|
+
/* @__PURE__ */ jsxs15("p", { className: "text-sm text-foreground", children: [
|
|
2639
|
+
box === "inbox" ? /* @__PURE__ */ jsx17("span", { className: "font-medium", children: row.requester?.displayName ?? "Someone" }) : /* @__PURE__ */ jsx17("span", { className: "font-medium", children: "You" }),
|
|
2640
|
+
" ",
|
|
2641
|
+
row.requestKind === "setting" ? "asked for a setting change" : row.requestKind === "resource_action" ? "asked you to delete" : `asked to ${row.requestedLevel === "admin" ? "give full access to" : row.requestedLevel === "editor" ? "edit" : "view"}`
|
|
2642
|
+
] }),
|
|
2643
|
+
/* @__PURE__ */ jsx17(EntityCell, { row })
|
|
2644
|
+
] })
|
|
2645
|
+
] }),
|
|
2646
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
2647
|
+
box === "sent" && /* @__PURE__ */ jsx17(
|
|
2648
|
+
"span",
|
|
2649
|
+
{
|
|
2650
|
+
className: cn3(
|
|
2651
|
+
"rounded-full px-2 py-0.5 text-[11px] font-medium",
|
|
2652
|
+
STATUS_TONE[row.status]
|
|
2653
|
+
),
|
|
2654
|
+
children: statusLabel(row)
|
|
2655
|
+
}
|
|
2656
|
+
),
|
|
2657
|
+
row.createdAt && /* @__PURE__ */ jsx17(
|
|
2658
|
+
"span",
|
|
2659
|
+
{
|
|
2660
|
+
className: "text-xs tabular-nums text-muted-foreground",
|
|
2661
|
+
title: new Date(row.createdAt).toLocaleString(),
|
|
2662
|
+
children: formatRelativeTime(row.createdAt)
|
|
2663
|
+
}
|
|
2664
|
+
)
|
|
2665
|
+
] })
|
|
2666
|
+
] }),
|
|
2667
|
+
row.message && /* @__PURE__ */ jsx17("p", { className: "whitespace-pre-line rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground [overflow-wrap:anywhere]", children: row.message }),
|
|
2668
|
+
box === "sent" && row.decisionNote && /* @__PURE__ */ jsxs15("p", { className: "text-xs text-muted-foreground", children: [
|
|
2669
|
+
/* @__PURE__ */ jsx17("span", { className: "font-medium text-foreground", children: "Their note:" }),
|
|
2670
|
+
" ",
|
|
2671
|
+
row.decisionNote
|
|
2672
|
+
] }),
|
|
2673
|
+
box === "inbox" && row.requestKind === "resource_action" && row.resourceAction ? /* @__PURE__ */ jsx17(
|
|
2674
|
+
ResourceActionRequestButtons,
|
|
2675
|
+
{
|
|
2676
|
+
requestId: row.id,
|
|
2677
|
+
actionKey: row.resourceAction.actionKey,
|
|
2678
|
+
itemName: row.entityTitle ?? row.entityLabel ?? "this item",
|
|
2679
|
+
onDone: () => void refresh()
|
|
2680
|
+
}
|
|
2681
|
+
) : box === "inbox" && row.requestKind === "setting" && row.settingRequest ? /* @__PURE__ */ jsx17(
|
|
2682
|
+
SettingRequestActionButtons,
|
|
2683
|
+
{
|
|
2684
|
+
requestId: row.id,
|
|
2685
|
+
href: row.settingRequest.href,
|
|
2686
|
+
actionKey: row.settingRequest.actionKey,
|
|
2687
|
+
onDone: () => void refresh()
|
|
2688
|
+
}
|
|
2689
|
+
) : box === "inbox" ? /* @__PURE__ */ jsxs15("div", { className: "flex flex-wrap gap-2", children: [
|
|
2690
|
+
/* @__PURE__ */ jsx17(
|
|
2691
|
+
Button14,
|
|
2692
|
+
{
|
|
2693
|
+
icon: /* @__PURE__ */ jsx17(CircleCheck, { "aria-hidden": true }),
|
|
2694
|
+
variant: "primary",
|
|
2695
|
+
disabled: busyId === row.id,
|
|
2696
|
+
onClick: () => decide(row, "viewer"),
|
|
2697
|
+
children: "Let them view"
|
|
2698
|
+
}
|
|
2699
|
+
),
|
|
2700
|
+
/* @__PURE__ */ jsx17(
|
|
2701
|
+
Button14,
|
|
2702
|
+
{
|
|
2703
|
+
icon: /* @__PURE__ */ jsx17(PenLine, { "aria-hidden": true }),
|
|
2704
|
+
variant: "outline",
|
|
2705
|
+
disabled: busyId === row.id,
|
|
2706
|
+
onClick: () => decide(row, "editor"),
|
|
2707
|
+
children: "Let them edit"
|
|
2708
|
+
}
|
|
2709
|
+
),
|
|
2710
|
+
/* @__PURE__ */ jsx17(
|
|
2711
|
+
Button14,
|
|
2712
|
+
{
|
|
2713
|
+
icon: /* @__PURE__ */ jsx17(KeyRound5, { "aria-hidden": true }),
|
|
2714
|
+
variant: "outline",
|
|
2715
|
+
disabled: busyId === row.id,
|
|
2716
|
+
onClick: () => decide(row, "admin"),
|
|
2717
|
+
children: "Give full access"
|
|
2718
|
+
}
|
|
2719
|
+
),
|
|
2720
|
+
/* @__PURE__ */ jsx17(
|
|
2721
|
+
Button14,
|
|
2722
|
+
{
|
|
2723
|
+
icon: /* @__PURE__ */ jsx17(CircleSlash3, { "aria-hidden": true }),
|
|
2724
|
+
variant: "outline",
|
|
2725
|
+
disabled: busyId === row.id,
|
|
2726
|
+
onClick: () => void decline(row),
|
|
2727
|
+
children: "Decline"
|
|
2728
|
+
}
|
|
2729
|
+
),
|
|
2730
|
+
/* @__PURE__ */ jsx17(
|
|
2731
|
+
Button14,
|
|
2732
|
+
{
|
|
2733
|
+
icon: /* @__PURE__ */ jsx17(Flag2, { "aria-hidden": true }),
|
|
2734
|
+
variant: "quiet",
|
|
2735
|
+
disabled: busyId === row.id,
|
|
2736
|
+
onClick: () => void report(row),
|
|
2737
|
+
children: "Report"
|
|
2738
|
+
}
|
|
2739
|
+
)
|
|
2740
|
+
] }) : row.status === "pending" && /* @__PURE__ */ jsx17("div", { className: "flex flex-wrap gap-2", children: /* @__PURE__ */ jsx17(
|
|
2741
|
+
Button14,
|
|
2742
|
+
{
|
|
2743
|
+
icon: /* @__PURE__ */ jsx17(Undo22, { "aria-hidden": true }),
|
|
2744
|
+
variant: "outline",
|
|
2745
|
+
disabled: busyId === row.id,
|
|
2746
|
+
onClick: () => void withdraw(row),
|
|
2747
|
+
children: "Withdraw"
|
|
2748
|
+
}
|
|
2749
|
+
) })
|
|
2750
|
+
] }) }, row.id)) })
|
|
2751
|
+
] })
|
|
2752
|
+
] });
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// src/access/react/useAccessStates.ts
|
|
2756
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo2, useState as useState17 } from "react";
|
|
2757
|
+
import { fetchAccessDeniedContext as fetchAccessDeniedContext3 } from "@ai-matrx/data/access";
|
|
2758
|
+
var CACHE = /* @__PURE__ */ new Map();
|
|
2759
|
+
var INFLIGHT = /* @__PURE__ */ new Map();
|
|
2760
|
+
function cacheKey(token, id) {
|
|
2761
|
+
return `${token}:${id}`;
|
|
2762
|
+
}
|
|
2763
|
+
function resolveOne(token, id) {
|
|
2764
|
+
const key = cacheKey(token, id);
|
|
2765
|
+
const cached = CACHE.get(key);
|
|
2766
|
+
if (cached) return Promise.resolve(cached);
|
|
2767
|
+
const existing = INFLIGHT.get(key);
|
|
2768
|
+
if (existing) return existing;
|
|
2769
|
+
const promise = fetchAccessDeniedContext3(token, id).then((context) => {
|
|
2770
|
+
INFLIGHT.delete(key);
|
|
2771
|
+
if (context.status !== "error") CACHE.set(key, context);
|
|
2772
|
+
return context;
|
|
2773
|
+
});
|
|
2774
|
+
INFLIGHT.set(key, promise);
|
|
2775
|
+
return promise;
|
|
2776
|
+
}
|
|
2777
|
+
function useAccessStates(token, ids) {
|
|
2778
|
+
const [nonce, setNonce] = useState17(0);
|
|
2779
|
+
const [resolved, setResolved] = useState17(() => /* @__PURE__ */ new Map());
|
|
2780
|
+
const idKey = useMemo2(() => Array.from(new Set(ids)).sort().join(","), [ids]);
|
|
2781
|
+
useEffect6(() => {
|
|
2782
|
+
const wanted2 = idKey ? idKey.split(",") : [];
|
|
2783
|
+
if (wanted2.length === 0) return;
|
|
2784
|
+
let active = true;
|
|
2785
|
+
void Promise.all(
|
|
2786
|
+
wanted2.map(
|
|
2787
|
+
(id) => resolveOne(token, id).then(
|
|
2788
|
+
(context) => [id, context]
|
|
2789
|
+
)
|
|
2790
|
+
)
|
|
2791
|
+
).then((entries) => {
|
|
2792
|
+
if (active) setResolved(new Map(entries));
|
|
2793
|
+
});
|
|
2794
|
+
return () => {
|
|
2795
|
+
active = false;
|
|
2796
|
+
};
|
|
2797
|
+
}, [token, idKey, nonce]);
|
|
2798
|
+
const refresh = useCallback3(() => {
|
|
2799
|
+
for (const id of idKey ? idKey.split(",") : []) {
|
|
2800
|
+
CACHE.delete(cacheKey(token, id));
|
|
2801
|
+
}
|
|
2802
|
+
setNonce((n) => n + 1);
|
|
2803
|
+
}, [token, idKey]);
|
|
2804
|
+
const wanted = useMemo2(() => idKey ? idKey.split(",") : [], [idKey]);
|
|
2805
|
+
const states = useMemo2(() => {
|
|
2806
|
+
const out = /* @__PURE__ */ new Map();
|
|
2807
|
+
for (const id of wanted) {
|
|
2808
|
+
const context = resolved.get(id);
|
|
2809
|
+
if (context) out.set(id, context);
|
|
2810
|
+
}
|
|
2811
|
+
return out;
|
|
2812
|
+
}, [wanted, resolved]);
|
|
2813
|
+
return {
|
|
2814
|
+
states,
|
|
2815
|
+
isLoading: states.size < wanted.length,
|
|
2816
|
+
refresh
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// src/access/react/cantRemoveOpener.ts
|
|
2821
|
+
import { isGovernedActionDenial } from "@ai-matrx/data/access";
|
|
2822
|
+
import { fetchRemovalRights as fetchRemovalRights2 } from "@ai-matrx/data/access";
|
|
2823
|
+
var CANT_REMOVE_DIALOG = "cantRemoveDialog";
|
|
2824
|
+
async function requestCantRemove(request) {
|
|
2825
|
+
const { requestOverlay } = await import("@ai-matrx/summon/request");
|
|
2826
|
+
const ui = getAccessUi();
|
|
2827
|
+
return requestOverlay(CANT_REMOVE_DIALOG, request, {
|
|
2828
|
+
cancel: "closed",
|
|
2829
|
+
dedupeKey: `${request.token}:${request.id}`,
|
|
2830
|
+
open: ({ instanceId, data }) => ui.openOverlay(CANT_REMOVE_DIALOG, instanceId, data),
|
|
2831
|
+
close: ({ instanceId }) => ui.closeOverlay(CANT_REMOVE_DIALOG, instanceId)
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
var RemovalNotYoursError = class extends Error {
|
|
2835
|
+
token;
|
|
2836
|
+
id;
|
|
2837
|
+
constructor(token, id) {
|
|
2838
|
+
super("Removing this is up to its owner.");
|
|
2839
|
+
this.name = "RemovalNotYoursError";
|
|
2840
|
+
this.token = token;
|
|
2841
|
+
this.id = id;
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
function isRemovalNotYours(error) {
|
|
2845
|
+
return error instanceof RemovalNotYoursError || isGovernedActionDenial(error);
|
|
2846
|
+
}
|
|
2847
|
+
function openOnce(request) {
|
|
2848
|
+
void requestCantRemove(request);
|
|
2849
|
+
}
|
|
2850
|
+
function offerOwnerChoices(error, request) {
|
|
2851
|
+
if (!isRemovalNotYours(error)) return false;
|
|
2852
|
+
openOnce(request);
|
|
2853
|
+
return true;
|
|
2854
|
+
}
|
|
2855
|
+
async function ensureRemovable(request) {
|
|
2856
|
+
const rights = await fetchRemovalRights2(request.token, request.id).catch(() => null);
|
|
2857
|
+
if (rights?.canView && !rights.canRemove) {
|
|
2858
|
+
openOnce(request);
|
|
2859
|
+
return false;
|
|
2860
|
+
}
|
|
2861
|
+
return true;
|
|
2862
|
+
}
|
|
2863
|
+
export {
|
|
2864
|
+
ACCESS_REQUESTS_HREF,
|
|
2865
|
+
AccessDenied,
|
|
2866
|
+
AccessDeniedView,
|
|
2867
|
+
AccessGate,
|
|
2868
|
+
AccessLink,
|
|
2869
|
+
AccessRequestsSurface,
|
|
2870
|
+
AccessUiProvider,
|
|
2871
|
+
BlindAccessAsk,
|
|
2872
|
+
CANT_REMOVE_DIALOG,
|
|
2873
|
+
ForbiddenSurfaceView,
|
|
2874
|
+
GovernedActionDialog,
|
|
2875
|
+
HiddenByMeButton,
|
|
2876
|
+
HiddenFilter,
|
|
2877
|
+
REQUEST_ACCESS_MANUAL_ACTION,
|
|
2878
|
+
RemovalNotYoursError,
|
|
2879
|
+
RequestAccess,
|
|
2880
|
+
RequestAccessPanel,
|
|
2881
|
+
ResourceActionRequestButtons,
|
|
2882
|
+
ServerReadRecheck,
|
|
2883
|
+
SettingAccessGate,
|
|
2884
|
+
SettingRequestActionButtons,
|
|
2885
|
+
SlugAccessGate,
|
|
2886
|
+
UnresolvedEntityRef,
|
|
2887
|
+
configureAccessUi,
|
|
2888
|
+
ensureRemovable,
|
|
2889
|
+
getAccessUi,
|
|
2890
|
+
isAccessUiConfigured,
|
|
2891
|
+
isRemovalNotYours,
|
|
2892
|
+
offerOwnerChoices,
|
|
2893
|
+
refreshHiddenByMe,
|
|
2894
|
+
requestCantRemove,
|
|
2895
|
+
useAccessGate,
|
|
2896
|
+
useAccessStates,
|
|
2897
|
+
useAccessUi,
|
|
2898
|
+
useHiddenByMe,
|
|
2899
|
+
useHiddenItems,
|
|
2900
|
+
useHideForMe,
|
|
2901
|
+
useRemovalRights,
|
|
2902
|
+
useRequestAccess
|
|
2903
|
+
};
|
|
2904
|
+
//# sourceMappingURL=access-react.js.map
|