@peers-app/peers-ui 0.27.2 → 0.27.4
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/screens/network-viewer/network-viewer-ipc.d.ts +3 -1
- package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
- package/dist/screens/network-viewer/sync-recovery.js +103 -1
- package/dist/screens/variables/variable-details.js +2 -1
- package/dist/tabs-layout/network-status-indicator.js +26 -12
- package/dist/tabs-layout/use-network-status.d.ts +6 -1
- package/dist/tabs-layout/use-network-status.js +27 -4
- package/package.json +9 -3
- package/.claude/settings.local.json +0 -34
- package/.github/README.md +0 -52
- package/.github/workflows/delete-all-artifacts.yml +0 -49
- package/.github/workflows/publish.yml +0 -51
- package/babel.config.js +0 -7
- package/biome.json +0 -198
- package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
- package/dist/app-updates/app-updates-consumer.test.js +0 -56
- package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
- package/dist/app-updates/app-updates-settings.test.js +0 -93
- package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
- package/dist/app-updates/remote-app-update-panel.test.js +0 -165
- package/dist/components/access-level-field.test.d.ts +0 -1
- package/dist/components/access-level-field.test.js +0 -28
- package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
- package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
- package/dist/components/responsive-section-nav.test.d.ts +0 -1
- package/dist/components/responsive-section-nav.test.js +0 -45
- package/dist/globals-hash.test.d.ts +0 -1
- package/dist/globals-hash.test.js +0 -16
- package/dist/hooks/use-observable-state.test.d.ts +0 -1
- package/dist/hooks/use-observable-state.test.js +0 -64
- package/dist/hooks/use-observable.test.d.ts +0 -1
- package/dist/hooks/use-observable.test.js +0 -191
- package/dist/hooks/use-rows.test.d.ts +0 -1
- package/dist/hooks/use-rows.test.js +0 -175
- package/dist/hooks/use-tool.test.d.ts +0 -1
- package/dist/hooks/use-tool.test.js +0 -190
- package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
- package/dist/host-shell/host-shell-consumer.test.js +0 -28
- package/dist/root-layout/root-layout-host.test.d.ts +0 -1
- package/dist/root-layout/root-layout-host.test.js +0 -86
- package/dist/screens/account/account-screen.test.d.ts +0 -1
- package/dist/screens/account/account-screen.test.js +0 -51
- package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
- package/dist/screens/assistants/assistant-config.test.js +0 -57
- package/dist/screens/contacts/contact-list.test.d.ts +0 -1
- package/dist/screens/contacts/contact-list.test.js +0 -48
- package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
- package/dist/screens/device-pairing/device-pairing.test.js +0 -119
- package/dist/screens/devices/devices-list.test.d.ts +0 -1
- package/dist/screens/devices/devices-list.test.js +0 -53
- package/dist/screens/groups/group-list.test.d.ts +0 -1
- package/dist/screens/groups/group-list.test.js +0 -38
- package/dist/screens/identity/identity-routes.test.d.ts +0 -1
- package/dist/screens/identity/identity-routes.test.js +0 -51
- package/dist/screens/identity/identity-screen.test.d.ts +0 -1
- package/dist/screens/identity/identity-screen.test.js +0 -103
- package/dist/screens/invites/accept-invite.test.d.ts +0 -1
- package/dist/screens/invites/accept-invite.test.js +0 -103
- package/dist/screens/invites/activity-view.test.d.ts +0 -1
- package/dist/screens/invites/activity-view.test.js +0 -108
- package/dist/screens/invites/invite-classify.test.d.ts +0 -1
- package/dist/screens/invites/invite-classify.test.js +0 -115
- package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
- package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
- package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
- package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
- package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
- package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
- package/dist/screens/packages/package-helpers.test.d.ts +0 -1
- package/dist/screens/packages/package-helpers.test.js +0 -53
- package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
- package/dist/screens/packages/package-runtime-status.test.js +0 -50
- package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
- package/dist/screens/packages/packages-ipc.test.js +0 -43
- package/dist/screens/settings/settings-page.test.d.ts +0 -1
- package/dist/screens/settings/settings-page.test.js +0 -19
- package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
- package/dist/screens/tools/tool-access-ipc.test.js +0 -40
- package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
- package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
- package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
- package/dist/tabs-layout/network-status-indicator.test.js +0 -665
- package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
- package/dist/tabs-layout/tabs-state.test.js +0 -110
- package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
- package/dist/tabs-layout/use-network-status.test.js +0 -45
- package/dist/ui-router/ui-loader.test.d.ts +0 -1
- package/dist/ui-router/ui-loader.test.js +0 -91
- package/jest.config.js +0 -25
- package/release-guard.js +0 -10
- package/src/app-updates/app-update-controls.tsx +0 -73
- package/src/app-updates/app-updates-consumer.test.ts +0 -83
- package/src/app-updates/app-updates-consumer.ts +0 -121
- package/src/app-updates/app-updates-settings.test.tsx +0 -109
- package/src/app-updates/app-updates-settings.tsx +0 -136
- package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
- package/src/app-updates/remote-app-update-panel.tsx +0 -210
- package/src/app-updates/use-app-update-status.ts +0 -16
- package/src/command-palette/command-palette-ui.tsx +0 -564
- package/src/command-palette/command-palette.ts +0 -242
- package/src/components/access-level-field.test.tsx +0 -32
- package/src/components/access-level-field.tsx +0 -68
- package/src/components/checkbox.tsx +0 -25
- package/src/components/group-switcher.tsx +0 -526
- package/src/components/input-date.tsx +0 -29
- package/src/components/input-number.tsx +0 -83
- package/src/components/input.tsx +0 -25
- package/src/components/inverse-lazy-list.tsx +0 -107
- package/src/components/io-schema-values.tsx +0 -103
- package/src/components/io-schema.tsx +0 -259
- package/src/components/lazy-list.tsx +0 -115
- package/src/components/list-screen.tsx +0 -126
- package/src/components/loading-indicator.tsx +0 -7
- package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
- package/src/components/markdown-editor/editor-inline.tsx +0 -9
- package/src/components/markdown-editor/editor.tsx +0 -158
- package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
- package/src/components/markdown-editor/mention-node.ts +0 -204
- package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
- package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
- package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
- package/src/components/markdown-editor/theme.ts +0 -46
- package/src/components/markdown-editor/toolbar.tsx +0 -269
- package/src/components/markdown-with-mentions.tsx +0 -198
- package/src/components/message-logs/message-logs.tsx +0 -523
- package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
- package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
- package/src/components/messages/avatar.tsx +0 -171
- package/src/components/messages/channel-message-list.tsx +0 -189
- package/src/components/messages/channel-view.tsx +0 -92
- package/src/components/messages/message-compose.tsx +0 -192
- package/src/components/messages/message-display.tsx +0 -231
- package/src/components/messages/thread-message-list.tsx +0 -135
- package/src/components/qr-scanner.tsx +0 -180
- package/src/components/responsive-section-nav.test.tsx +0 -88
- package/src/components/responsive-section-nav.tsx +0 -92
- package/src/components/router.tsx +0 -175
- package/src/components/save-button.tsx +0 -134
- package/src/components/sortable-list.tsx +0 -130
- package/src/components/tabs.tsx +0 -76
- package/src/components/tooltip.tsx +0 -43
- package/src/components/trust-level-badge.tsx +0 -44
- package/src/components/trust-level-dropdown.tsx +0 -136
- package/src/components/typeahead.tsx +0 -357
- package/src/globals-hash.test.ts +0 -19
- package/src/globals.tsx +0 -226
- package/src/hooks/index.ts +0 -8
- package/src/hooks/use-host-contract.ts +0 -75
- package/src/hooks/use-observable-state.test.tsx +0 -101
- package/src/hooks/use-observable-state.ts +0 -29
- package/src/hooks/use-observable.test.tsx +0 -258
- package/src/hooks/use-observable.ts +0 -67
- package/src/hooks/use-on-screen.ts +0 -24
- package/src/hooks/use-promise.ts +0 -34
- package/src/hooks/use-rows.test.tsx +0 -196
- package/src/hooks/use-rows.ts +0 -120
- package/src/hooks/use-subscription.ts +0 -24
- package/src/hooks/use-tool.test.tsx +0 -225
- package/src/hooks/use-tool.ts +0 -116
- package/src/host-shell/host-shell-consumer.test.ts +0 -32
- package/src/host-shell/host-shell-consumer.ts +0 -53
- package/src/index.tsx +0 -42
- package/src/mention-configs.ts +0 -185
- package/src/operator-console/operator-console-state.ts +0 -119
- package/src/operator-console/operator-console.tsx +0 -233
- package/src/peers-services-registration.ts +0 -6
- package/src/root-layout/global-overlays.ts +0 -50
- package/src/root-layout/layout-picker.tsx +0 -47
- package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
- package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
- package/src/root-layout/root-layout-host.test.tsx +0 -108
- package/src/root-layout/root-layout-host.tsx +0 -147
- package/src/root-layout/root-layout-registry.ts +0 -51
- package/src/root-layout/root-layout-state.ts +0 -55
- package/src/root-layout/root-layout.ts +0 -32
- package/src/screens/account/account-screen.test.tsx +0 -65
- package/src/screens/account/account-screen.tsx +0 -942
- package/src/screens/assistants/assistant-config.test.tsx +0 -54
- package/src/screens/assistants/assistant-config.tsx +0 -121
- package/src/screens/assistants/assistant-details.tsx +0 -89
- package/src/screens/assistants/assistant-info.tsx +0 -66
- package/src/screens/assistants/assistant-list.tsx +0 -145
- package/src/screens/assistants/assistant-tools.tsx +0 -82
- package/src/screens/console-logs/console-logs-list.tsx +0 -374
- package/src/screens/console-logs/log-display.tsx +0 -137
- package/src/screens/console-logs/log-filters.tsx +0 -64
- package/src/screens/console-logs/mobile-log-card.tsx +0 -161
- package/src/screens/console-logs/resizable-table-header.tsx +0 -110
- package/src/screens/contacts/contact-details.tsx +0 -123
- package/src/screens/contacts/contact-list.test.tsx +0 -59
- package/src/screens/contacts/contact-list.tsx +0 -286
- package/src/screens/contacts/index.ts +0 -2
- package/src/screens/data-explorer/data-explorer.tsx +0 -633
- package/src/screens/data-explorer/index.ts +0 -6
- package/src/screens/data-explorer/query-executor.tsx +0 -520
- package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
- package/src/screens/device-pairing/device-pairing.tsx +0 -461
- package/src/screens/device-pairing/identity-ipc.ts +0 -25
- package/src/screens/device-pairing/index.ts +0 -2
- package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
- package/src/screens/devices/devices-list.test.tsx +0 -68
- package/src/screens/devices/devices-list.tsx +0 -365
- package/src/screens/devices/index.ts +0 -1
- package/src/screens/groups/group-details.tsx +0 -160
- package/src/screens/groups/group-invite-panel.tsx +0 -311
- package/src/screens/groups/group-list.test.tsx +0 -44
- package/src/screens/groups/group-list.tsx +0 -317
- package/src/screens/groups/group-members.tsx +0 -343
- package/src/screens/groups/index.ts +0 -3
- package/src/screens/identity/identity-routes.test.ts +0 -70
- package/src/screens/identity/identity-routes.ts +0 -301
- package/src/screens/identity/identity-screen.test.tsx +0 -138
- package/src/screens/identity/identity-screen.tsx +0 -282
- package/src/screens/identity/index.ts +0 -2
- package/src/screens/identity/invite-contact-panel.tsx +0 -45
- package/src/screens/identity/profile-invite.tsx +0 -93
- package/src/screens/invites/accept-invite.test.tsx +0 -115
- package/src/screens/invites/accept-invite.tsx +0 -318
- package/src/screens/invites/activity-view.test.tsx +0 -127
- package/src/screens/invites/activity-view.tsx +0 -162
- package/src/screens/invites/index.ts +0 -6
- package/src/screens/invites/invite-capture.ts +0 -104
- package/src/screens/invites/invite-classify.test.ts +0 -129
- package/src/screens/invites/invite-share.tsx +0 -104
- package/src/screens/invites/invites-ipc.ts +0 -25
- package/src/screens/invites/pending-invites-list.tsx +0 -256
- package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
- package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
- package/src/screens/network-viewer/device-details-modal.tsx +0 -339
- package/src/screens/network-viewer/group-details-modal.tsx +0 -421
- package/src/screens/network-viewer/index.ts +0 -8
- package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
- package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
- package/src/screens/network-viewer/network-viewer-section.ts +0 -12
- package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
- package/src/screens/network-viewer/network-viewer.tsx +0 -708
- package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
- package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
- package/src/screens/network-viewer/sync-recovery.tsx +0 -461
- package/src/screens/network-viewer/usage-graph.tsx +0 -455
- package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
- package/src/screens/packages/local-folders-ipc.ts +0 -28
- package/src/screens/packages/package-details.tsx +0 -166
- package/src/screens/packages/package-helpers.test.ts +0 -57
- package/src/screens/packages/package-helpers.ts +0 -137
- package/src/screens/packages/package-info.tsx +0 -354
- package/src/screens/packages/package-list.tsx +0 -519
- package/src/screens/packages/package-network-panel.tsx +0 -82
- package/src/screens/packages/package-new-local.tsx +0 -100
- package/src/screens/packages/package-runtime-status.test.tsx +0 -58
- package/src/screens/packages/package-runtime-status.tsx +0 -68
- package/src/screens/packages/package-versions.tsx +0 -298
- package/src/screens/packages/packages-ipc.test.ts +0 -47
- package/src/screens/packages/packages-ipc.ts +0 -125
- package/src/screens/peer-types/peer-type-details.tsx +0 -172
- package/src/screens/peer-types/peer-type-list.tsx +0 -64
- package/src/screens/search/global-search.tsx +0 -406
- package/src/screens/settings/color-mode-dropdown.tsx +0 -82
- package/src/screens/settings/settings-page.test.tsx +0 -24
- package/src/screens/settings/settings-page.tsx +0 -426
- package/src/screens/setup-user.tsx +0 -355
- package/src/screens/tools/tool-access-ipc.test.ts +0 -55
- package/src/screens/tools/tool-access-ipc.ts +0 -27
- package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
- package/src/screens/tools/tool-catalog-ipc.ts +0 -51
- package/src/screens/tools/tool-details.tsx +0 -52
- package/src/screens/tools/tool-info.tsx +0 -45
- package/src/screens/tools/tool-list.tsx +0 -106
- package/src/screens/tools/tool-schema.tsx +0 -44
- package/src/screens/tools/tool-test-details.tsx +0 -89
- package/src/screens/tools/tool-test-list.tsx +0 -73
- package/src/screens/variables/variable-details.tsx +0 -289
- package/src/screens/variables/variable-list.tsx +0 -110
- package/src/screens/welcome-modal.tsx +0 -294
- package/src/screens/workflows/workflow-details.tsx +0 -115
- package/src/screens/workflows/workflow-info.tsx +0 -22
- package/src/screens/workflows/workflow-instructions.tsx +0 -134
- package/src/screens/workflows/workflow-list.tsx +0 -117
- package/src/setupTests.ts +0 -38
- package/src/system-apps/assistants.app.ts +0 -7
- package/src/system-apps/console-logs.app.ts +0 -7
- package/src/system-apps/data-explorer.app.ts +0 -8
- package/src/system-apps/identity.app.ts +0 -9
- package/src/system-apps/index.ts +0 -77
- package/src/system-apps/mobile-settings.app.ts +0 -7
- package/src/system-apps/network-viewer.app.ts +0 -8
- package/src/system-apps/packages.app.ts +0 -7
- package/src/system-apps/search.app.ts +0 -8
- package/src/system-apps/settings.app.ts +0 -7
- package/src/system-apps/threads.app.ts +0 -7
- package/src/system-apps/tools.app.ts +0 -7
- package/src/system-apps/types.app.ts +0 -7
- package/src/system-apps/variables.app.ts +0 -7
- package/src/system-apps/workflows.app.ts +0 -7
- package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
- package/src/tabs-layout/network-status-indicator.tsx +0 -756
- package/src/tabs-layout/tabs-layout.tsx +0 -857
- package/src/tabs-layout/tabs-state.test.ts +0 -152
- package/src/tabs-layout/tabs-state.ts +0 -587
- package/src/tabs-layout/use-network-status.test.tsx +0 -58
- package/src/tabs-layout/use-network-status.ts +0 -68
- package/src/ui-defaults/index.ts +0 -2
- package/src/ui-defaults/list-screen.tsx +0 -94
- package/src/ui-defaults/markdown-field.tsx +0 -87
- package/src/ui-router/routes-loader.ts +0 -193
- package/src/ui-router/ui-loader.test.tsx +0 -108
- package/src/ui-router/ui-loader.tsx +0 -606
- package/src/utils.ts +0 -95
- package/tsconfig.json +0 -18
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type GroupInviteMode,
|
|
3
|
-
GroupMemberRole,
|
|
4
|
-
type ICreatedInvite,
|
|
5
|
-
type IUserWithSource,
|
|
6
|
-
usersCursor,
|
|
7
|
-
} from "@peers-app/peers-sdk";
|
|
8
|
-
import { useState } from "react";
|
|
9
|
-
import { Typeahead, type TypeaheadItem } from "../../components/typeahead";
|
|
10
|
-
import { PendingWorkLink } from "../invites/activity-view";
|
|
11
|
-
import { InviteShare } from "../invites/invite-share";
|
|
12
|
-
import { inviteTools } from "../invites/invites-ipc";
|
|
13
|
-
|
|
14
|
-
/** Props for {@link GroupInvitePanel}. */
|
|
15
|
-
export interface GroupInvitePanelProps {
|
|
16
|
-
groupId: string;
|
|
17
|
-
/** Users who are already members; hidden from the contact picker. */
|
|
18
|
-
excludeUserIds: string[];
|
|
19
|
-
/** The local user's role; caps the roles offered. */
|
|
20
|
-
myRole: GroupMemberRole;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const ROLE_OPTIONS: Array<{ value: GroupMemberRole; label: string }> = [
|
|
24
|
-
{ value: GroupMemberRole.Reader, label: "Reader" },
|
|
25
|
-
{ value: GroupMemberRole.Writer, label: "Writer" },
|
|
26
|
-
{ value: GroupMemberRole.Admin, label: "Admin" },
|
|
27
|
-
{ value: GroupMemberRole.Owner, label: "Owner" },
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
const EXPIRY_OPTIONS: Array<{ ms: number; label: string }> = [
|
|
31
|
-
{ ms: 60 * 60_000, label: "1 hour" },
|
|
32
|
-
{ ms: 24 * 60 * 60_000, label: "1 day" },
|
|
33
|
-
{ ms: 7 * 24 * 60 * 60_000, label: "7 days" },
|
|
34
|
-
{ ms: 30 * 24 * 60 * 60_000, label: "30 days" },
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
function RoleSelect(props: {
|
|
38
|
-
value: GroupMemberRole;
|
|
39
|
-
max: GroupMemberRole;
|
|
40
|
-
disabled?: boolean;
|
|
41
|
-
onChange: (role: GroupMemberRole) => void;
|
|
42
|
-
}) {
|
|
43
|
-
return (
|
|
44
|
-
<select
|
|
45
|
-
className="form-select"
|
|
46
|
-
value={props.value}
|
|
47
|
-
disabled={props.disabled}
|
|
48
|
-
onChange={(event) => props.onChange(Number(event.target.value) as GroupMemberRole)}
|
|
49
|
-
aria-label="Role"
|
|
50
|
-
>
|
|
51
|
-
{ROLE_OPTIONS.filter((option) => option.value <= props.max).map((option) => (
|
|
52
|
-
<option key={option.value} value={option.value}>
|
|
53
|
-
{option.label}
|
|
54
|
-
</option>
|
|
55
|
-
))}
|
|
56
|
-
</select>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function ContactPicker(props: {
|
|
61
|
-
excludeUserIds: string[];
|
|
62
|
-
selected: IUserWithSource[];
|
|
63
|
-
disabled: boolean;
|
|
64
|
-
onChange: (items: IUserWithSource[]) => void;
|
|
65
|
-
}) {
|
|
66
|
-
const search = async (query: string): Promise<(IUserWithSource & TypeaheadItem)[]> => {
|
|
67
|
-
const cursor = await usersCursor({ name: { $matchWords: query } }, { includeTrustLevel: true });
|
|
68
|
-
const results: IUserWithSource[] = [];
|
|
69
|
-
for await (const contact of cursor) {
|
|
70
|
-
if (!props.excludeUserIds.includes(contact.userId)) {
|
|
71
|
-
results.push(contact);
|
|
72
|
-
}
|
|
73
|
-
if (results.length >= 10) break;
|
|
74
|
-
}
|
|
75
|
-
return results.map((contact) => ({ ...contact, id: contact.userId }));
|
|
76
|
-
};
|
|
77
|
-
return (
|
|
78
|
-
<Typeahead
|
|
79
|
-
placeholder="Search contacts…"
|
|
80
|
-
searchFn={search}
|
|
81
|
-
onSelectionChange={(items) => props.onChange(items as IUserWithSource[])}
|
|
82
|
-
renderItem={(contact) => (
|
|
83
|
-
<div className="d-flex align-items-center">
|
|
84
|
-
<i className="bi-person-circle me-2" style={{ fontSize: 20 }} />
|
|
85
|
-
<div className="flex-grow-1">
|
|
86
|
-
<div className="fw-semibold">{(contact as IUserWithSource).name}</div>
|
|
87
|
-
<small className="text-muted">{(contact as IUserWithSource).userId}</small>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
renderBadge={(contact) => <span>{(contact as IUserWithSource).name}</span>}
|
|
92
|
-
selectedItems={props.selected.map((contact) => ({ ...contact, id: contact.userId }))}
|
|
93
|
-
multiSelect
|
|
94
|
-
disabled={props.disabled}
|
|
95
|
-
/>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Invite known contacts directly (they get a ready-to-accept invite; no link needed). */
|
|
100
|
-
function InviteContactsCard(props: GroupInvitePanelProps) {
|
|
101
|
-
const [selected, setSelected] = useState<IUserWithSource[]>([]);
|
|
102
|
-
const [role, setRole] = useState<GroupMemberRole>(GroupMemberRole.Writer);
|
|
103
|
-
const [busy, setBusy] = useState(false);
|
|
104
|
-
const [message, setMessage] = useState<{ ok: boolean; text: string } | undefined>(undefined);
|
|
105
|
-
|
|
106
|
-
const send = async () => {
|
|
107
|
-
setBusy(true);
|
|
108
|
-
setMessage(undefined);
|
|
109
|
-
try {
|
|
110
|
-
const tools = await inviteTools();
|
|
111
|
-
let queued = 0;
|
|
112
|
-
for (const contact of selected) {
|
|
113
|
-
const { result } = await tools.inviteContactToGroup({
|
|
114
|
-
groupId: props.groupId,
|
|
115
|
-
userId: contact.userId,
|
|
116
|
-
role,
|
|
117
|
-
});
|
|
118
|
-
if (result.deliveryState === "queued") queued++;
|
|
119
|
-
}
|
|
120
|
-
setMessage({
|
|
121
|
-
ok: true,
|
|
122
|
-
text:
|
|
123
|
-
queued > 0
|
|
124
|
-
? `Invited ${selected.length}. ${queued} will be delivered when they are reachable.`
|
|
125
|
-
: `Invited ${selected.length}. They will appear once they accept.`,
|
|
126
|
-
});
|
|
127
|
-
setSelected([]);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
setMessage({ ok: false, text: (err as Error).message || "Could not send the invite." });
|
|
130
|
-
} finally {
|
|
131
|
-
setBusy(false);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<div className="row g-2 align-items-start">
|
|
137
|
-
<div className="col-md-6">
|
|
138
|
-
<ContactPicker
|
|
139
|
-
excludeUserIds={props.excludeUserIds}
|
|
140
|
-
selected={selected}
|
|
141
|
-
disabled={busy}
|
|
142
|
-
onChange={setSelected}
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
<div className="col-md-3">
|
|
146
|
-
<RoleSelect value={role} max={props.myRole} disabled={busy} onChange={setRole} />
|
|
147
|
-
</div>
|
|
148
|
-
<div className="col-md-3">
|
|
149
|
-
<button
|
|
150
|
-
type="button"
|
|
151
|
-
className="btn btn-primary w-100"
|
|
152
|
-
disabled={busy || selected.length === 0}
|
|
153
|
-
onClick={() => void send()}
|
|
154
|
-
>
|
|
155
|
-
{busy
|
|
156
|
-
? "Sending…"
|
|
157
|
-
: selected.length > 1
|
|
158
|
-
? `Invite ${selected.length} contacts`
|
|
159
|
-
: "Invite contact"}
|
|
160
|
-
</button>
|
|
161
|
-
</div>
|
|
162
|
-
{message ? (
|
|
163
|
-
<div className="col-12">
|
|
164
|
-
<div className={`alert py-2 mb-0 ${message.ok ? "alert-success" : "alert-danger"}`}>
|
|
165
|
-
{message.text}
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
) : null}
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Issue a shareable link for people who are not contacts yet. */
|
|
174
|
-
function ShareLinkCard(props: GroupInvitePanelProps) {
|
|
175
|
-
const [role, setRole] = useState<GroupMemberRole>(GroupMemberRole.Reader);
|
|
176
|
-
const [mode, setMode] = useState<GroupInviteMode>("approve");
|
|
177
|
-
const [expiresInMs, setExpiresInMs] = useState(EXPIRY_OPTIONS[1].ms);
|
|
178
|
-
const [busy, setBusy] = useState(false);
|
|
179
|
-
const [error, setError] = useState("");
|
|
180
|
-
const [invite, setInvite] = useState<ICreatedInvite | undefined>(undefined);
|
|
181
|
-
|
|
182
|
-
const create = async () => {
|
|
183
|
-
setBusy(true);
|
|
184
|
-
setError("");
|
|
185
|
-
try {
|
|
186
|
-
const tools = await inviteTools();
|
|
187
|
-
const { invite: created } = await tools.createGroupInvite({
|
|
188
|
-
groupId: props.groupId,
|
|
189
|
-
role,
|
|
190
|
-
mode,
|
|
191
|
-
expiresInMs,
|
|
192
|
-
});
|
|
193
|
-
setInvite(created);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
setError((err as Error).message || "Could not create the link.");
|
|
196
|
-
} finally {
|
|
197
|
-
setBusy(false);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
if (invite) {
|
|
202
|
-
return (
|
|
203
|
-
<InviteShare
|
|
204
|
-
invite={invite}
|
|
205
|
-
description={
|
|
206
|
-
mode === "open"
|
|
207
|
-
? "Anyone with this link joins immediately. Revoke it from the pending list when you are done."
|
|
208
|
-
: "People who use this link appear below as join requests for you to approve."
|
|
209
|
-
}
|
|
210
|
-
shareText={`Join ${invite.token.group?.name ?? "my group"} on Peers:`}
|
|
211
|
-
onDone={() => setInvite(undefined)}
|
|
212
|
-
/>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return (
|
|
217
|
-
<div className="row g-2 align-items-end">
|
|
218
|
-
<div className="col-md-3">
|
|
219
|
-
<label htmlFor="link-role" className="form-label small text-muted mb-1">
|
|
220
|
-
Role
|
|
221
|
-
</label>
|
|
222
|
-
<RoleSelect value={role} max={props.myRole} disabled={busy} onChange={setRole} />
|
|
223
|
-
</div>
|
|
224
|
-
<div className="col-md-3">
|
|
225
|
-
<label htmlFor="link-mode" className="form-label small text-muted mb-1">
|
|
226
|
-
Approval
|
|
227
|
-
</label>
|
|
228
|
-
<select
|
|
229
|
-
id="link-mode"
|
|
230
|
-
className="form-select"
|
|
231
|
-
value={mode}
|
|
232
|
-
disabled={busy}
|
|
233
|
-
onChange={(event) => setMode(event.target.value as GroupInviteMode)}
|
|
234
|
-
>
|
|
235
|
-
<option value="approve">I approve each join</option>
|
|
236
|
-
<option value="open">Anyone with the link joins</option>
|
|
237
|
-
</select>
|
|
238
|
-
</div>
|
|
239
|
-
<div className="col-md-3">
|
|
240
|
-
<label htmlFor="link-expiry" className="form-label small text-muted mb-1">
|
|
241
|
-
Expires in
|
|
242
|
-
</label>
|
|
243
|
-
<select
|
|
244
|
-
id="link-expiry"
|
|
245
|
-
className="form-select"
|
|
246
|
-
value={expiresInMs}
|
|
247
|
-
disabled={busy}
|
|
248
|
-
onChange={(event) => setExpiresInMs(Number(event.target.value))}
|
|
249
|
-
>
|
|
250
|
-
{EXPIRY_OPTIONS.map((option) => (
|
|
251
|
-
<option key={option.ms} value={option.ms}>
|
|
252
|
-
{option.label}
|
|
253
|
-
</option>
|
|
254
|
-
))}
|
|
255
|
-
</select>
|
|
256
|
-
</div>
|
|
257
|
-
<div className="col-md-3">
|
|
258
|
-
<button
|
|
259
|
-
type="button"
|
|
260
|
-
className="btn btn-outline-primary w-100"
|
|
261
|
-
disabled={busy}
|
|
262
|
-
onClick={() => void create()}
|
|
263
|
-
>
|
|
264
|
-
{busy ? "Creating…" : "Create link"}
|
|
265
|
-
</button>
|
|
266
|
-
</div>
|
|
267
|
-
{error ? (
|
|
268
|
-
<div className="col-12">
|
|
269
|
-
<div className="alert alert-danger py-2 mb-0">{error}</div>
|
|
270
|
-
</div>
|
|
271
|
-
) : null}
|
|
272
|
-
</div>
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Admin controls for growing a group: invite existing contacts first, share a link second,
|
|
278
|
-
* and act on pending join requests. Membership changes land when the other side accepts.
|
|
279
|
-
*/
|
|
280
|
-
export function GroupInvitePanel(props: GroupInvitePanelProps) {
|
|
281
|
-
const [tab, setTab] = useState<"contacts" | "link">("contacts");
|
|
282
|
-
return (
|
|
283
|
-
<>
|
|
284
|
-
<div className="card mb-3">
|
|
285
|
-
<div className="card-header d-flex align-items-center gap-3">
|
|
286
|
-
<h6 className="mb-0">Invite people</h6>
|
|
287
|
-
<div className="btn-group btn-group-sm ms-auto" role="group" aria-label="Invite method">
|
|
288
|
-
<button
|
|
289
|
-
type="button"
|
|
290
|
-
className={`btn ${tab === "contacts" ? "btn-primary" : "btn-outline-primary"}`}
|
|
291
|
-
onClick={() => setTab("contacts")}
|
|
292
|
-
>
|
|
293
|
-
Contacts
|
|
294
|
-
</button>
|
|
295
|
-
<button
|
|
296
|
-
type="button"
|
|
297
|
-
className={`btn ${tab === "link" ? "btn-primary" : "btn-outline-primary"}`}
|
|
298
|
-
onClick={() => setTab("link")}
|
|
299
|
-
>
|
|
300
|
-
Share a link
|
|
301
|
-
</button>
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
<div className="card-body">
|
|
305
|
-
{tab === "contacts" ? <InviteContactsCard {...props} /> : <ShareLinkCard {...props} />}
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
<PendingWorkLink type="group" groupId={props.groupId} />
|
|
309
|
-
</>
|
|
310
|
-
);
|
|
311
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { GroupMemberRole } from "@peers-app/peers-sdk";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { GroupSummaryRow, type IGroupSummary } from "./group-list";
|
|
4
|
-
|
|
5
|
-
jest.mock("../../globals", () => ({
|
|
6
|
-
mainContentPath: jest.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
jest.mock("../../hooks", () => ({
|
|
10
|
-
usePromise: jest.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
jest.mock("../invites/activity-view", () => ({
|
|
14
|
-
PendingWorkLink: () => null,
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
const summary: IGroupSummary = {
|
|
18
|
-
group: {
|
|
19
|
-
groupId: "zyxwvutsrqponmlkjihgfedcb",
|
|
20
|
-
name: "Neighborhood",
|
|
21
|
-
description: "**Local** projects and shared plans",
|
|
22
|
-
founderUserId: "abcdefghijklmnopqrstuvwxy",
|
|
23
|
-
publicKey: "signing-key",
|
|
24
|
-
publicBoxKey: "box-key",
|
|
25
|
-
signature: "signature",
|
|
26
|
-
},
|
|
27
|
-
memberCount: 7,
|
|
28
|
-
myRole: GroupMemberRole.Admin,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe("GroupSummaryRow", () => {
|
|
32
|
-
it("shows description, member count, and local role in a clickable row", () => {
|
|
33
|
-
render(<GroupSummaryRow summary={summary} />);
|
|
34
|
-
|
|
35
|
-
expect(screen.getByText("Neighborhood")).toBeInTheDocument();
|
|
36
|
-
expect(screen.getByText("Local projects and shared plans")).toBeInTheDocument();
|
|
37
|
-
expect(screen.getByText("7 members")).toBeInTheDocument();
|
|
38
|
-
expect(screen.getByText("Admin")).toBeInTheDocument();
|
|
39
|
-
expect(screen.getByRole("link")).toHaveAttribute(
|
|
40
|
-
"href",
|
|
41
|
-
`#identity/groups/${summary.group.groupId}`,
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type GroupMemberRole,
|
|
3
|
-
GroupMembers,
|
|
4
|
-
Groups,
|
|
5
|
-
getMe,
|
|
6
|
-
getRoleLabel,
|
|
7
|
-
getUserContext,
|
|
8
|
-
type IGroup,
|
|
9
|
-
newid,
|
|
10
|
-
newKeys,
|
|
11
|
-
saveGroupSecretKey,
|
|
12
|
-
setUserTrustLevel,
|
|
13
|
-
TrustLevel,
|
|
14
|
-
type UserContext,
|
|
15
|
-
Users,
|
|
16
|
-
} from "@peers-app/peers-sdk";
|
|
17
|
-
import { useEffect, useMemo, useState } from "react";
|
|
18
|
-
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
19
|
-
import { mainContentPath } from "../../globals";
|
|
20
|
-
import { usePromise } from "../../hooks";
|
|
21
|
-
import { IDENTITY_GROUP_NEW_PATH, IDENTITY_GROUPS_PATH } from "../identity/identity-routes";
|
|
22
|
-
import { PendingWorkLink } from "../invites/activity-view";
|
|
23
|
-
import { packagesTools } from "../packages/packages-ipc";
|
|
24
|
-
|
|
25
|
-
/** Search, summarize, and create groups owned by the signed-in user. */
|
|
26
|
-
export function GroupList(props: { creating?: boolean } = {}) {
|
|
27
|
-
const userContext = usePromise(getUserContext);
|
|
28
|
-
const [creating, setCreating] = useState(props.creating ?? false);
|
|
29
|
-
const [draftName, setDraftName] = useState("");
|
|
30
|
-
const [searchText, setSearchText] = useState("");
|
|
31
|
-
const [summaries, setSummaries] = useState<IGroupSummary[] | undefined>();
|
|
32
|
-
const [error, setError] = useState("");
|
|
33
|
-
const [creatingBusy, setCreatingBusy] = useState(false);
|
|
34
|
-
const [groupRevision, setGroupRevision] = useState(0);
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (!userContext) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
let disposed = false;
|
|
41
|
-
const subscriptions: Array<{ unsubscribe: () => void }> = [];
|
|
42
|
-
const refresh = async () => {
|
|
43
|
-
try {
|
|
44
|
-
const next = await loadGroupSummaries(userContext);
|
|
45
|
-
if (!disposed) {
|
|
46
|
-
setSummaries(next);
|
|
47
|
-
setError("");
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
if (!disposed) {
|
|
51
|
-
setError((err as Error).message || "Could not load groups.");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
const table = Groups(userContext.userDataContext);
|
|
56
|
-
subscriptions.push(table.dataChanged.subscribe(() => setGroupRevision((value) => value + 1)));
|
|
57
|
-
void table.list({ disabled: { $ne: true } }).then((groups) => {
|
|
58
|
-
if (disposed) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
for (const group of groups) {
|
|
62
|
-
subscriptions.push(
|
|
63
|
-
GroupMembers(userContext.getDataContext(group.groupId)).dataChanged.subscribe(
|
|
64
|
-
() => void refresh(),
|
|
65
|
-
),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
void refresh();
|
|
69
|
-
});
|
|
70
|
-
return () => {
|
|
71
|
-
disposed = true;
|
|
72
|
-
for (const subscription of subscriptions) {
|
|
73
|
-
subscription.unsubscribe();
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
}, [groupRevision, userContext]);
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
setCreating(props.creating ?? false);
|
|
80
|
-
}, [props.creating]);
|
|
81
|
-
|
|
82
|
-
const filtered = useMemo(() => {
|
|
83
|
-
const query = searchText.trim().toLocaleLowerCase();
|
|
84
|
-
if (!query) {
|
|
85
|
-
return summaries ?? [];
|
|
86
|
-
}
|
|
87
|
-
return (summaries ?? []).filter(
|
|
88
|
-
({ group }) =>
|
|
89
|
-
group.name.toLocaleLowerCase().includes(query) ||
|
|
90
|
-
group.description.toLocaleLowerCase().includes(query),
|
|
91
|
-
);
|
|
92
|
-
}, [searchText, summaries]);
|
|
93
|
-
|
|
94
|
-
function startCreate() {
|
|
95
|
-
setDraftName(searchText.trim());
|
|
96
|
-
mainContentPath(IDENTITY_GROUP_NEW_PATH);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function confirmCreate() {
|
|
100
|
-
const name = draftName.trim();
|
|
101
|
-
if (!name) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
setCreatingBusy(true);
|
|
105
|
-
setError("");
|
|
106
|
-
try {
|
|
107
|
-
setDraftName("");
|
|
108
|
-
setSearchText("");
|
|
109
|
-
await newRecord(name);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
setError((err as Error).message || "Could not create the group.");
|
|
112
|
-
} finally {
|
|
113
|
-
setCreatingBusy(false);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function newRecord(name: string) {
|
|
118
|
-
const userContext = await getUserContext();
|
|
119
|
-
const groupName = name.trim();
|
|
120
|
-
const groupId = newid();
|
|
121
|
-
|
|
122
|
-
// Generate cryptographic keys for the group
|
|
123
|
-
const groupKeys = newKeys();
|
|
124
|
-
|
|
125
|
-
// NOTE: this is relying on the userContext to be able to create a group context for a group that doesn't yet exist
|
|
126
|
-
// at time of writing, this is supported but is probably unintuitive behavior so we need to watch for this changing in the future
|
|
127
|
-
const groupDataContext = userContext.getDataContext(groupId);
|
|
128
|
-
|
|
129
|
-
// Store the validated secret under its deterministic persistent-variable ID.
|
|
130
|
-
await saveGroupSecretKey(userContext.userDataContext, groupId, groupKeys.secretKey, groupKeys);
|
|
131
|
-
|
|
132
|
-
// add me to the group's users table
|
|
133
|
-
const me = await getMe();
|
|
134
|
-
await Users(groupDataContext).save(me);
|
|
135
|
-
|
|
136
|
-
// Set trust level for current user in the group context
|
|
137
|
-
await setUserTrustLevel(me.userId, TrustLevel.Trusted, groupDataContext);
|
|
138
|
-
|
|
139
|
-
// create group in both the user's and group's context
|
|
140
|
-
const group = await Groups(userContext.userDataContext).signAndSave({
|
|
141
|
-
groupId,
|
|
142
|
-
name: groupName,
|
|
143
|
-
description: "",
|
|
144
|
-
iconClassName: "bi-people-fill", // Default icon
|
|
145
|
-
founderUserId: me.userId,
|
|
146
|
-
disabled: false,
|
|
147
|
-
publicKey: groupKeys.publicKey,
|
|
148
|
-
publicBoxKey: groupKeys.publicBoxKey,
|
|
149
|
-
signature: "", // will be set via signAndSave
|
|
150
|
-
});
|
|
151
|
-
await Groups(groupDataContext).signAndSave(group);
|
|
152
|
-
|
|
153
|
-
// Auto-install peers-core for the new group
|
|
154
|
-
await packagesTools(groupId)
|
|
155
|
-
.then((tools) => tools.installDefaultPackages())
|
|
156
|
-
.catch((err: unknown) => {
|
|
157
|
-
console.error("Error auto-installing peers-core for group:", err);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
mainContentPath(`${IDENTITY_GROUPS_PATH}/${group.groupId}`);
|
|
161
|
-
return group;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
<div className="container-xxl p-3">
|
|
166
|
-
<div className="mb-3">
|
|
167
|
-
<h4 className="mb-1">
|
|
168
|
-
<i className="bi-people-fill me-2" />
|
|
169
|
-
Groups
|
|
170
|
-
</h4>
|
|
171
|
-
<div className="small text-muted">Shared spaces, membership, and access</div>
|
|
172
|
-
</div>
|
|
173
|
-
{creating ? (
|
|
174
|
-
<div className="card mb-3">
|
|
175
|
-
<div className="card-body">
|
|
176
|
-
<h6 className="card-title">Create a group</h6>
|
|
177
|
-
<form
|
|
178
|
-
className="input-group"
|
|
179
|
-
onSubmit={(event) => {
|
|
180
|
-
event.preventDefault();
|
|
181
|
-
void confirmCreate();
|
|
182
|
-
}}
|
|
183
|
-
>
|
|
184
|
-
<input
|
|
185
|
-
id="new-group-name"
|
|
186
|
-
className="form-control"
|
|
187
|
-
value={draftName}
|
|
188
|
-
onChange={(event) => setDraftName(event.target.value)}
|
|
189
|
-
placeholder="Group name"
|
|
190
|
-
aria-label="Group name"
|
|
191
|
-
/>
|
|
192
|
-
<button
|
|
193
|
-
type="submit"
|
|
194
|
-
className="btn btn-primary"
|
|
195
|
-
disabled={!draftName.trim() || creatingBusy}
|
|
196
|
-
>
|
|
197
|
-
{creatingBusy ? "Creating…" : "Create"}
|
|
198
|
-
</button>
|
|
199
|
-
<a href={`#${IDENTITY_GROUPS_PATH}`} className="btn btn-outline-secondary">
|
|
200
|
-
Cancel
|
|
201
|
-
</a>
|
|
202
|
-
</form>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
) : null}
|
|
206
|
-
<PendingWorkLink type="group" />
|
|
207
|
-
<div className="position-relative my-3">
|
|
208
|
-
<i
|
|
209
|
-
className="bi bi-search position-absolute text-muted"
|
|
210
|
-
style={{ left: 12, top: "50%", transform: "translateY(-50%)" }}
|
|
211
|
-
/>
|
|
212
|
-
<input
|
|
213
|
-
type="search"
|
|
214
|
-
className="form-control ps-5"
|
|
215
|
-
value={searchText}
|
|
216
|
-
onChange={(event) => setSearchText(event.target.value)}
|
|
217
|
-
placeholder="Search groups"
|
|
218
|
-
aria-label="Search groups"
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
{error ? <div className="alert alert-danger">{error}</div> : null}
|
|
223
|
-
{!summaries && !error ? <LoadingIndicator /> : null}
|
|
224
|
-
{summaries && filtered.length === 0 ? (
|
|
225
|
-
<div className="border rounded p-4 text-center">
|
|
226
|
-
<i className="bi bi-people fs-2 text-muted" />
|
|
227
|
-
<h6 className="mt-2">{searchText ? "No matching groups" : "No groups yet"}</h6>
|
|
228
|
-
<p className="small text-muted">
|
|
229
|
-
{searchText
|
|
230
|
-
? "Try a different name or description."
|
|
231
|
-
: "Create a shared space or use an invite to join one."}
|
|
232
|
-
</p>
|
|
233
|
-
{!searchText ? (
|
|
234
|
-
<button type="button" className="btn btn-primary btn-sm" onClick={startCreate}>
|
|
235
|
-
Create your first group
|
|
236
|
-
</button>
|
|
237
|
-
) : null}
|
|
238
|
-
</div>
|
|
239
|
-
) : null}
|
|
240
|
-
{filtered.length > 0 ? (
|
|
241
|
-
<div className="list-group">
|
|
242
|
-
{filtered.map((summary) => (
|
|
243
|
-
<GroupSummaryRow key={summary.group.groupId} summary={summary} />
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
) : null}
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Data shown for one group in the Identity list. */
|
|
252
|
-
export interface IGroupSummary {
|
|
253
|
-
/** Signed group record. */
|
|
254
|
-
group: IGroup;
|
|
255
|
-
/** Number of active memberships. */
|
|
256
|
-
memberCount: number;
|
|
257
|
-
/** Signed-in user's role in this group. */
|
|
258
|
-
myRole?: GroupMemberRole;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function loadGroupSummaries(userContext: UserContext): Promise<IGroupSummary[]> {
|
|
262
|
-
const groups = await Groups(userContext.userDataContext).list({ disabled: { $ne: true } });
|
|
263
|
-
const summaries = await Promise.all(
|
|
264
|
-
groups.map(async (group) => {
|
|
265
|
-
const members = await GroupMembers(userContext.getDataContext(group.groupId)).list({
|
|
266
|
-
deleted: { $ne: true },
|
|
267
|
-
});
|
|
268
|
-
return {
|
|
269
|
-
group,
|
|
270
|
-
memberCount: members.length,
|
|
271
|
-
myRole: members.find((member) => member.userId === userContext.userId)?.role,
|
|
272
|
-
};
|
|
273
|
-
}),
|
|
274
|
-
);
|
|
275
|
-
return summaries.sort((left, right) => left.group.name.localeCompare(right.group.name));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** Compact, fully clickable group summary row. */
|
|
279
|
-
export function GroupSummaryRow({ summary }: { summary: IGroupSummary }) {
|
|
280
|
-
const { group } = summary;
|
|
281
|
-
return (
|
|
282
|
-
<a
|
|
283
|
-
href={`#${IDENTITY_GROUPS_PATH}/${group.groupId}`}
|
|
284
|
-
className="list-group-item list-group-item-action d-flex align-items-center gap-3 py-3"
|
|
285
|
-
>
|
|
286
|
-
<span
|
|
287
|
-
className="rounded-3 bg-body-secondary d-inline-flex align-items-center justify-content-center flex-shrink-0"
|
|
288
|
-
style={{ width: 44, height: 44 }}
|
|
289
|
-
aria-hidden="true"
|
|
290
|
-
>
|
|
291
|
-
<i className={group.iconClassName || "bi bi-people-fill"} />
|
|
292
|
-
</span>
|
|
293
|
-
<span className="flex-grow-1 overflow-hidden">
|
|
294
|
-
<span className="fw-semibold d-block text-truncate">{group.name || "Unnamed group"}</span>
|
|
295
|
-
<span className="small text-muted d-block text-truncate">
|
|
296
|
-
{plainDescription(group.description) || "No description yet"}
|
|
297
|
-
</span>
|
|
298
|
-
</span>
|
|
299
|
-
<span className="d-flex align-items-center gap-2 flex-wrap justify-content-end">
|
|
300
|
-
<span className="badge text-bg-light border">
|
|
301
|
-
{summary.memberCount} member{summary.memberCount === 1 ? "" : "s"}
|
|
302
|
-
</span>
|
|
303
|
-
{summary.myRole !== undefined ? (
|
|
304
|
-
<span className="badge text-bg-secondary">{getRoleLabel(summary.myRole)}</span>
|
|
305
|
-
) : null}
|
|
306
|
-
</span>
|
|
307
|
-
<i className="bi bi-chevron-right text-muted" aria-hidden="true" />
|
|
308
|
-
</a>
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function plainDescription(markdown: string): string {
|
|
313
|
-
return markdown
|
|
314
|
-
.replace(/[#_*`>[\]()!-]/g, " ")
|
|
315
|
-
.replace(/\s+/g, " ")
|
|
316
|
-
.trim();
|
|
317
|
-
}
|