@protobase/ui 0.60.1 → 0.61.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/dist/app/app.js +7 -3
- package/dist/app/marks/mark-people.js +24 -0
- package/dist/app/pages/merge-ui.d.ts +2 -1
- package/dist/app/pages/merge-ui.js +4 -1
- package/dist/app/pages/project-ui.d.ts +7 -1
- package/dist/app/use-nav-recent.js +7 -2
- package/dist/app-shell/nav.d.ts +4 -1
- package/dist/app-shell/record-marks.js +21 -2
- package/dist/app-shell/sidebar-records.js +7 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/organizations/members-page.js +11 -5
- package/dist/package.js +1 -1
- package/dist/presence/presence-dot.js +17 -0
- package/dist/presence/presence-provider.js +36 -0
- package/dist/presence/presence.d.ts +14 -0
- package/dist/presence/presence.js +31 -0
- package/dist/shell/assets/{app-DTBU_zpR.js → app-kB6jRU7T.js} +7 -7
- package/dist/shell/assets/{index-Jqqke1wV.js → index-BWyRAop3.js} +2 -2
- package/dist/shell/assets/index-Iii7afo2.css +2 -0
- package/dist/shell/assets/{records-map-BbFggRwi.js → records-map-C6-E01TL.js} +2 -2
- package/dist/shell/assets/shared-protobase-ui-Bsuq-ac2.js +2 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +2 -2
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-kB6jRU7T.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-Jqqke1wV.js.map → index-BWyRAop3.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-BbFggRwi.js.map → records-map-C6-E01TL.js.map} +1 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-Bsuq-ac2.js.map +1 -0
- package/dist/status/messages.js +6 -2
- package/dist/status/status-mark.d.ts +4 -4
- package/dist/status/status-mark.js +3 -2
- package/package.json +4 -4
- package/dist/shell/assets/index-D0sx7n9i.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-BgSucvf-.js +0 -2
- package/dist/shell-source-maps/assets/app-DTBU_zpR.js.map +0 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-BgSucvf-.js.map +0 -1
package/dist/app/app.js
CHANGED
|
@@ -41,8 +41,10 @@ import { CreateRoute } from "./create/create-route.js";
|
|
|
41
41
|
import { userMenuFromMeta } from "./user-menu-from-meta.js";
|
|
42
42
|
import { navFromMeta, navItemId } from "./nav-from-meta.js";
|
|
43
43
|
import { ListPage } from "./list-page.js";
|
|
44
|
+
import { usePresence } from "../presence/presence.js";
|
|
44
45
|
import { MembersPage } from "../organizations/members-page.js";
|
|
45
46
|
import { InvitationsPage } from "../organizations/invitations-page.js";
|
|
47
|
+
import { PresenceProvider } from "../presence/presence-provider.js";
|
|
46
48
|
import { reopenIn, useOrganizations } from "../organizations/use-organizations.js";
|
|
47
49
|
import { OrganizationPage } from "../organizations/organization-page.js";
|
|
48
50
|
import { OrganizationsPage } from "../organizations/organizations-page.js";
|
|
@@ -118,6 +120,7 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
118
120
|
const bell = meta.bell && user && !staff && /* @__PURE__ */ jsx(ShellBell, { own: shell?.bell });
|
|
119
121
|
const status = useStatusMenu(Boolean(meta.status && user && !staff));
|
|
120
122
|
const statuses = useStatuses().data;
|
|
123
|
+
const presence = usePresence();
|
|
121
124
|
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
122
125
|
const assistantUrl = meta.assistant?.url;
|
|
123
126
|
const assistantButton = assistantUrl && /* @__PURE__ */ jsx(AssistantButton, {
|
|
@@ -193,7 +196,8 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
|
|
|
193
196
|
children: text("onlyOwners")
|
|
194
197
|
}) : path === "/-/organization/members" ? /* @__PURE__ */ jsx(MembersPage, {
|
|
195
198
|
current,
|
|
196
|
-
statuses
|
|
199
|
+
statuses,
|
|
200
|
+
presence
|
|
197
201
|
}) : /* @__PURE__ */ jsx(InvitationsPage, { current }) : organizationPage && path === "/-/organizations" || outsideOrganizations ? /* @__PURE__ */ jsx(OrganizationsPage, {
|
|
198
202
|
state: organizations,
|
|
199
203
|
basePath
|
|
@@ -302,12 +306,12 @@ const Authenticated = ({ baseUrl, client, sources, workspace, sidebarMode, ui, a
|
|
|
302
306
|
onUnauthenticated: markSignedOut,
|
|
303
307
|
children: /* @__PURE__ */ jsx(MetaGate, { children: /* @__PURE__ */ jsxs(ToastProvider, { children: [!sidebarBanner && /* @__PURE__ */ jsx(NewVersionToast, {}), path === connectRoute ? /* @__PURE__ */ jsx(ConnectRoute, {}) : /* @__PURE__ */ jsx(PluginGate, {
|
|
304
308
|
ui,
|
|
305
|
-
children: /* @__PURE__ */ jsx(RecordTitles, { children: /* @__PURE__ */ jsx(Routes, {
|
|
309
|
+
children: /* @__PURE__ */ jsx(PresenceProvider, { children: /* @__PURE__ */ jsx(RecordTitles, { children: /* @__PURE__ */ jsx(Routes, {
|
|
306
310
|
workspace,
|
|
307
311
|
sidebarMode,
|
|
308
312
|
assistant,
|
|
309
313
|
runtime
|
|
310
|
-
}) })
|
|
314
|
+
}) }) })
|
|
311
315
|
})] }) })
|
|
312
316
|
});
|
|
313
317
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { presenceOf } from "../../presence/presence.js";
|
|
2
|
+
//#region src/app/marks/mark-people.ts
|
|
3
|
+
/**
|
|
4
|
+
* The marks as the sidebar draws them: each mark that names a `person` carries that person's presence and status, by
|
|
5
|
+
* their user id, each left out when the app gives none for them.
|
|
6
|
+
*/
|
|
7
|
+
const markPeople = (marks, presence, statuses) => {
|
|
8
|
+
if (!marks) return;
|
|
9
|
+
const statusOf = new Map((statuses ?? []).map((status) => [status.userId, status]));
|
|
10
|
+
return Object.fromEntries(Object.entries(marks).map(([key, { person, ...mark }]) => {
|
|
11
|
+
if (person === void 0) return [key, mark];
|
|
12
|
+
const status = statusOf.get(person);
|
|
13
|
+
const shown = presenceOf(presence, person);
|
|
14
|
+
return [key, {
|
|
15
|
+
...mark,
|
|
16
|
+
person: {
|
|
17
|
+
...status && { status },
|
|
18
|
+
...shown && { presence: shown }
|
|
19
|
+
}
|
|
20
|
+
}];
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { markPeople };
|
|
@@ -3,7 +3,8 @@ import { ProjectUi } from "./project-ui.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* One UI config from several, each after the ones it `extends`, the last being the app's own: components, action
|
|
5
5
|
* handlers, field types, drop rules, board drops, title rules and marks hooks are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
6
|
-
* and `beforeSignIn` component is drawn, the extended configs' first, except the logo and the bell, the last config's to set one
|
|
6
|
+
* and `beforeSignIn` component is drawn, the extended configs' first, except the logo and the bell, the last config's to set one,
|
|
7
|
+
* as is the presence hook.
|
|
7
8
|
*/
|
|
8
9
|
export declare const mergeUi: (...uis: ProjectUi[]) => ProjectUi;
|
|
9
10
|
//#endregion
|
|
@@ -36,7 +36,8 @@ const around = (wrappers) => {
|
|
|
36
36
|
/**
|
|
37
37
|
* One UI config from several, each after the ones it `extends`, the last being the app's own: components, action
|
|
38
38
|
* handlers, field types, drop rules, board drops, title rules and marks hooks are all kept, and one name defined by two configs is an error naming both; every shell slot
|
|
39
|
-
* and `beforeSignIn` component is drawn, the extended configs' first, except the logo and the bell, the last config's to set one
|
|
39
|
+
* and `beforeSignIn` component is drawn, the extended configs' first, except the logo and the bell, the last config's to set one,
|
|
40
|
+
* as is the presence hook.
|
|
40
41
|
*/
|
|
41
42
|
const mergeUi = (...uis) => {
|
|
42
43
|
const named = uis.flatMap((ui, index) => flatten(ui, ui.name ?? `UI config ${index + 1}`));
|
|
@@ -51,6 +52,7 @@ const mergeUi = (...uis) => {
|
|
|
51
52
|
const rightPanel = slot(named.flatMap(({ ui }) => ui.shell?.rightPanel ? [ui.shell.rightPanel] : []));
|
|
52
53
|
const logo = named.findLast(({ ui }) => ui.shell?.logo)?.ui.shell?.logo;
|
|
53
54
|
const bell = named.findLast(({ ui }) => ui.shell?.bell)?.ui.shell?.bell;
|
|
55
|
+
const presence = named.findLast(({ ui }) => ui.presence)?.ui.presence;
|
|
54
56
|
const beforeSignIn = around(named.flatMap(({ ui }) => ui.beforeSignIn ? [ui.beforeSignIn] : []));
|
|
55
57
|
const name = uis.at(-1)?.name;
|
|
56
58
|
return {
|
|
@@ -68,6 +70,7 @@ const mergeUi = (...uis) => {
|
|
|
68
70
|
...logo && { logo },
|
|
69
71
|
...bell && { bell }
|
|
70
72
|
} },
|
|
73
|
+
...presence && { presence },
|
|
71
74
|
...beforeSignIn && { beforeSignIn }
|
|
72
75
|
};
|
|
73
76
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PresenceHook } from "../../presence/presence.js";
|
|
1
2
|
import { ShellLogoProps } from "../../app-shell/logo.js";
|
|
2
3
|
import { FieldWidget } from "../field-types/field-widget.js";
|
|
3
4
|
import { BoardDrop } from "../displays/board-drop.js";
|
|
@@ -48,7 +49,7 @@ export type BeforeSignInProps = {
|
|
|
48
49
|
* The project's own React code for composed pages: custom components by the name their layout declares
|
|
49
50
|
* (`component('UsageChart')`) or a widget part names, handlers for named actions that have no built-in behaviour,
|
|
50
51
|
* field types by the name a view's `.widget(name)` chooses, drop rules by the name a list's `drag.rule` gives, board
|
|
51
|
-
* drops by the name a list's `drag.board` gives, title rules by the name a view's `.titleFrom` gives, marks hooks by the name a view's `nav.recent.marks` gives, and the shell's slots. `extends` lists UI configs merged in before it (see `mergeUi`), or whole `protobase.ui.tsx` modules
|
|
52
|
+
* drops by the name a list's `drag.board` gives, title rules by the name a view's `.titleFrom` gives, marks hooks by the name a view's `nav.recent.marks` gives, the app's presence, and the shell's slots. `extends` lists UI configs merged in before it (see `mergeUi`), or whole `protobase.ui.tsx` modules
|
|
52
53
|
* (`import * as tasksUi from '...'`), their exported components included; `name` names it in their errors.
|
|
53
54
|
*/
|
|
54
55
|
export type ProjectUi = {
|
|
@@ -66,6 +67,11 @@ export type ProjectUi = {
|
|
|
66
67
|
titles?: Record<string, TitleRule>;
|
|
67
68
|
/** How the sidebar marks a resource's records for the person reading, by the name a view's `nav.recent.marks` gives (see `MarksHook`). */
|
|
68
69
|
marks?: Record<string, MarksHook>;
|
|
70
|
+
/**
|
|
71
|
+
* Who is online or away, by user id, from the app's own presence (see `PresenceHook`): the members page and the
|
|
72
|
+
* sidebar rows whose mark names a `person` show it beside each person's status.
|
|
73
|
+
*/
|
|
74
|
+
presence?: PresenceHook;
|
|
69
75
|
/**
|
|
70
76
|
* Rendered in the shell on every page: `actions` at the end of the top bar, `rightPanel` beside the page, `logo`
|
|
71
77
|
* at the top of the sidebar in place of Protobase's (see `ShellLogoProps`), and `bell` in the top bar in place of the
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { keys } from "../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../data/api-provider.js";
|
|
3
|
+
import { useStatuses } from "../status/use-statuses.js";
|
|
3
4
|
import { useProjectUi } from "./pages/project-ui.js";
|
|
4
5
|
import { useRecordTitle } from "./titles/record-titles.js";
|
|
5
6
|
import { userMenuResources } from "./user-menu-from-meta.js";
|
|
7
|
+
import { usePresence } from "../presence/presence.js";
|
|
8
|
+
import { markPeople } from "./marks/mark-people.js";
|
|
6
9
|
import { recentMarks, recentRecords, recentRequest, recentRowDiffers } from "./nav-recent.js";
|
|
7
10
|
import { useEffect, useMemo } from "react";
|
|
8
11
|
import { useQueries, useQueryClient } from "@tanstack/react-query";
|
|
9
12
|
//#region src/app/use-nav-recent.ts
|
|
13
|
+
const withPeople = (useMarks) => useMarks && (() => markPeople(useMarks(), usePresence(), useStatuses().data));
|
|
10
14
|
const groupKey = ({ model, request }) => keys.navRecent(model.name, request.filter, request.orderBy, request.pageSize);
|
|
11
15
|
/**
|
|
12
16
|
* Refetches a group when a record of its resource is read anew (a record page, or an app following one) and shows
|
|
@@ -32,7 +36,8 @@ const useFollowRecords = (groups) => {
|
|
|
32
36
|
/**
|
|
33
37
|
* The records of every sidebar group (`nav.recent`), one list request each, under the `list` query key so that any
|
|
34
38
|
* write to the resource refreshes them, as does reading one of their records anew. The server applies the caller's
|
|
35
|
-
* access, as on the list page. A group that names `marks` gets the project UI's hook, which the sidebar calls
|
|
39
|
+
* access, as on the list page. A group that names `marks` gets the project UI's hook, which the sidebar calls, its
|
|
40
|
+
* marks' people given their presence and status.
|
|
36
41
|
*/
|
|
37
42
|
const useNavRecent = (meta, basePath, active) => {
|
|
38
43
|
const client = useClient();
|
|
@@ -48,7 +53,7 @@ const useNavRecent = (meta, basePath, active) => {
|
|
|
48
53
|
view,
|
|
49
54
|
recent,
|
|
50
55
|
request: recentRequest(model, view, recent),
|
|
51
|
-
useMarks: recentMarks(model.name, recent, marks)
|
|
56
|
+
useMarks: withPeople(recentMarks(model.name, recent, marks))
|
|
52
57
|
}] : [];
|
|
53
58
|
});
|
|
54
59
|
}, [meta, marks]);
|
package/dist/app-shell/nav.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import "../status/status-mark.js";
|
|
1
2
|
import "lucide-react";
|
|
2
3
|
import "@protobase/schema";
|
|
3
4
|
//#region src/app-shell/nav.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* How a record under a sidebar entry stands out for the person reading, such as an unread conversation: `strong` draws
|
|
6
7
|
* its title bold, `count` a badge at the end of its row (none at 0), and `label`, such as "3 mentions", is added to its
|
|
7
|
-
* name for screen readers and its tooltip.
|
|
8
|
+
* name for screen readers and its tooltip. `person`, a user id, names the one person the record stands for, such as
|
|
9
|
+
* the other person in a direct message: the row shows their presence and status beside its title.
|
|
8
10
|
*/
|
|
9
11
|
export type RecordMark = {
|
|
10
12
|
strong?: boolean;
|
|
11
13
|
count?: number;
|
|
12
14
|
label?: string;
|
|
15
|
+
person?: string;
|
|
13
16
|
};
|
|
14
17
|
//#endregion
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { text } from "../status/messages.js";
|
|
2
|
+
import { availabilityLabel } from "../status/status-mark.js";
|
|
3
|
+
import { heldStatus } from "@protobase/schema/internal";
|
|
1
4
|
//#region src/app-shell/record-marks.ts
|
|
2
5
|
const markRecords = (records, marks) => records.map((record) => {
|
|
3
6
|
const mark = marks && Object.hasOwn(marks, record.id) ? marks[record.id] : void 0;
|
|
@@ -6,9 +9,25 @@ const markRecords = (records, marks) => records.map((record) => {
|
|
|
6
9
|
mark
|
|
7
10
|
} : record;
|
|
8
11
|
});
|
|
9
|
-
|
|
12
|
+
const personWords = (person) => {
|
|
13
|
+
if (!person) return [];
|
|
14
|
+
const held = heldStatus(person.status);
|
|
15
|
+
return [
|
|
16
|
+
person.presence && text(person.presence),
|
|
17
|
+
held && held.availability !== "automatic" ? availabilityLabel(held.availability) : void 0,
|
|
18
|
+
held?.text ?? void 0
|
|
19
|
+
];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* What a sidebar record is called for screen readers and in its tooltip: its title, with its status, its person's
|
|
23
|
+
* presence and status, and its mark's label.
|
|
24
|
+
*/
|
|
10
25
|
const recordNames = (record) => {
|
|
11
|
-
const extra = [
|
|
26
|
+
const extra = [
|
|
27
|
+
record.status?.label,
|
|
28
|
+
...personWords(record.mark?.person),
|
|
29
|
+
record.mark?.label
|
|
30
|
+
].filter((part) => part !== void 0 && part !== "");
|
|
12
31
|
return {
|
|
13
32
|
ariaLabel: [record.label, ...extra].join(", "),
|
|
14
33
|
title: extra.length === 0 ? record.label : `${record.label} (${extra.join(", ")})`
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { subRow } from "./sidebar-rows.js";
|
|
3
3
|
import { CountBadge } from "../primitives/count-badge.js";
|
|
4
|
-
import { markRecords, recordNames } from "./record-marks.js";
|
|
5
4
|
import { StatusDot } from "./status-dot.js";
|
|
5
|
+
import { StatusMark } from "../status/status-mark.js";
|
|
6
|
+
import { markRecords, recordNames } from "./record-marks.js";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/app-shell/sidebar-records.tsx
|
|
8
9
|
const RecordRows = ({ records, mode, onNavigate }) => records.map((record) => {
|
|
@@ -28,6 +29,11 @@ const RecordRows = ({ records, mode, onNavigate }) => records.map((record) => {
|
|
|
28
29
|
className: "truncate",
|
|
29
30
|
children: record.label
|
|
30
31
|
}),
|
|
32
|
+
record.mark?.person && /* @__PURE__ */ jsx(StatusMark, {
|
|
33
|
+
...record.mark.person,
|
|
34
|
+
textShown: "hover",
|
|
35
|
+
className: "ml-1.5 shrink-0"
|
|
36
|
+
}),
|
|
31
37
|
/* @__PURE__ */ jsx(CountBadge, {
|
|
32
38
|
count: record.mark?.count,
|
|
33
39
|
className: "ml-auto shrink-0"
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Presence, PresenceHook, usePresence } from "./presence/presence.js";
|
|
2
|
+
import { AvailabilityMark, StatusMark, StatusMarkProps } from "./status/status-mark.js";
|
|
1
3
|
import { RecordMark } from "./app-shell/nav.js";
|
|
2
4
|
import { LogoMark, ShellLogoProps } from "./app-shell/logo.js";
|
|
3
5
|
import { FieldWidget, FieldWidgetEditorProps, FieldWidgetProps } from "./app/field-types/field-widget.js";
|
|
@@ -31,7 +33,6 @@ import { useApiToken } from "./auth/use-api-token.js";
|
|
|
31
33
|
import { PushState } from "./notifications/push/web-push.js";
|
|
32
34
|
import { SignedInUser, useSignedInUser } from "./auth/use-signed-in-user.js";
|
|
33
35
|
import { Member, MembersResult, useMembers } from "./people/use-members.js";
|
|
34
|
-
import { AvailabilityMark, StatusMark, StatusMarkProps } from "./status/status-mark.js";
|
|
35
36
|
import { statusRefreshMs, useOwnStatus, useStatuses } from "./status/use-statuses.js";
|
|
36
37
|
import { usePushNotifications } from "./notifications/push/use-push-notifications.js";
|
|
37
38
|
import { useInbox, useMarkNotificationsRead, useUnreadNotifications } from "./notifications/bell/use-inbox.js";
|
|
@@ -86,4 +87,4 @@ import { Section } from "./record-view/section.js";
|
|
|
86
87
|
import { Form, FormProps } from "./app/create/form.js";
|
|
87
88
|
import { RecordPicker, RecordPickerProps } from "./app/record-picker.js";
|
|
88
89
|
import { Spinner } from "./primitives/spinner.js";
|
|
89
|
-
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, AvailabilityMark, type BackendData, type BackendRefresh, type BeforeSignInProps, type BoardDrop, type BoardDropContext, type BoardDropUndo, type BoardDropWrite, Button, type ButtonProps, type CardAction, type CardTone, Carousel, type CarouselImage, type CarouselProps, type ChatAuthor, ChatDivider, type ChatDividerProps, ChatMessage, type ChatMessageProps, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, type Drop, type DropRule, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, Markdown, type MarkdownContentProps as MarkdownProps, type MarksHook, type Member, type MembersResult, MentionBox, type MentionBoxProps, type MentionDraft, MentionedText, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PeopleSearch, type PickedUser, type PluginInfo, type ProjectUi, type PushState, type RecordCardProps, RecordLink, type RecordMark, RecordPicker, type RecordPickerProps, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type SignedInUser, Spinner, StatusMark, type StatusMarkProps, type Step, StepList, type StepState, Textarea, type TextareaProps, type TitleReader, type TitleRule, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, statusRefreshMs, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, useOwnStatus, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useSignedInUser, useStatuses, useUnreadNotifications, useUpdateRecord };
|
|
90
|
+
export { ActionCard, type ActionCardProps, type ActionContext, type ActionHandler, ActionInputError, type ActionInputProps, ApiProvider, type ApiProviderProps, App, AppLink, type AppProps, AssistantDock, type AssistantDockProps, AuthLayout, type AuthLayoutProps, AvailabilityMark, type BackendData, type BackendRefresh, type BeforeSignInProps, type BoardDrop, type BoardDropContext, type BoardDropUndo, type BoardDropWrite, Button, type ButtonProps, type CardAction, type CardTone, Carousel, type CarouselImage, type CarouselProps, type ChatAuthor, ChatDivider, type ChatDividerProps, ChatMessage, type ChatMessageProps, ChatThread, type ChatThreadProps, type ChoiceOption, Choices, type ChoicesProps, CodeForm, type CodeFormProps, CompactTable, type CompactTableProps, Composer, type ComposerProps, type CurrentAddress, Dialog, type DialogProps, type DiffLine, DiffView, type DiffViewProps, type Drop, type DropRule, Field, FieldList, type FieldProps, type FieldWidget, type FieldWidgetEditorProps, type FieldWidgetProps, Form, type FormProps, Input, type InputProps, type LayoutRecord, type ListAddress, type ListChange, type ListDisplay, type ListDisplayRow, LogoMark, Markdown, type MarkdownContentProps as MarkdownProps, type MarksHook, type Member, type MembersResult, MentionBox, type MentionBoxProps, type MentionDraft, MentionedText, MenuSelect, type MenuSelectProps, type MetaData, OtpInput, type OtpInputProps, PartView, type PeopleSearch, type PickedUser, type PluginInfo, type Presence, type PresenceHook, type ProjectUi, type PushState, type RecordCardProps, RecordLink, type RecordMark, RecordPicker, type RecordPickerProps, ResendCodeButton, type ResendCodeButtonProps, ResponsiveDialog, type ResponsiveDialogProps, Section, Segmented, type SegmentedOption, type SegmentedProps, type ShellLogoProps, SidePanel, type SidePanelProps, SignInPrompt, type SignInPromptProps, type SignedInUser, Spinner, StatusMark, type StatusMarkProps, type Step, StepList, type StepState, Textarea, type TextareaProps, type TitleReader, type TitleRule, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, statusRefreshMs, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, useOwnStatus, usePlugin, usePresence, usePushNotifications, useRecord, useResourceClient, useSeries, useSignedInUser, useStatuses, useUnreadNotifications, useUpdateRecord };
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { ActionInputError } from "./app/pages/action-inputs/input-errors.js";
|
|
|
21
21
|
import { useLayoutRecord } from "./app/pages/record-scope.js";
|
|
22
22
|
import { useFacets, useHistogram, useSeries } from "./data/use-aggregates.js";
|
|
23
23
|
import { LogoMark } from "./app-shell/logo.js";
|
|
24
|
+
import { AvailabilityMark, StatusMark } from "./status/status-mark.js";
|
|
24
25
|
import { SignInPrompt } from "./auth/sign-in-prompt.js";
|
|
25
26
|
import { useListDisplay } from "./app/displays/display-context.js";
|
|
26
27
|
import { MenuSelect } from "./primitives/menu-select.js";
|
|
@@ -55,7 +56,7 @@ import { AssistantDock } from "./assistant-dock/assistant-dock.js";
|
|
|
55
56
|
import { useAssistant } from "./assistant-dock/use-assistant.js";
|
|
56
57
|
import { useAssistantClient } from "./assistant-dock/use-assistant-client.js";
|
|
57
58
|
import { Form } from "./app/create/form.js";
|
|
58
|
-
import {
|
|
59
|
+
import { usePresence } from "./presence/presence.js";
|
|
59
60
|
import { usePushNotifications } from "./notifications/push/use-push-notifications.js";
|
|
60
61
|
import { useInbox, useMarkNotificationsRead, useUnreadNotifications } from "./notifications/bell/use-inbox.js";
|
|
61
62
|
import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
@@ -65,4 +66,4 @@ import { useSignedInUser } from "./auth/use-signed-in-user.js";
|
|
|
65
66
|
import { Carousel } from "./carousel/carousel.js";
|
|
66
67
|
import { Choices } from "./primitives/choices.js";
|
|
67
68
|
import { RecordPicker } from "./app/record-picker.js";
|
|
68
|
-
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, AvailabilityMark, Button, Carousel, ChatDivider, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, Markdown, MentionBox, MentionedText, MenuSelect, OtpInput, PartView, RecordLink, RecordPicker, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StatusMark, StepList, Textarea, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, statusRefreshMs, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, useOwnStatus, usePlugin, usePushNotifications, useRecord, useResourceClient, useSeries, useSignedInUser, useStatuses, useUnreadNotifications, useUpdateRecord };
|
|
69
|
+
export { ActionCard, ActionInputError, ApiProvider, App, AppLink, AssistantDock, AuthLayout, AvailabilityMark, Button, Carousel, ChatDivider, ChatMessage, ChatThread, Choices, CodeForm, CompactTable, Composer, Dialog, DiffView, Field, FieldList, Form, Input, LogoMark, Markdown, MentionBox, MentionedText, MenuSelect, OtpInput, PartView, RecordLink, RecordPicker, ResendCodeButton, ResponsiveDialog, Section, Segmented, SidePanel, SignInPrompt, Spinner, StatusMark, StepList, Textarea, allowDelayMs, buttonClass, chatDayLabel, definePlugin, defineUi, diffStats, emptyDraft, isConflict, isFramed, mentionedIds, mergeUi, parseDiff, pluginApi, protobaseVersion, sameChatDay, statusRefreshMs, uiFromModule, useApiToken, useAppAddress, useArmedAfterFocus, useAssistant, useAssistantClient, useBackendData, useClient, useFacets, useFieldControl, useGuardedPress, useHistogram, useInbox, useLayoutRecord, useList, useListDisplay, useMarkNotificationsRead, useMembers, useMeta, useOwnStatus, usePlugin, usePresence, usePushNotifications, useRecord, useResourceClient, useSeries, useSignedInUser, useStatuses, useUnreadNotifications, useUpdateRecord };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useAuth } from "../auth/auth-provider.js";
|
|
2
2
|
import { Spinner } from "../primitives/spinner.js";
|
|
3
3
|
import { Button } from "../primitives/button.js";
|
|
4
|
+
import { StatusMark } from "../status/status-mark.js";
|
|
4
5
|
import { PageHeader } from "../app-shell/page-header.js";
|
|
5
6
|
import { AccountSection, SectionError } from "../account/account-section.js";
|
|
6
7
|
import { authMessage } from "../auth/auth-messages.js";
|
|
@@ -9,7 +10,7 @@ import { text } from "./messages.js";
|
|
|
9
10
|
import { AppRolesPicker } from "./app-roles-picker.js";
|
|
10
11
|
import { grantableRoles, organizationRoleLabel } from "./organization-paths.js";
|
|
11
12
|
import { PersonRow } from "../people/person-row.js";
|
|
12
|
-
import {
|
|
13
|
+
import { presenceOf } from "../presence/presence.js";
|
|
13
14
|
import { useCallback, useEffect, useState } from "react";
|
|
14
15
|
import { AuthError } from "@protobase/client";
|
|
15
16
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -20,7 +21,7 @@ const assignableOrganizationRoles = (current) => current?.role === "owner" ? [
|
|
|
20
21
|
"admin",
|
|
21
22
|
"member"
|
|
22
23
|
] : ["admin", "member"];
|
|
23
|
-
const MemberRow = ({ member, current, status, onChanged }) => {
|
|
24
|
+
const MemberRow = ({ member, current, status, presence, onChanged }) => {
|
|
24
25
|
const { session, organizations } = useAuth();
|
|
25
26
|
const organizationId = current.organization.id;
|
|
26
27
|
const { busy, error, run } = useAction();
|
|
@@ -34,7 +35,10 @@ const MemberRow = ({ member, current, status, onChanged }) => {
|
|
|
34
35
|
email: member.email,
|
|
35
36
|
self,
|
|
36
37
|
error,
|
|
37
|
-
...status && { status: /* @__PURE__ */ jsx(StatusMark, {
|
|
38
|
+
...(status || presence) && { status: /* @__PURE__ */ jsx(StatusMark, {
|
|
39
|
+
status,
|
|
40
|
+
presence
|
|
41
|
+
}) },
|
|
38
42
|
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("select", {
|
|
39
43
|
"aria-label": text("roleOf", member.name),
|
|
40
44
|
value: member.role,
|
|
@@ -74,9 +78,10 @@ const MemberRow = ({ member, current, status, onChanged }) => {
|
|
|
74
78
|
/**
|
|
75
79
|
* The members of the organization one works in, for its owners and admins: organization roles, which decide who
|
|
76
80
|
* manages it, and app roles, which decide what each sees and does; only roles the person may give can change. In an
|
|
77
|
-
* app with status, `statuses` puts each member's beside their name (see `useStatuses`)
|
|
81
|
+
* app with status, `statuses` puts each member's beside their name (see `useStatuses`), and `presence`, the app's, puts
|
|
82
|
+
* whether they are online or away before it (see `usePresence`).
|
|
78
83
|
*/
|
|
79
|
-
const MembersPage = ({ current, statuses }) => {
|
|
84
|
+
const MembersPage = ({ current, statuses, presence }) => {
|
|
80
85
|
const { session } = useAuth();
|
|
81
86
|
const [members, setMembers] = useState();
|
|
82
87
|
const [error, setError] = useState();
|
|
@@ -100,6 +105,7 @@ const MembersPage = ({ current, statuses }) => {
|
|
|
100
105
|
member,
|
|
101
106
|
current,
|
|
102
107
|
status: statuses?.find((status) => status.userId === member.userId),
|
|
108
|
+
presence: presenceOf(presence, member.userId),
|
|
103
109
|
onChanged: load
|
|
104
110
|
}, `${member.id}/${member.role}/${member.appRoles.join()}`))
|
|
105
111
|
}) : !error && /* @__PURE__ */ jsxs("div", {
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StatusDot } from "../app-shell/status-dot.js";
|
|
2
|
+
import { text } from "../status/messages.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/presence/presence-dot.tsx
|
|
5
|
+
/** Online or away as a dot, named for screen readers and on hover. */
|
|
6
|
+
const PresenceDot = ({ presence }) => {
|
|
7
|
+
const label = text(presence);
|
|
8
|
+
return /* @__PURE__ */ jsx("span", {
|
|
9
|
+
role: "img",
|
|
10
|
+
"aria-label": label,
|
|
11
|
+
title: label,
|
|
12
|
+
className: "flex shrink-0",
|
|
13
|
+
children: /* @__PURE__ */ jsx(StatusDot, { tone: presence === "online" ? "success" : "neutral" })
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { PresenceDot };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useProjectUi } from "../app/pages/project-ui.js";
|
|
2
|
+
import { PresenceContext, createPresenceStore } from "./presence.js";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/presence/presence-provider.tsx
|
|
6
|
+
const PresenceReader = ({ usePresence, store }) => {
|
|
7
|
+
const presence = usePresence();
|
|
8
|
+
useEffect(() => store.set(presence), [store, presence]);
|
|
9
|
+
useEffect(() => () => store.set(void 0), [store]);
|
|
10
|
+
return null;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Calls the project UI's `presence` hook once, for every place that shows presence beneath it (see `usePresence`).
|
|
14
|
+
* A hook a plugin brings late, after the app first drew, starts its own reader, so the hooks never mix.
|
|
15
|
+
*/
|
|
16
|
+
const PresenceProvider = ({ children }) => {
|
|
17
|
+
const { presence } = useProjectUi();
|
|
18
|
+
const [store] = useState(createPresenceStore);
|
|
19
|
+
const [reader, setReader] = useState({
|
|
20
|
+
hook: presence,
|
|
21
|
+
key: 0
|
|
22
|
+
});
|
|
23
|
+
if (reader.hook !== presence) setReader({
|
|
24
|
+
hook: presence,
|
|
25
|
+
key: reader.key + 1
|
|
26
|
+
});
|
|
27
|
+
return /* @__PURE__ */ jsxs(PresenceContext.Provider, {
|
|
28
|
+
value: store,
|
|
29
|
+
children: [presence && /* @__PURE__ */ jsx(PresenceReader, {
|
|
30
|
+
usePresence: presence,
|
|
31
|
+
store
|
|
32
|
+
}, reader.key), children]
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { PresenceProvider };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/presence/presence.d.ts
|
|
2
|
+
/** Whether someone has a page of the app open and in use, `away` when open but idle; by the app's own rule. */
|
|
3
|
+
export type Presence = 'online' | 'away';
|
|
4
|
+
/**
|
|
5
|
+
* The app's presence, by user id, as `presence` in `protobase.ui.tsx` gives it: a React hook, called once while the app
|
|
6
|
+
* shows, so it may use the app's own channels and live updates. Someone it leaves out has no page open, and shows no
|
|
7
|
+
* presence; returning undefined, as while it loads, shows none.
|
|
8
|
+
*/
|
|
9
|
+
export type PresenceHook = () => PresenceByUser | undefined;
|
|
10
|
+
/** Presence by user id. */
|
|
11
|
+
export type PresenceByUser = Readonly<Record<string, Presence>>;
|
|
12
|
+
/** The app's presence by user id, undefined in an app that gives none (see `PresenceProvider`). */
|
|
13
|
+
export declare const usePresence: () => Readonly<Record<string, Presence>> | undefined;
|
|
14
|
+
//#endregion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore } from "react";
|
|
2
|
+
//#region src/presence/presence.ts
|
|
3
|
+
/** Holds the latest presence the app's hook gave, so the places that show it read it without calling the hook again. */
|
|
4
|
+
const createPresenceStore = () => {
|
|
5
|
+
let latest;
|
|
6
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
return {
|
|
8
|
+
get: () => latest,
|
|
9
|
+
set: (presence) => {
|
|
10
|
+
if (presence === latest) return;
|
|
11
|
+
latest = presence;
|
|
12
|
+
for (const listener of listeners) listener();
|
|
13
|
+
},
|
|
14
|
+
subscribe: (listener) => {
|
|
15
|
+
listeners.add(listener);
|
|
16
|
+
return () => void listeners.delete(listener);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const PresenceContext = createContext(void 0);
|
|
21
|
+
const nobody = () => () => {};
|
|
22
|
+
const none = () => void 0;
|
|
23
|
+
/** The app's presence by user id, undefined in an app that gives none (see `PresenceProvider`). */
|
|
24
|
+
const usePresence = () => {
|
|
25
|
+
const store = useContext(PresenceContext);
|
|
26
|
+
return useSyncExternalStore(store?.subscribe ?? nobody, store?.get ?? none, store?.get ?? none);
|
|
27
|
+
};
|
|
28
|
+
/** One person's presence, from presence by user id: nothing for someone it leaves out. */
|
|
29
|
+
const presenceOf = (presence, userId) => presence && Object.hasOwn(presence, userId) ? presence[userId] : void 0;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { PresenceContext, createPresenceStore, presenceOf, usePresence };
|