@protobase/ui 0.22.0 → 0.22.1
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/dist/app/pages/action-href.js +1 -1
- package/dist/app/pages/render-node.js +3 -1
- package/dist/app/pages/use-run-action.js +3 -3
- package/dist/invitations/invite-form.js +56 -0
- package/dist/invitations/link-notice.js +12 -0
- package/dist/invitations/pending-invitation-row.js +54 -0
- package/dist/lists/paged-search-list.js +42 -0
- package/dist/{organizations/use-all-organizations.js → lists/use-paged-search.js} +11 -10
- package/dist/organizations/all-organizations.js +24 -45
- package/dist/organizations/app-roles-picker.js +5 -2
- package/dist/organizations/invitation-page.js +59 -16
- package/dist/organizations/invitations-page.js +41 -116
- package/dist/organizations/members-page.js +40 -54
- package/dist/package.js +1 -1
- package/dist/people/person-row.js +26 -0
- package/dist/shell/assets/app-xO_xPXCq.js +69 -0
- package/dist/shell/assets/{index-DB2bNsqa.js → index-BvrIX29L.js} +1 -1
- package/dist/shell/assets/{maplibre-DRs-NHQ9.js → maplibre-DEGWdV6B.js} +1 -1
- package/dist/shell/assets/{markdown-content-obHEqNW8.js → markdown-content-DdB7hUqJ.js} +1 -1
- package/dist/shell/assets/{record-map-zJ2TD-WG.js → record-map-C_dz1z7c.js} +1 -1
- package/dist/shell/assets/{records-map-BVaFY5HA.js → records-map-ByiKJWnW.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DWzrRp3Z.js → shared-protobase-ui-Di_9bo0v.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/users/app-invitations.js +63 -0
- package/dist/users/create-user-form.js +86 -0
- package/dist/users/role-names.js +6 -0
- package/dist/users/use-user-roles.js +20 -0
- package/dist/users/user-list.js +34 -0
- package/dist/users/user-row.js +62 -0
- package/dist/users/users-block.js +30 -0
- package/package.json +4 -4
- package/dist/shell/assets/app-DFxg-FwV.js +0 -69
|
@@ -32,7 +32,7 @@ const holdsAll = (record, values) => Object.entries(values).every(([name, value]
|
|
|
32
32
|
*/
|
|
33
33
|
const actionFor = (action, record) => {
|
|
34
34
|
const run = action.run;
|
|
35
|
-
if (run?.kind === "update") return record && holdsAll(record, run.values) ? void 0 : action;
|
|
35
|
+
if (run?.kind === "update") return record && !action.inputs?.length && holdsAll(record, run.values) ? void 0 : action;
|
|
36
36
|
if (run?.kind !== "link" || !("href" in run)) return action;
|
|
37
37
|
const target = linkTarget(run.href, record);
|
|
38
38
|
if (target === void 0) return void 0;
|
|
@@ -16,6 +16,7 @@ import { RecordRowBlock } from "./blocks/record-row.js";
|
|
|
16
16
|
import { ShowBlock } from "./blocks/show.js";
|
|
17
17
|
import { StatBlock } from "./blocks/stat.js";
|
|
18
18
|
import { TableBlock } from "./blocks/table.js";
|
|
19
|
+
import { UsersBlock } from "../../users/users-block.js";
|
|
19
20
|
import { Fragment } from "react";
|
|
20
21
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
21
22
|
import { unmarkFieldRefs } from "@protobase/schema/internal";
|
|
@@ -36,7 +37,8 @@ const blocks = {
|
|
|
36
37
|
ModalForm: ModalFormBlock,
|
|
37
38
|
Action: ActionBlock,
|
|
38
39
|
Show: ShowBlock,
|
|
39
|
-
Link: LinkBlock
|
|
40
|
+
Link: LinkBlock,
|
|
41
|
+
Users: UsersBlock
|
|
40
42
|
};
|
|
41
43
|
const isInline = (child) => typeof child === "string" || !child.custom && child.type === "Link";
|
|
42
44
|
/** Text and links run together as one paragraph, like a sentence with a link in it; other elements stand alone. */
|
|
@@ -7,7 +7,7 @@ import { can } from "../permissions.js";
|
|
|
7
7
|
import { actionFor, linkTarget, openLink } from "./action-href.js";
|
|
8
8
|
import { useRefreshResource } from "./use-layout-data.js";
|
|
9
9
|
import { useState } from "react";
|
|
10
|
-
import { fillAddress } from "@protobase/schema/internal";
|
|
10
|
+
import { fillAddress, updateValues } from "@protobase/schema/internal";
|
|
11
11
|
//#region src/app/pages/use-run-action.ts
|
|
12
12
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
13
|
/**
|
|
@@ -15,7 +15,7 @@ const message = (error) => error instanceof Error ? error.message : "Something w
|
|
|
15
15
|
* (`update`, `delete`, `link`, and `ai`, which the server runs), else the handler the project registered under the
|
|
16
16
|
* action's name. An update whose values the record holds already is left out, and so is a link to a URL the record
|
|
17
17
|
* holds while it holds none. `blocked` says why one cannot run; `afterRun` hears about every action that succeeded.
|
|
18
|
-
* `submit` runs one with the answers to its inputs and throws its failure, for the dialog to show.
|
|
18
|
+
* `submit` runs one with the answers to its inputs (a handler gets them, an update writes them) and throws its failure, for the dialog to show.
|
|
19
19
|
*/
|
|
20
20
|
const useActionRunner = (resource, scope, afterRun) => {
|
|
21
21
|
const { views } = useAdminMeta();
|
|
@@ -45,7 +45,7 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
45
45
|
else openLink(linkTarget(run.href, scope?.record));
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
if (run?.kind === "update") await client.update(resource, scope.key, run.values, scope.etag ?? "*");
|
|
48
|
+
if (run?.kind === "update") await client.update(resource, scope.key, updateValues(run.values, values ?? {}), scope.etag ?? "*");
|
|
49
49
|
else if (run?.kind === "delete") await client.remove(resource, scope.key, { etag: scope.etag });
|
|
50
50
|
else if (run?.kind === "ai") await client.generate(resource, scope.key, action.name);
|
|
51
51
|
else await handlers[action.name]({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { AccountSection, SectionError } from "../account/account-section.js";
|
|
4
|
+
import { useAction } from "../organizations/use-action.js";
|
|
5
|
+
import { LinkNotice } from "./link-notice.js";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/invitations/invite-form.tsx
|
|
9
|
+
/** Inviting an address: the address, the controls for what the invitation gives, Invite, and the link without mail. */
|
|
10
|
+
const InviteForm = ({ title, description, onInvite, beside, children, incomplete = false }) => {
|
|
11
|
+
const [email, setEmail] = useState("");
|
|
12
|
+
const [link, setLink] = useState();
|
|
13
|
+
const { busy, error, run } = useAction();
|
|
14
|
+
const invite = () => run(async () => {
|
|
15
|
+
setLink((await onInvite(email.trim())).link);
|
|
16
|
+
setEmail("");
|
|
17
|
+
});
|
|
18
|
+
return /* @__PURE__ */ jsxs(AccountSection, {
|
|
19
|
+
title,
|
|
20
|
+
description,
|
|
21
|
+
children: [
|
|
22
|
+
/* @__PURE__ */ jsx(SectionError, { message: error }),
|
|
23
|
+
/* @__PURE__ */ jsxs("form", {
|
|
24
|
+
className: "flex flex-col gap-3",
|
|
25
|
+
onSubmit: (event) => {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
invite();
|
|
28
|
+
},
|
|
29
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: "flex flex-col gap-2 sm:flex-row",
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ jsx(Input, {
|
|
33
|
+
"aria-label": "Email",
|
|
34
|
+
type: "email",
|
|
35
|
+
placeholder: "name@example.com",
|
|
36
|
+
value: email,
|
|
37
|
+
onChange: (event) => setEmail(event.target.value),
|
|
38
|
+
wrapperClassName: "flex-1"
|
|
39
|
+
}),
|
|
40
|
+
beside,
|
|
41
|
+
/* @__PURE__ */ jsx(Button, {
|
|
42
|
+
type: "submit",
|
|
43
|
+
variant: "primary",
|
|
44
|
+
loading: busy,
|
|
45
|
+
disabled: !email.trim() || incomplete,
|
|
46
|
+
children: "Invite"
|
|
47
|
+
})
|
|
48
|
+
]
|
|
49
|
+
}), children]
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ jsx(LinkNotice, { link })
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { InviteForm };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
//#region src/invitations/link-notice.tsx
|
|
3
|
+
/** The invitation link to pass on, when the app cannot email it. */
|
|
4
|
+
const LinkNotice = ({ link }) => link ? /* @__PURE__ */ jsxs("p", {
|
|
5
|
+
className: "mt-3 break-all rounded-md border bg-muted px-3 py-2 text-xs",
|
|
6
|
+
children: ["This app cannot send email, so pass this link on yourself. It works once, until the invitation expires: ", /* @__PURE__ */ jsx("span", {
|
|
7
|
+
className: "font-mono",
|
|
8
|
+
children: link
|
|
9
|
+
})]
|
|
10
|
+
}) : null;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { LinkNotice };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { SectionError } from "../account/account-section.js";
|
|
3
|
+
import { useAction } from "../organizations/use-action.js";
|
|
4
|
+
import { LinkNotice } from "./link-notice.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/invitations/pending-invitation-row.tsx
|
|
8
|
+
/** An invitation not accepted yet: to whom, what it gives and until when, with Send again and Revoke. */
|
|
9
|
+
const PendingInvitationRow = ({ email, gives, expiresAt, onResend, onRevoke }) => {
|
|
10
|
+
const [link, setLink] = useState();
|
|
11
|
+
const { busy, error, run } = useAction();
|
|
12
|
+
const expired = new Date(expiresAt).getTime() < Date.now();
|
|
13
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
14
|
+
className: "px-3 py-2.5",
|
|
15
|
+
children: [
|
|
16
|
+
/* @__PURE__ */ jsxs("div", {
|
|
17
|
+
className: "flex flex-wrap items-center gap-2",
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ jsxs("span", {
|
|
20
|
+
className: "min-w-0 flex-1",
|
|
21
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
22
|
+
className: "block truncate font-medium",
|
|
23
|
+
children: email
|
|
24
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
25
|
+
className: "block text-xs text-muted-foreground",
|
|
26
|
+
children: [
|
|
27
|
+
gives,
|
|
28
|
+
" · ",
|
|
29
|
+
expired ? "Expired" : `Until ${new Date(expiresAt).toLocaleDateString()}`
|
|
30
|
+
]
|
|
31
|
+
})]
|
|
32
|
+
}),
|
|
33
|
+
/* @__PURE__ */ jsx(Button, {
|
|
34
|
+
size: "sm",
|
|
35
|
+
loading: busy,
|
|
36
|
+
onClick: () => void run(async () => setLink((await onResend()).link)),
|
|
37
|
+
children: "Send again"
|
|
38
|
+
}),
|
|
39
|
+
/* @__PURE__ */ jsx(Button, {
|
|
40
|
+
size: "sm",
|
|
41
|
+
variant: "danger",
|
|
42
|
+
loading: busy,
|
|
43
|
+
onClick: () => void run(onRevoke),
|
|
44
|
+
children: "Revoke"
|
|
45
|
+
})
|
|
46
|
+
]
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ jsx(SectionError, { message: error }),
|
|
49
|
+
/* @__PURE__ */ jsx(LinkNotice, { link })
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { PendingInvitationRow };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Input } from "../primitives/input.js";
|
|
2
|
+
import { Button } from "../primitives/button.js";
|
|
3
|
+
import { AccountSection, SectionError } from "../account/account-section.js";
|
|
4
|
+
import { Search } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/lists/paged-search-list.tsx
|
|
7
|
+
/** A section with a search box, how many of how many show, the rows found so far and "Show more" for the next page. */
|
|
8
|
+
const PagedSearchList = ({ title, description, searchLabel, placeholder, query, onQuery, list, error, renderRow }) => /* @__PURE__ */ jsxs(AccountSection, {
|
|
9
|
+
title,
|
|
10
|
+
description,
|
|
11
|
+
children: [
|
|
12
|
+
/* @__PURE__ */ jsx(SectionError, { message: error }),
|
|
13
|
+
/* @__PURE__ */ jsx(Input, {
|
|
14
|
+
"aria-label": searchLabel,
|
|
15
|
+
placeholder,
|
|
16
|
+
value: query,
|
|
17
|
+
onChange: (event) => onQuery(event.target.value),
|
|
18
|
+
leading: /* @__PURE__ */ jsx(Search, { className: "size-3.5" }),
|
|
19
|
+
wrapperClassName: "mb-2"
|
|
20
|
+
}),
|
|
21
|
+
/* @__PURE__ */ jsxs("p", {
|
|
22
|
+
className: "mb-3 text-xs text-muted-foreground",
|
|
23
|
+
children: [
|
|
24
|
+
list.found.length,
|
|
25
|
+
" of ",
|
|
26
|
+
list.total
|
|
27
|
+
]
|
|
28
|
+
}),
|
|
29
|
+
list.found.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
30
|
+
className: "divide-y rounded-md border",
|
|
31
|
+
children: list.found.map(renderRow)
|
|
32
|
+
}),
|
|
33
|
+
list.found.length < list.total && /* @__PURE__ */ jsx(Button, {
|
|
34
|
+
className: "mt-3",
|
|
35
|
+
loading: list.loading,
|
|
36
|
+
onClick: () => void list.more(),
|
|
37
|
+
children: "Show more"
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
//#endregion
|
|
42
|
+
export { PagedSearchList };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
//#region src/
|
|
2
|
+
//#region src/lists/use-paged-search.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* next page. A page that arrives after the text changed is dropped, so it never adds rows of the old filter.
|
|
4
|
+
* What `search` finds for `query`, a page at a time (searched 200 ms after the text stops changing). `more` adds the
|
|
5
|
+
* next page. A page that arrives after the text changed is dropped, so it never adds rows of the old filter. `search`
|
|
6
|
+
* must keep its identity between renders, or each render searches again.
|
|
6
7
|
*/
|
|
7
|
-
const
|
|
8
|
+
const usePagedSearch = (search, query) => {
|
|
8
9
|
const [found, setFound] = useState([]);
|
|
9
10
|
const [total, setTotal] = useState(0);
|
|
10
11
|
const [loading, setLoading] = useState(false);
|
|
@@ -13,9 +14,9 @@ const useAllOrganizations = (client, query) => {
|
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
const timer = setTimeout(() => {
|
|
15
16
|
setLoading(true);
|
|
16
|
-
|
|
17
|
+
search(query, 0).then((page) => {
|
|
17
18
|
if (latest.current !== query) return;
|
|
18
|
-
setFound(page.
|
|
19
|
+
setFound(page.items);
|
|
19
20
|
setTotal(page.total);
|
|
20
21
|
setLoading(false);
|
|
21
22
|
}, () => {
|
|
@@ -26,14 +27,14 @@ const useAllOrganizations = (client, query) => {
|
|
|
26
27
|
});
|
|
27
28
|
}, 200);
|
|
28
29
|
return () => clearTimeout(timer);
|
|
29
|
-
}, [
|
|
30
|
+
}, [search, query]);
|
|
30
31
|
const more = async () => {
|
|
31
32
|
const asked = query;
|
|
32
33
|
setLoading(true);
|
|
33
|
-
const page = await
|
|
34
|
+
const page = await search(asked, found.length).catch(() => void 0);
|
|
34
35
|
if (latest.current !== asked) return;
|
|
35
36
|
if (page) {
|
|
36
|
-
setFound((shown) => [...shown, ...page.
|
|
37
|
+
setFound((shown) => [...shown, ...page.items]);
|
|
37
38
|
setTotal(page.total);
|
|
38
39
|
}
|
|
39
40
|
setLoading(false);
|
|
@@ -46,4 +47,4 @@ const useAllOrganizations = (client, query) => {
|
|
|
46
47
|
};
|
|
47
48
|
};
|
|
48
49
|
//#endregion
|
|
49
|
-
export {
|
|
50
|
+
export { usePagedSearch };
|
|
@@ -1,61 +1,40 @@
|
|
|
1
|
-
import { Input } from "../primitives/input.js";
|
|
2
1
|
import { useAuth } from "../auth/auth-provider.js";
|
|
3
|
-
import { Button } from "../primitives/button.js";
|
|
4
|
-
import { AccountSection, SectionError } from "../account/account-section.js";
|
|
5
2
|
import { useAction } from "./use-action.js";
|
|
3
|
+
import { PagedSearchList } from "../lists/paged-search-list.js";
|
|
4
|
+
import { usePagedSearch } from "../lists/use-paged-search.js";
|
|
6
5
|
import { reopenIn } from "./use-organizations.js";
|
|
7
6
|
import { OrganizationRow } from "./organization-row.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { Search } from "lucide-react";
|
|
11
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useCallback, useState } from "react";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
12
9
|
//#region src/organizations/all-organizations.tsx
|
|
13
10
|
/** Every organization in the app for people with a global role, who may work in any of them. */
|
|
14
11
|
const AllOrganizations = ({ basePath, currentId }) => {
|
|
15
12
|
const { session } = useAuth();
|
|
16
13
|
const [query, setQuery] = useState("");
|
|
17
|
-
const
|
|
14
|
+
const search = useCallback((text, offset) => session.organizations.search(text, offset).then((page) => ({
|
|
15
|
+
items: page.organizations,
|
|
16
|
+
total: page.total
|
|
17
|
+
})), [session]);
|
|
18
|
+
const list = usePagedSearch(search, query);
|
|
18
19
|
const { busy, error, run } = useAction();
|
|
19
|
-
return /* @__PURE__ */
|
|
20
|
+
return /* @__PURE__ */ jsx(PagedSearchList, {
|
|
20
21
|
title: "All organizations",
|
|
21
22
|
description: "Every organization in the app. Your global roles let you work in any of them, with what those roles allow there. Opening one you are not a member of is written to the audit log.",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
found.length,
|
|
36
|
-
" of ",
|
|
37
|
-
total
|
|
38
|
-
]
|
|
39
|
-
}),
|
|
40
|
-
found.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
41
|
-
className: "divide-y rounded-md border",
|
|
42
|
-
children: found.map((organization) => /* @__PURE__ */ jsx(OrganizationRow, {
|
|
43
|
-
organization,
|
|
44
|
-
current: organization.id === currentId,
|
|
45
|
-
busy,
|
|
46
|
-
onOpen: () => void run(async () => {
|
|
47
|
-
await session.organizations.switchTo(organization.id);
|
|
48
|
-
reopenIn(basePath);
|
|
49
|
-
})
|
|
50
|
-
}, organization.id))
|
|
51
|
-
}),
|
|
52
|
-
found.length < total && /* @__PURE__ */ jsx(Button, {
|
|
53
|
-
className: "mt-3",
|
|
54
|
-
loading,
|
|
55
|
-
onClick: () => void more(),
|
|
56
|
-
children: "Show more"
|
|
23
|
+
searchLabel: "Find an organization",
|
|
24
|
+
placeholder: "Name or slug",
|
|
25
|
+
query,
|
|
26
|
+
onQuery: setQuery,
|
|
27
|
+
list,
|
|
28
|
+
error,
|
|
29
|
+
renderRow: (organization) => /* @__PURE__ */ jsx(OrganizationRow, {
|
|
30
|
+
organization,
|
|
31
|
+
current: organization.id === currentId,
|
|
32
|
+
busy,
|
|
33
|
+
onOpen: () => void run(async () => {
|
|
34
|
+
await session.organizations.switchTo(organization.id);
|
|
35
|
+
reopenIn(basePath);
|
|
57
36
|
})
|
|
58
|
-
|
|
37
|
+
}, organization.id)
|
|
59
38
|
});
|
|
60
39
|
};
|
|
61
40
|
//#endregion
|
|
@@ -2,10 +2,13 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { Checkbox } from "../primitives/checkbox.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/organizations/app-roles-picker.tsx
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The roles given, ticked when held; only those the person may give or take away can change. Organization pages pass
|
|
7
|
+
* the roles a member can hold (`membership`), the Users page every role.
|
|
8
|
+
*/
|
|
6
9
|
const AppRolesPicker = ({ roles, selected, grantable, disabled, onChange }) => /* @__PURE__ */ jsx("div", {
|
|
7
10
|
className: "flex flex-wrap gap-x-4 gap-y-2",
|
|
8
|
-
children: roles.
|
|
11
|
+
children: roles.map((role) => {
|
|
9
12
|
const held = selected.includes(role.name);
|
|
10
13
|
const locked = disabled || !grantable.has(role.name);
|
|
11
14
|
return /* @__PURE__ */ jsxs("label", {
|
|
@@ -10,9 +10,9 @@ import { organizationRoleLabels, roleLabel } from "./organization-paths.js";
|
|
|
10
10
|
import { useAction } from "./use-action.js";
|
|
11
11
|
import { useEffect, useState } from "react";
|
|
12
12
|
import { AuthError } from "@protobase/client";
|
|
13
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
//#region src/organizations/invitation-page.tsx
|
|
15
|
-
const SignUp = ({ token, preview, onDone }) => {
|
|
15
|
+
const SignUp = ({ token, preview, joins, onDone }) => {
|
|
16
16
|
const { session, signInMethods, recheck } = useAuth();
|
|
17
17
|
const [name, setName] = useState("");
|
|
18
18
|
const [password, setPassword] = useState("");
|
|
@@ -34,13 +34,9 @@ const SignUp = ({ token, preview, onDone }) => {
|
|
|
34
34
|
submit();
|
|
35
35
|
},
|
|
36
36
|
children: [
|
|
37
|
-
/* @__PURE__ */
|
|
37
|
+
/* @__PURE__ */ jsx("p", {
|
|
38
38
|
className: "text-muted-foreground",
|
|
39
|
-
children:
|
|
40
|
-
"Make your account for ",
|
|
41
|
-
preview.email,
|
|
42
|
-
" to join."
|
|
43
|
-
]
|
|
39
|
+
children: joins ? `Make your account for ${preview.email} to join.` : `Make your account for ${preview.email}.`
|
|
44
40
|
}),
|
|
45
41
|
error && /* @__PURE__ */ jsx("p", {
|
|
46
42
|
role: "alert",
|
|
@@ -67,14 +63,15 @@ const SignUp = ({ token, preview, onDone }) => {
|
|
|
67
63
|
variant: "primary",
|
|
68
64
|
loading: busy,
|
|
69
65
|
disabled: !name.trim() || withPassword && !passwordOptional && password.length < 12,
|
|
70
|
-
children: "Create account and join"
|
|
66
|
+
children: joins ? "Create account and join" : "Create your account"
|
|
71
67
|
})
|
|
72
68
|
]
|
|
73
69
|
});
|
|
74
70
|
};
|
|
75
71
|
/**
|
|
76
72
|
* Where an emailed invitation link leads: what it is for, then accepting it signed in as the invited address, signing
|
|
77
|
-
* in first, or making an account for an address without one.
|
|
73
|
+
* in first, or making an account for an address without one. An invitation to the app itself, rather than to an
|
|
74
|
+
* organization, only makes the account: someone with one just signs in.
|
|
78
75
|
*/
|
|
79
76
|
const InvitationPage = ({ token, onDone, signIn }) => {
|
|
80
77
|
const { session, state, signOut, organizations } = useAuth();
|
|
@@ -100,11 +97,56 @@ const InvitationPage = ({ token, onDone, signIn }) => {
|
|
|
100
97
|
children: [/* @__PURE__ */ jsx(Spinner, {}), " Loading"]
|
|
101
98
|
})
|
|
102
99
|
});
|
|
103
|
-
const roles = preview.appRoles.map((name) => roleLabel(organizations?.roles ?? [], name));
|
|
104
|
-
const description = `${preview.inviter.name} invites you to ${preview.organization.name} as ${organizationRoleLabels[preview.role].toLowerCase()}${roles.length > 0 ? `, with ${roles.join(", ")}` : ""}.`;
|
|
105
100
|
const user = state.kind === "signed-in" ? state.user : void 0;
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
const sameAddress = user?.email.toLowerCase() === preview.email.toLowerCase();
|
|
102
|
+
if (!preview.organization) {
|
|
103
|
+
const app = preview.app?.name ?? "";
|
|
104
|
+
const description = `${preview.inviter.name} invites you to join ${app}.`;
|
|
105
|
+
return /* @__PURE__ */ jsx(AuthLayout, {
|
|
106
|
+
title: `Join ${app}`,
|
|
107
|
+
description,
|
|
108
|
+
children: preview.hasAccount ? user && sameAddress ? /* @__PURE__ */ jsx(Button, {
|
|
109
|
+
variant: "primary",
|
|
110
|
+
className: "w-full",
|
|
111
|
+
onClick: onDone,
|
|
112
|
+
children: "Go to the app"
|
|
113
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("p", {
|
|
114
|
+
className: "mb-3 text-muted-foreground",
|
|
115
|
+
children: [preview.email, " has an account already: sign in with it."]
|
|
116
|
+
}), user ? /* @__PURE__ */ jsx(Button, {
|
|
117
|
+
className: "w-full",
|
|
118
|
+
onClick: () => void signOut(),
|
|
119
|
+
children: "Sign out"
|
|
120
|
+
}) : /* @__PURE__ */ jsxs(RouteLink, {
|
|
121
|
+
to: signIn,
|
|
122
|
+
className: cn(buttonClass({ variant: "primary" }), "w-full"),
|
|
123
|
+
children: ["Sign in as ", preview.email]
|
|
124
|
+
})] }) : user ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("p", {
|
|
125
|
+
className: "mb-3 text-muted-foreground",
|
|
126
|
+
children: [
|
|
127
|
+
"This invitation is for ",
|
|
128
|
+
preview.email,
|
|
129
|
+
", and you are signed in as ",
|
|
130
|
+
user.email,
|
|
131
|
+
"."
|
|
132
|
+
]
|
|
133
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
134
|
+
className: "w-full",
|
|
135
|
+
onClick: () => void signOut(),
|
|
136
|
+
children: "Sign out to accept it"
|
|
137
|
+
})] }) : /* @__PURE__ */ jsx(SignUp, {
|
|
138
|
+
token,
|
|
139
|
+
preview,
|
|
140
|
+
joins: false,
|
|
141
|
+
onDone
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const organization = preview.organization;
|
|
146
|
+
const roles = preview.appRoles.map((name) => roleLabel(organizations?.roles ?? [], name));
|
|
147
|
+
const description = `${preview.inviter.name} invites you to ${organization.name} as ${organizationRoleLabels[preview.role].toLowerCase()}${roles.length > 0 ? `, with ${roles.join(", ")}` : ""}.`;
|
|
148
|
+
if (user && !sameAddress) return /* @__PURE__ */ jsxs(AuthLayout, {
|
|
149
|
+
title: `Join ${organization.name}`,
|
|
108
150
|
description,
|
|
109
151
|
children: [/* @__PURE__ */ jsxs("p", {
|
|
110
152
|
className: "mb-3 text-muted-foreground",
|
|
@@ -122,7 +164,7 @@ const InvitationPage = ({ token, onDone, signIn }) => {
|
|
|
122
164
|
})]
|
|
123
165
|
});
|
|
124
166
|
return /* @__PURE__ */ jsx(AuthLayout, {
|
|
125
|
-
title: `Join ${
|
|
167
|
+
title: `Join ${organization.name}`,
|
|
126
168
|
description,
|
|
127
169
|
children: user ? /* @__PURE__ */ jsxs("div", {
|
|
128
170
|
className: "flex flex-col gap-3",
|
|
@@ -138,7 +180,7 @@ const InvitationPage = ({ token, onDone, signIn }) => {
|
|
|
138
180
|
await session.organizations.accept(token);
|
|
139
181
|
onDone();
|
|
140
182
|
}),
|
|
141
|
-
children: ["Join ",
|
|
183
|
+
children: ["Join ", organization.name]
|
|
142
184
|
})]
|
|
143
185
|
}) : preview.hasAccount ? /* @__PURE__ */ jsxs(RouteLink, {
|
|
144
186
|
to: signIn,
|
|
@@ -151,6 +193,7 @@ const InvitationPage = ({ token, onDone, signIn }) => {
|
|
|
151
193
|
}) : /* @__PURE__ */ jsx(SignUp, {
|
|
152
194
|
token,
|
|
153
195
|
preview,
|
|
196
|
+
joins: true,
|
|
154
197
|
onDone
|
|
155
198
|
})
|
|
156
199
|
});
|