@peers-app/peers-ui 0.27.3 → 0.27.5
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/group-details-modal.d.ts +4 -0
- package/dist/screens/network-viewer/group-details-modal.js +6 -46
- package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -2
- 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,180 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
/** Props for {@link QrScanner}. */
|
|
4
|
-
export interface QrScannerProps {
|
|
5
|
-
/** Called once with the decoded text of the first QR code seen; scanning stops after this. */
|
|
6
|
-
onScan: (text: string) => void;
|
|
7
|
-
/** Called when the camera cannot be opened. */
|
|
8
|
-
onError?: (message: string) => void;
|
|
9
|
-
/** Optional filter; return `false` to keep scanning past a code that is not ours. */
|
|
10
|
-
accept?: (text: string) => boolean;
|
|
11
|
-
/** Square edge length in CSS pixels. */
|
|
12
|
-
size?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type BarcodeDetectorLike = {
|
|
16
|
-
detect(source: ImageBitmapSource): Promise<Array<{ rawValue: string }>>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type BarcodeDetectorCtor = new (options?: { formats?: string[] }) => BarcodeDetectorLike;
|
|
20
|
-
|
|
21
|
-
function nativeDetector(): BarcodeDetectorLike | undefined {
|
|
22
|
-
const ctor = (globalThis as { BarcodeDetector?: BarcodeDetectorCtor }).BarcodeDetector;
|
|
23
|
-
if (!ctor) {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
return new ctor({ formats: ["qr_code"] });
|
|
28
|
-
} catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Whether this host can plausibly scan (secure context with a camera API). */
|
|
34
|
-
export function canScanQr(): boolean {
|
|
35
|
-
return (
|
|
36
|
-
typeof navigator !== "undefined" &&
|
|
37
|
-
Boolean(navigator.mediaDevices?.getUserMedia) &&
|
|
38
|
-
(typeof window === "undefined" || window.isSecureContext !== false)
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Live camera QR scanner. Uses the platform `BarcodeDetector` when available and falls
|
|
44
|
-
* back to `jsqr` on a canvas. Stops the camera on unmount or after the first accepted code.
|
|
45
|
-
*/
|
|
46
|
-
export function QrScanner(props: QrScannerProps) {
|
|
47
|
-
const { onScan, onError, accept, size = 280 } = props;
|
|
48
|
-
const videoRef = useRef<HTMLVideoElement>(null);
|
|
49
|
-
const [status, setStatus] = useState<"starting" | "scanning" | "error">("starting");
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
let cancelled = false;
|
|
53
|
-
let stream: MediaStream | undefined;
|
|
54
|
-
let frame = 0;
|
|
55
|
-
const canvas = document.createElement("canvas");
|
|
56
|
-
const detector = nativeDetector();
|
|
57
|
-
let jsqr: typeof import("jsqr").default | undefined;
|
|
58
|
-
|
|
59
|
-
const stop = () => {
|
|
60
|
-
cancelled = true;
|
|
61
|
-
if (frame) {
|
|
62
|
-
cancelAnimationFrame(frame);
|
|
63
|
-
}
|
|
64
|
-
for (const track of stream?.getTracks() ?? []) {
|
|
65
|
-
track.stop();
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const deliver = (text: string) => {
|
|
70
|
-
if (cancelled || (accept && !accept(text))) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
stop();
|
|
74
|
-
onScan(text);
|
|
75
|
-
return true;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const tick = async () => {
|
|
79
|
-
const video = videoRef.current;
|
|
80
|
-
if (cancelled || !video || video.readyState < 2) {
|
|
81
|
-
frame = requestAnimationFrame(() => void tick());
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
if (detector) {
|
|
86
|
-
const codes = await detector.detect(video);
|
|
87
|
-
for (const code of codes) {
|
|
88
|
-
if (deliver(code.rawValue)) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
} else if (jsqr) {
|
|
93
|
-
const width = video.videoWidth;
|
|
94
|
-
const height = video.videoHeight;
|
|
95
|
-
if (width && height) {
|
|
96
|
-
canvas.width = width;
|
|
97
|
-
canvas.height = height;
|
|
98
|
-
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
99
|
-
if (ctx) {
|
|
100
|
-
ctx.drawImage(video, 0, 0, width, height);
|
|
101
|
-
const image = ctx.getImageData(0, 0, width, height);
|
|
102
|
-
const result = jsqr(image.data, width, height, { inversionAttempts: "dontInvert" });
|
|
103
|
-
if (result?.data && deliver(result.data)) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// A single bad frame is not fatal; keep scanning.
|
|
111
|
-
}
|
|
112
|
-
if (!cancelled) {
|
|
113
|
-
frame = requestAnimationFrame(() => void tick());
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
(async () => {
|
|
118
|
-
try {
|
|
119
|
-
if (!detector) {
|
|
120
|
-
jsqr = (await import("jsqr")).default;
|
|
121
|
-
}
|
|
122
|
-
stream = await navigator.mediaDevices.getUserMedia({
|
|
123
|
-
video: { facingMode: { ideal: "environment" } },
|
|
124
|
-
audio: false,
|
|
125
|
-
});
|
|
126
|
-
if (cancelled) {
|
|
127
|
-
stop();
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const video = videoRef.current;
|
|
131
|
-
if (!video) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
video.srcObject = stream;
|
|
135
|
-
await video.play();
|
|
136
|
-
setStatus("scanning");
|
|
137
|
-
frame = requestAnimationFrame(() => void tick());
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (cancelled) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
setStatus("error");
|
|
143
|
-
onError?.(
|
|
144
|
-
error instanceof DOMException && error.name === "NotAllowedError"
|
|
145
|
-
? "Camera access was denied."
|
|
146
|
-
: "Could not open the camera.",
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
})();
|
|
150
|
-
|
|
151
|
-
return stop;
|
|
152
|
-
}, [onScan, onError, accept]);
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<div
|
|
156
|
-
className="position-relative bg-black rounded overflow-hidden mx-auto"
|
|
157
|
-
style={{ width: size, height: size }}
|
|
158
|
-
>
|
|
159
|
-
<video
|
|
160
|
-
ref={videoRef}
|
|
161
|
-
muted
|
|
162
|
-
playsInline
|
|
163
|
-
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
164
|
-
aria-label="Camera preview"
|
|
165
|
-
/>
|
|
166
|
-
{status === "starting" ? (
|
|
167
|
-
<output className="position-absolute top-50 start-50 translate-middle text-light small">
|
|
168
|
-
Starting camera…
|
|
169
|
-
</output>
|
|
170
|
-
) : null}
|
|
171
|
-
{status === "scanning" ? (
|
|
172
|
-
<div
|
|
173
|
-
aria-hidden="true"
|
|
174
|
-
className="position-absolute top-50 start-50 translate-middle border border-2 border-light rounded"
|
|
175
|
-
style={{ width: "60%", height: "60%", opacity: 0.7 }}
|
|
176
|
-
/>
|
|
177
|
-
) : null}
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import userEvent from "@testing-library/user-event";
|
|
3
|
-
import { ResponsiveSectionNav } from "./responsive-section-nav";
|
|
4
|
-
|
|
5
|
-
jest.mock("../globals", () => {
|
|
6
|
-
const { observable: createObservable } =
|
|
7
|
-
jest.requireActual<typeof import("@peers-app/peers-sdk")>("@peers-app/peers-sdk");
|
|
8
|
-
return { isDesktop: createObservable(true) };
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
import { isDesktop } from "../globals";
|
|
12
|
-
|
|
13
|
-
const sections = [
|
|
14
|
-
{ id: "activity", label: "Activity", iconClassName: "bi-inbox", badge: 2 },
|
|
15
|
-
{ id: "people", label: "People", iconClassName: "bi-person" },
|
|
16
|
-
{ id: "groups", label: "Groups" },
|
|
17
|
-
{ id: "devices", label: "Devices" },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
describe("ResponsiveSectionNav", () => {
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
isDesktop(true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("uses a vertical sidebar on desktop and marks the selected section", () => {
|
|
26
|
-
render(
|
|
27
|
-
<ResponsiveSectionNav
|
|
28
|
-
label="Identity"
|
|
29
|
-
sections={sections}
|
|
30
|
-
activeSectionId="people"
|
|
31
|
-
onSelect={() => undefined}
|
|
32
|
-
toolbar={<div>Quick actions</div>}
|
|
33
|
-
>
|
|
34
|
-
<div>People body</div>
|
|
35
|
-
</ResponsiveSectionNav>,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
expect(screen.getByRole("navigation", { name: "Identity" }).querySelector("ul")).toHaveClass(
|
|
39
|
-
"nav-pills",
|
|
40
|
-
"flex-column",
|
|
41
|
-
);
|
|
42
|
-
expect(screen.getByRole("button", { name: /People/ })).toHaveAttribute("aria-current", "page");
|
|
43
|
-
expect(screen.getByText("People body")).toBeInTheDocument();
|
|
44
|
-
expect(screen.getByText("Quick actions")).toBeInTheDocument();
|
|
45
|
-
expect(screen.getByText("Quick actions").closest("[data-section-scroll-container]")).toBeNull();
|
|
46
|
-
expect(screen.getByText("People body").closest("[data-section-scroll-container]")).toHaveClass(
|
|
47
|
-
"overflow-auto",
|
|
48
|
-
);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("uses a non-wrapping horizontal strip on a narrow screen", () => {
|
|
52
|
-
isDesktop(false);
|
|
53
|
-
render(
|
|
54
|
-
<ResponsiveSectionNav
|
|
55
|
-
label="Identity"
|
|
56
|
-
sections={sections}
|
|
57
|
-
activeSectionId="activity"
|
|
58
|
-
onSelect={() => undefined}
|
|
59
|
-
>
|
|
60
|
-
<div>Activity body</div>
|
|
61
|
-
</ResponsiveSectionNav>,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
expect(screen.getByRole("navigation", { name: "Identity" }).querySelector("ul")).toHaveClass(
|
|
65
|
-
"nav-tabs",
|
|
66
|
-
"flex-nowrap",
|
|
67
|
-
"overflow-x-auto",
|
|
68
|
-
"overflow-y-hidden",
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("reports the section chosen from the keyboard-focusable button", async () => {
|
|
73
|
-
const onSelect = jest.fn();
|
|
74
|
-
render(
|
|
75
|
-
<ResponsiveSectionNav
|
|
76
|
-
label="Identity"
|
|
77
|
-
sections={sections}
|
|
78
|
-
activeSectionId="activity"
|
|
79
|
-
onSelect={onSelect}
|
|
80
|
-
>
|
|
81
|
-
<div>Activity body</div>
|
|
82
|
-
</ResponsiveSectionNav>,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
await userEvent.click(screen.getByRole("button", { name: /Groups/ }));
|
|
86
|
-
expect(onSelect).toHaveBeenCalledWith("groups");
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { isDesktop } from "../globals";
|
|
3
|
-
import { useObservable } from "../hooks";
|
|
4
|
-
|
|
5
|
-
/** One destination in {@link ResponsiveSectionNav}. */
|
|
6
|
-
export interface ISectionNavItem {
|
|
7
|
-
/** Stable id passed to {@link ResponsiveSectionNavProps.onSelect}. */
|
|
8
|
-
id: string;
|
|
9
|
-
/** Visible label. */
|
|
10
|
-
label: string;
|
|
11
|
-
/** Bootstrap icon class, without a leading markup wrapper. */
|
|
12
|
-
iconClassName?: string;
|
|
13
|
-
/** Optional count. Hidden when zero or omitted. */
|
|
14
|
-
badge?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Props for {@link ResponsiveSectionNav}. */
|
|
18
|
-
export interface ResponsiveSectionNavProps {
|
|
19
|
-
/** Accessible name for the section navigation. */
|
|
20
|
-
label: string;
|
|
21
|
-
/** Sections in display order. */
|
|
22
|
-
sections: ISectionNavItem[];
|
|
23
|
-
/** Id of the selected section. */
|
|
24
|
-
activeSectionId: string;
|
|
25
|
-
/** Called when a section button is activated. */
|
|
26
|
-
onSelect: (sectionId: string) => void;
|
|
27
|
-
/** Optional controls that remain visible above the scrolling section body. */
|
|
28
|
-
toolbar?: ReactNode;
|
|
29
|
-
/** Selected section body. */
|
|
30
|
-
children: ReactNode;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Section navigation that uses a left sidebar on a wide screen and a
|
|
35
|
-
* non-wrapping horizontal strip on a narrow one.
|
|
36
|
-
*
|
|
37
|
-
* The parent owns the route. This component only reports the selected id.
|
|
38
|
-
*/
|
|
39
|
-
export function ResponsiveSectionNav(props: ResponsiveSectionNavProps) {
|
|
40
|
-
const [desktop] = useObservable(isDesktop);
|
|
41
|
-
return (
|
|
42
|
-
<div
|
|
43
|
-
className={`d-flex h-100 overflow-hidden ${desktop ? "flex-row" : "flex-column"}`}
|
|
44
|
-
style={{ minHeight: 0, minWidth: 0 }}
|
|
45
|
-
>
|
|
46
|
-
<nav
|
|
47
|
-
aria-label={props.label}
|
|
48
|
-
className={desktop ? "border-end p-3 overflow-auto" : "border-bottom px-2 pt-2 pb-1"}
|
|
49
|
-
style={desktop ? { flex: "0 0 220px", width: 220 } : undefined}
|
|
50
|
-
>
|
|
51
|
-
<ul
|
|
52
|
-
className={`nav ${desktop ? "nav-pills flex-column" : "nav-tabs flex-nowrap overflow-x-auto overflow-y-hidden"}`}
|
|
53
|
-
>
|
|
54
|
-
{props.sections.map((section) => {
|
|
55
|
-
const selected = section.id === props.activeSectionId;
|
|
56
|
-
return (
|
|
57
|
-
<li className="nav-item" key={section.id}>
|
|
58
|
-
<button
|
|
59
|
-
type="button"
|
|
60
|
-
className={`nav-link d-flex align-items-center gap-2 w-100 text-start ${selected ? "active" : ""}`}
|
|
61
|
-
aria-current={selected ? "page" : undefined}
|
|
62
|
-
onClick={() => props.onSelect(section.id)}
|
|
63
|
-
>
|
|
64
|
-
{section.iconClassName ? <i className={section.iconClassName} /> : null}
|
|
65
|
-
<span>{section.label}</span>
|
|
66
|
-
{section.badge ? (
|
|
67
|
-
<span className={`badge ${selected ? "bg-light text-primary" : "bg-primary"}`}>
|
|
68
|
-
{section.badge}
|
|
69
|
-
</span>
|
|
70
|
-
) : null}
|
|
71
|
-
</button>
|
|
72
|
-
</li>
|
|
73
|
-
);
|
|
74
|
-
})}
|
|
75
|
-
</ul>
|
|
76
|
-
</nav>
|
|
77
|
-
<div
|
|
78
|
-
className="d-flex flex-column flex-grow-1 overflow-hidden"
|
|
79
|
-
style={{ minHeight: 0, minWidth: 0 }}
|
|
80
|
-
>
|
|
81
|
-
{props.toolbar ? <div className="flex-shrink-0">{props.toolbar}</div> : null}
|
|
82
|
-
<div
|
|
83
|
-
className="flex-grow-1 overflow-auto"
|
|
84
|
-
style={{ minHeight: 0, minWidth: 0 }}
|
|
85
|
-
data-section-scroll-container
|
|
86
|
-
>
|
|
87
|
-
{props.children}
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import * as globals from "../globals";
|
|
2
|
-
import { useObservable } from "../hooks";
|
|
3
|
-
import { AssistantDetails } from "../screens/assistants/assistant-details";
|
|
4
|
-
import { AssistantList } from "../screens/assistants/assistant-list";
|
|
5
|
-
import { canonicalizeRoute } from "../screens/identity/identity-routes";
|
|
6
|
-
import { PackageDetails } from "../screens/packages/package-details";
|
|
7
|
-
import { PackageList } from "../screens/packages/package-list";
|
|
8
|
-
import { PackageNewLocal } from "../screens/packages/package-new-local";
|
|
9
|
-
import { PeerTypeDetails } from "../screens/peer-types/peer-type-details";
|
|
10
|
-
import { PeerTypeLists } from "../screens/peer-types/peer-type-list";
|
|
11
|
-
import { GlobalSearch } from "../screens/search/global-search";
|
|
12
|
-
import { SettingsPage } from "../screens/settings/settings-page";
|
|
13
|
-
// import { TaskDetails } from "../screens/tasks/task-details";
|
|
14
|
-
// import { TaskList } from "../screens/tasks/task-list";
|
|
15
|
-
import { ToolDetails } from "../screens/tools/tool-details";
|
|
16
|
-
import { ToolList } from "../screens/tools/tool-list";
|
|
17
|
-
import { ToolTestDetails } from "../screens/tools/tool-test-details";
|
|
18
|
-
import { VariableDetails } from "../screens/variables/variable-details";
|
|
19
|
-
import { VariableList } from "../screens/variables/variable-list";
|
|
20
|
-
import { WorkflowDetails } from "../screens/workflows/workflow-details";
|
|
21
|
-
import { WorkflowList } from "../screens/workflows/workflow-list";
|
|
22
|
-
import { UIRouter } from "../ui-router/ui-loader";
|
|
23
|
-
import { ChannelMessages } from "./messages/channel-view";
|
|
24
|
-
import { ThreadMessageList } from "./messages/thread-message-list";
|
|
25
|
-
import "../screens/console-logs/console-logs-list";
|
|
26
|
-
import "../screens/identity";
|
|
27
|
-
import "../screens/network-viewer";
|
|
28
|
-
import "../screens/data-explorer";
|
|
29
|
-
|
|
30
|
-
/** Render the built-in or registered UI that owns a canonicalized route. */
|
|
31
|
-
export function Router({ path: providedPath }: { path?: string } = {}) {
|
|
32
|
-
// Hooks must run unconditionally (same order every render)
|
|
33
|
-
const [globalPath] = useObservable(globals.mainContentPath);
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// Use provided path or fall back to global mainContentPath.
|
|
37
|
-
// Legacy contacts, groups, devices, invites, and account paths render as Identity.
|
|
38
|
-
let path = canonicalizeRoute(providedPath ?? globalPath);
|
|
39
|
-
const originalPath = path;
|
|
40
|
-
path = path.toLowerCase().trim();
|
|
41
|
-
path = path.split("?")[0]; // remove query string
|
|
42
|
-
|
|
43
|
-
// remove leading '/'
|
|
44
|
-
if (path.startsWith("/")) {
|
|
45
|
-
path = path.substring(1);
|
|
46
|
-
}
|
|
47
|
-
// remove trailing '/'
|
|
48
|
-
if (path.endsWith("/")) {
|
|
49
|
-
path = path.substring(0, path.length - 1);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (path === "search") {
|
|
53
|
-
return <GlobalSearch />;
|
|
54
|
-
}
|
|
55
|
-
if (path === "settings" || path === "profile") {
|
|
56
|
-
return <SettingsPage />;
|
|
57
|
-
}
|
|
58
|
-
if (path === "threads" || path === "") {
|
|
59
|
-
return <ChannelMessages />;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const ui = UIRouter({ path, props: {}, uiCategory: "screen" });
|
|
63
|
-
if (ui) {
|
|
64
|
-
return ui;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (path.match(/^channel\//)) {
|
|
68
|
-
const parts = path.split("/");
|
|
69
|
-
parts.shift();
|
|
70
|
-
const channelId = parts[0];
|
|
71
|
-
// todo add additional params as props if they exist
|
|
72
|
-
return <ChannelMessages channelId={channelId} />;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (path === "assistants") {
|
|
76
|
-
return <AssistantList />;
|
|
77
|
-
}
|
|
78
|
-
if (path.match(/^assistants\//)) {
|
|
79
|
-
const parts = path.split("/");
|
|
80
|
-
parts.shift();
|
|
81
|
-
const id = parts[0];
|
|
82
|
-
return <AssistantDetails assistantId={id} />;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// if (path === 'tasks') {
|
|
86
|
-
// return <TaskList />;
|
|
87
|
-
// }
|
|
88
|
-
// if (path.match(/^tasks\//)) {
|
|
89
|
-
// const parts = path.split('/');
|
|
90
|
-
// parts.shift();
|
|
91
|
-
// const taskId = parts[0];
|
|
92
|
-
// return <TaskDetails taskId={taskId} />;
|
|
93
|
-
// }
|
|
94
|
-
|
|
95
|
-
if (path === "workflows") {
|
|
96
|
-
return <WorkflowList />;
|
|
97
|
-
}
|
|
98
|
-
if (path.match(/^workflows\//)) {
|
|
99
|
-
const parts = path.split("/");
|
|
100
|
-
parts.shift();
|
|
101
|
-
const workflowId = parts[0];
|
|
102
|
-
return <WorkflowDetails workflowId={workflowId} />;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (path === "tools") {
|
|
106
|
-
return <ToolList />;
|
|
107
|
-
}
|
|
108
|
-
if (path.match(/^tools\/tests\//)) {
|
|
109
|
-
const parts = path.split("/");
|
|
110
|
-
parts.shift();
|
|
111
|
-
parts.shift();
|
|
112
|
-
const toolTestId = parts[0];
|
|
113
|
-
return <ToolTestDetails toolTestId={toolTestId} />;
|
|
114
|
-
}
|
|
115
|
-
if (path.match(/^tools\//)) {
|
|
116
|
-
const parts = path.split("/");
|
|
117
|
-
parts.shift();
|
|
118
|
-
const toolId = parts[0];
|
|
119
|
-
return <ToolDetails toolId={toolId} />;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (path === "packages") {
|
|
123
|
-
return <PackageList />;
|
|
124
|
-
}
|
|
125
|
-
if (path.match(/^packages\//)) {
|
|
126
|
-
const parts = path.split("/");
|
|
127
|
-
parts.shift();
|
|
128
|
-
const id = parts[0];
|
|
129
|
-
if (id.toLowerCase() === "newlocal") {
|
|
130
|
-
return <PackageNewLocal />;
|
|
131
|
-
}
|
|
132
|
-
return <PackageDetails packageId={id} />;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (path === "variables") {
|
|
136
|
-
return <VariableList />;
|
|
137
|
-
}
|
|
138
|
-
if (path.match(/^variables\//)) {
|
|
139
|
-
const parts = path.split("/");
|
|
140
|
-
parts.shift();
|
|
141
|
-
const id = parts[0];
|
|
142
|
-
return <VariableDetails persistentVarId={id} />;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (path === "peer-types") {
|
|
146
|
-
return <PeerTypeLists />;
|
|
147
|
-
}
|
|
148
|
-
if (path.match(/^peer-types\//)) {
|
|
149
|
-
const parts = path.split("/");
|
|
150
|
-
parts.shift();
|
|
151
|
-
const id = parts[0];
|
|
152
|
-
return <PeerTypeDetails peerTypeId={id} />;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (path.match(/^threads\//)) {
|
|
156
|
-
const parts = path.split("/");
|
|
157
|
-
parts.shift();
|
|
158
|
-
const threadId = parts[0];
|
|
159
|
-
return <ThreadMessageList threadId={threadId} />;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
throw new Error(`Unrecognized route: ${originalPath}`);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return (
|
|
165
|
-
<div className="p-3">
|
|
166
|
-
<a href="#threads">Home</a>
|
|
167
|
-
<h3>Error loading route</h3>
|
|
168
|
-
<pre>
|
|
169
|
-
route: {`${globals.mainContentPath()}\n`}
|
|
170
|
-
{String(err)}
|
|
171
|
-
</pre>
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import type { IDoc } from "@peers-app/peers-sdk";
|
|
2
|
-
import type React from "react";
|
|
3
|
-
import { isDesktop, mainContentPath } from "../globals";
|
|
4
|
-
import { useObservable } from "../hooks";
|
|
5
|
-
|
|
6
|
-
/** Props for {@link SaveButton}. */
|
|
7
|
-
export interface ISaveButtonProps {
|
|
8
|
-
/** Used with many concrete table row shapes; IDoc stays generic at call sites. */
|
|
9
|
-
doc: IDoc<any>;
|
|
10
|
-
/** Save handler; defaults to `doc.save`. */
|
|
11
|
-
onClick?: () => void | Promise<void>;
|
|
12
|
-
/** Extra dropdown actions: buttons, or arbitrary nodes such as dividers. */
|
|
13
|
-
addActions?: (
|
|
14
|
-
| {
|
|
15
|
-
label: string;
|
|
16
|
-
onClick: () => void;
|
|
17
|
-
className?: string;
|
|
18
|
-
}
|
|
19
|
-
| React.ReactNode
|
|
20
|
-
)[];
|
|
21
|
-
/** Hide the built-in Discard Changes and Delete actions. */
|
|
22
|
-
noBuiltInActions?: boolean;
|
|
23
|
-
/** Label for the built-in delete action. Default: `Delete`. */
|
|
24
|
-
deleteLabel?: string;
|
|
25
|
-
/**
|
|
26
|
-
* Replace the built-in delete (`doc.delete()`), for records whose removal must go through
|
|
27
|
-
* a Runtime contract. Runs after the user confirms; the screen then navigates up.
|
|
28
|
-
*/
|
|
29
|
-
onDelete?: () => void | Promise<void>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Save button with a dropdown of extra actions plus built-in Discard Changes and Delete.
|
|
34
|
-
* Enabled only while the document has unsaved changes.
|
|
35
|
-
*/
|
|
36
|
-
export const SaveButton = (props: ISaveButtonProps) => {
|
|
37
|
-
let { doc, addActions } = props;
|
|
38
|
-
useObservable(doc.q);
|
|
39
|
-
const q = doc.q;
|
|
40
|
-
|
|
41
|
-
if (!addActions) {
|
|
42
|
-
addActions = [];
|
|
43
|
-
}
|
|
44
|
-
const allActions: ISaveButtonProps["addActions"] = props.addActions ? [...addActions] : [];
|
|
45
|
-
|
|
46
|
-
if (!props.noBuiltInActions) {
|
|
47
|
-
if (q()) {
|
|
48
|
-
allActions.unshift({
|
|
49
|
-
label: "Discard Changes",
|
|
50
|
-
className: "text-bg-warning",
|
|
51
|
-
onClick: () => {
|
|
52
|
-
if (confirm("Are you sure you want to discard your changes?")) {
|
|
53
|
-
doc.load().then(() => {
|
|
54
|
-
setTimeout(() => doc.q(0), 100);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (allActions.length) {
|
|
61
|
-
allActions.push(<hr className="dropdown-divider mt-3" />);
|
|
62
|
-
}
|
|
63
|
-
const deleteLabel = props.deleteLabel ?? "Delete";
|
|
64
|
-
allActions.push({
|
|
65
|
-
label: deleteLabel,
|
|
66
|
-
className: `text-bg-danger`,
|
|
67
|
-
onClick: async () => {
|
|
68
|
-
if (confirm(`Are you sure you want to ${deleteLabel.toLowerCase()} this?`)) {
|
|
69
|
-
try {
|
|
70
|
-
await (props.onDelete ? props.onDelete() : doc.delete());
|
|
71
|
-
} catch (err) {
|
|
72
|
-
alert(`${deleteLabel} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const pathParts = mainContentPath().split("/");
|
|
76
|
-
pathParts.pop();
|
|
77
|
-
const path = pathParts.join("/") ?? "";
|
|
78
|
-
mainContentPath(path);
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let className = `btn `;
|
|
85
|
-
if (q()) {
|
|
86
|
-
className += "btn-primary";
|
|
87
|
-
} else {
|
|
88
|
-
className += "disabled";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!addActions?.length && props.noBuiltInActions) {
|
|
92
|
-
return (
|
|
93
|
-
<button className={className} onClick={props.onClick ?? doc.save} disabled={!q()}>
|
|
94
|
-
{isDesktop() ? "Save Changes" : "Save"}
|
|
95
|
-
</button>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div style={{ display: "inline-block" }}>
|
|
101
|
-
<div className="input-group">
|
|
102
|
-
<button className={className} onClick={props.onClick ?? doc.save} disabled={!q()}>
|
|
103
|
-
{isDesktop() ? "Save Changes" : "Save"}
|
|
104
|
-
</button>
|
|
105
|
-
<button
|
|
106
|
-
type="button"
|
|
107
|
-
data-bs-toggle="dropdown"
|
|
108
|
-
aria-expanded="false"
|
|
109
|
-
// className={`btn dropdown-toggle dropdown-toggle-split ${q() ? 'btn-primary' : 'btn-secondary'}`}
|
|
110
|
-
className={`btn dropdown-toggle dropdown-toggle-split btn-secondary`}
|
|
111
|
-
>
|
|
112
|
-
<span className="visually-hidden">Toggle Dropdown</span>
|
|
113
|
-
</button>
|
|
114
|
-
<ul className="dropdown-menu dropdown-menu-end">
|
|
115
|
-
{allActions.map((action) => {
|
|
116
|
-
if (typeof action !== "object" || action === null || !("label" in action)) {
|
|
117
|
-
return <li key={Math.random()}>{action}</li>;
|
|
118
|
-
}
|
|
119
|
-
return (
|
|
120
|
-
<li key={action.label}>
|
|
121
|
-
<button
|
|
122
|
-
className={`dropdown-item rounded mt-2 ${action.className || ""}`}
|
|
123
|
-
onClick={action.onClick}
|
|
124
|
-
>
|
|
125
|
-
{action.label}
|
|
126
|
-
</button>
|
|
127
|
-
</li>
|
|
128
|
-
);
|
|
129
|
-
})}
|
|
130
|
-
</ul>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
|
-
};
|