@anymux/connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GitBrowser-BLgTNQyd.js +905 -0
- package/dist/GitBrowser-BLgTNQyd.js.map +1 -0
- package/dist/GitBrowser-CIyWiuX-.js +3 -0
- package/dist/ObjectStorageBrowser-B2YkUxMl.js +3 -0
- package/dist/ObjectStorageBrowser-B_25Emfu.js +267 -0
- package/dist/ObjectStorageBrowser-B_25Emfu.js.map +1 -0
- package/dist/RepoPicker-BprFGOn7.js +3 -0
- package/dist/RepoPicker-CoHMiJ-3.js +168 -0
- package/dist/RepoPicker-CoHMiJ-3.js.map +1 -0
- package/dist/index.d.ts +697 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2539 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +3 -0
- package/dist/scope-labels-B4VAwoL6.js +582 -0
- package/dist/scope-labels-B4VAwoL6.js.map +1 -0
- package/dist/scope-labels-DvdJLcSL.d.ts +50 -0
- package/dist/scope-labels-DvdJLcSL.d.ts.map +1 -0
- package/package.json +87 -0
- package/src/adapters/adapter-registry.ts +177 -0
- package/src/auth/auth-client.ts +101 -0
- package/src/auth/token-manager.ts +27 -0
- package/src/components/ActionHistoryPanel.tsx +137 -0
- package/src/components/CapabilityCell.tsx +97 -0
- package/src/components/CapabilityError.tsx +50 -0
- package/src/components/CapabilityPanel.tsx +530 -0
- package/src/components/CapabilityPill.tsx +56 -0
- package/src/components/ConnectButton.tsx +149 -0
- package/src/components/ConnectedMenu.tsx +142 -0
- package/src/components/ConnectionStatus.tsx +28 -0
- package/src/components/CredentialForm.tsx +246 -0
- package/src/components/FullScreenBrowser.tsx +84 -0
- package/src/components/GitBrowser.tsx +705 -0
- package/src/components/GitHubRepoPicker.tsx +125 -0
- package/src/components/ObjectStorageBrowser.tsx +176 -0
- package/src/components/RepoPicker.tsx +93 -0
- package/src/components/ServiceCard.tsx +77 -0
- package/src/components/ServiceCardGrid.tsx +141 -0
- package/src/components/ServiceDashboard.tsx +84 -0
- package/src/components/ServiceIcon.tsx +37 -0
- package/src/components/ServiceRow.tsx +50 -0
- package/src/components/useAdapter.ts +33 -0
- package/src/demos/ServiceDashboardDemo.tsx +108 -0
- package/src/index.ts +68 -0
- package/src/models/ActionNotificationModel.ts +72 -0
- package/src/models/ConnectionManagerModel.ts +410 -0
- package/src/models/CredentialFormModel.ts +111 -0
- package/src/models/DashboardModel.ts +157 -0
- package/src/models/GitHostBrowserModel.ts +89 -0
- package/src/models/GitRepoBrowserModel.ts +285 -0
- package/src/models/ObjectStorageBrowserModel.ts +131 -0
- package/src/models/RepoPickerModel.ts +132 -0
- package/src/registry/service-registry.ts +46 -0
- package/src/registry/services/apple.ts +22 -0
- package/src/registry/services/bitbucket.ts +24 -0
- package/src/registry/services/box.ts +22 -0
- package/src/registry/services/browser-fs.ts +19 -0
- package/src/registry/services/dropbox.ts +22 -0
- package/src/registry/services/flickr.ts +22 -0
- package/src/registry/services/gitea.ts +24 -0
- package/src/registry/services/github.ts +24 -0
- package/src/registry/services/gitlab.ts +24 -0
- package/src/registry/services/google.ts +24 -0
- package/src/registry/services/icloud.ts +23 -0
- package/src/registry/services/indexeddb.ts +19 -0
- package/src/registry/services/instagram.ts +22 -0
- package/src/registry/services/microsoft.ts +24 -0
- package/src/registry/services/s3.ts +21 -0
- package/src/registry/services/webdav.ts +21 -0
- package/src/registry.ts +4 -0
- package/src/types/connection-state.ts +33 -0
- package/src/types/connection.ts +11 -0
- package/src/types/optional-deps.d.ts +149 -0
- package/src/types/service.ts +18 -0
- package/src/types/user-profile.ts +21 -0
- package/src/utils/action-toast.ts +53 -0
- package/src/utils/scope-labels.ts +91 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { ConnectionManagerModel } from '../models/ConnectionManagerModel';
|
|
4
|
+
import type { DashboardModel } from '../models/DashboardModel';
|
|
5
|
+
import { serviceRegistry } from '../registry/service-registry';
|
|
6
|
+
import { ServiceRow } from './ServiceRow';
|
|
7
|
+
import { ServiceCard } from './ServiceCard';
|
|
8
|
+
import { FullScreenBrowser } from './FullScreenBrowser';
|
|
9
|
+
|
|
10
|
+
const COLUMN_HEADERS = [
|
|
11
|
+
{ label: 'Service', className: 'text-left' },
|
|
12
|
+
{ label: 'Status', className: 'text-left' },
|
|
13
|
+
{ label: 'FS', className: 'text-center' },
|
|
14
|
+
{ label: 'Obj Storage', className: 'text-center' },
|
|
15
|
+
{ label: 'Git', className: 'text-center' },
|
|
16
|
+
{ label: 'Media', className: 'text-center' },
|
|
17
|
+
{ label: 'Contacts', className: 'text-center' },
|
|
18
|
+
{ label: 'Calendar', className: 'text-center' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
interface ServiceDashboardProps {
|
|
22
|
+
connectionManager: ConnectionManagerModel;
|
|
23
|
+
dashboardModel: DashboardModel;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ServiceDashboard: React.FC<ServiceDashboardProps> = observer(
|
|
27
|
+
({ connectionManager, dashboardModel }) => {
|
|
28
|
+
const services = serviceRegistry.getAll();
|
|
29
|
+
|
|
30
|
+
if (dashboardModel.panelOpen && dashboardModel.selectedCell) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="animate-in fade-in duration-200 h-full flex flex-col min-h-0">
|
|
33
|
+
<FullScreenBrowser dashboardModel={dashboardModel} connectionManager={connectionManager} />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
{/* Desktop view — table */}
|
|
41
|
+
<div className="hidden md:block">
|
|
42
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
43
|
+
<table className="w-full text-sm">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr className="bg-muted border-b border-border">
|
|
46
|
+
{COLUMN_HEADERS.map((col) => (
|
|
47
|
+
<th
|
|
48
|
+
key={col.label}
|
|
49
|
+
className={`px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider ${col.className}`}
|
|
50
|
+
>
|
|
51
|
+
{col.label}
|
|
52
|
+
</th>
|
|
53
|
+
))}
|
|
54
|
+
</tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody className="bg-card divide-y divide-border">
|
|
57
|
+
{services.map((service) => (
|
|
58
|
+
<ServiceRow
|
|
59
|
+
key={service.id}
|
|
60
|
+
service={service}
|
|
61
|
+
connectionManager={connectionManager}
|
|
62
|
+
dashboardModel={dashboardModel}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Mobile: card layout (always) */}
|
|
71
|
+
<div className="md:hidden space-y-3">
|
|
72
|
+
{services.map((service) => (
|
|
73
|
+
<ServiceCard
|
|
74
|
+
key={service.id}
|
|
75
|
+
service={service}
|
|
76
|
+
connectionManager={connectionManager}
|
|
77
|
+
dashboardModel={dashboardModel}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as icons from 'lucide-react';
|
|
3
|
+
import type { LucideProps } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
/** Custom icons for services not available in lucide-react */
|
|
6
|
+
const AppleIcon: React.FC<LucideProps> = ({ size = 24, color = 'currentColor', strokeWidth: _sw, ...props }) => (
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
width={size}
|
|
10
|
+
height={size}
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill={color}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const customIcons: Record<string, React.FC<LucideProps>> = {
|
|
20
|
+
Apple: AppleIcon,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface ServiceIconProps extends LucideProps {
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ServiceIcon: React.FC<ServiceIconProps> = ({ name, ...props }) => {
|
|
28
|
+
const CustomIcon = customIcons[name];
|
|
29
|
+
if (CustomIcon) {
|
|
30
|
+
return <CustomIcon {...props} />;
|
|
31
|
+
}
|
|
32
|
+
const Icon = (icons as unknown as Record<string, React.FC<LucideProps>>)[name];
|
|
33
|
+
if (!Icon) {
|
|
34
|
+
return <icons.HelpCircle {...props} />;
|
|
35
|
+
}
|
|
36
|
+
return <Icon {...props} />;
|
|
37
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import type { CapabilityId, ServiceDefinition } from '../types/service';
|
|
4
|
+
import type { ConnectionManagerModel } from '../models/ConnectionManagerModel';
|
|
5
|
+
import type { DashboardModel } from '../models/DashboardModel';
|
|
6
|
+
import { ServiceIcon } from './ServiceIcon';
|
|
7
|
+
import { ConnectButton } from './ConnectButton';
|
|
8
|
+
import { CapabilityCell } from './CapabilityCell';
|
|
9
|
+
|
|
10
|
+
const CAPABILITY_COLUMNS: CapabilityId[] = [
|
|
11
|
+
'file-system',
|
|
12
|
+
'object-storage',
|
|
13
|
+
'git-repo',
|
|
14
|
+
'media',
|
|
15
|
+
'contacts',
|
|
16
|
+
'calendar',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
interface ServiceRowProps {
|
|
20
|
+
service: ServiceDefinition;
|
|
21
|
+
connectionManager: ConnectionManagerModel;
|
|
22
|
+
dashboardModel: DashboardModel;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ServiceRow: React.FC<ServiceRowProps> = observer(
|
|
26
|
+
({ service, connectionManager, dashboardModel }) => {
|
|
27
|
+
return (
|
|
28
|
+
<tr className="border-b border-border h-11">
|
|
29
|
+
<td className="px-3 py-2 align-middle">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<ServiceIcon name={service.icon} className="h-4 w-4" style={{ color: service.color }} />
|
|
32
|
+
<a href={`/providers/${service.id}`} className="text-sm font-medium truncate hover:underline" title={service.name}>{service.name}</a>
|
|
33
|
+
</div>
|
|
34
|
+
</td>
|
|
35
|
+
<td className="px-3 py-2 align-middle">
|
|
36
|
+
<ConnectButton service={service} connectionManager={connectionManager} />
|
|
37
|
+
</td>
|
|
38
|
+
{CAPABILITY_COLUMNS.map((cap) => (
|
|
39
|
+
<CapabilityCell
|
|
40
|
+
key={cap}
|
|
41
|
+
service={service}
|
|
42
|
+
capabilityId={cap}
|
|
43
|
+
connectionManager={connectionManager}
|
|
44
|
+
dashboardModel={dashboardModel}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</tr>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Hook to async-create an adapter, run a pre-flight check, and track loading/error state */
|
|
4
|
+
export function useAdapter<T>(
|
|
5
|
+
factory: () => Promise<T>,
|
|
6
|
+
deps: unknown[],
|
|
7
|
+
preflight?: (adapter: T) => Promise<void>
|
|
8
|
+
): { adapter: T | null; loading: boolean; error: string | null; retry: () => void } {
|
|
9
|
+
const [adapter, setAdapter] = useState<T | null>(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
setAdapter(null);
|
|
19
|
+
|
|
20
|
+
factory()
|
|
21
|
+
.then(async (a) => {
|
|
22
|
+
// Run pre-flight check (e.g. readdir) to catch auth errors before mounting UI
|
|
23
|
+
if (preflight) await preflight(a);
|
|
24
|
+
if (!cancelled) { setAdapter(a); setLoading(false); }
|
|
25
|
+
})
|
|
26
|
+
.catch((e) => { if (!cancelled) { setError(e instanceof Error ? e.message : String(e)); setLoading(false); } });
|
|
27
|
+
|
|
28
|
+
return () => { cancelled = true; };
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, [...deps, retryCount]);
|
|
31
|
+
|
|
32
|
+
return { adapter, loading, error, retry: () => setRetryCount((c) => c + 1) };
|
|
33
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { ConnectionManagerModel } from '../models/ConnectionManagerModel';
|
|
4
|
+
import { DashboardModel } from '../models/DashboardModel';
|
|
5
|
+
import { ServiceDashboard } from '../components/ServiceDashboard';
|
|
6
|
+
import { createConnectAuthClient } from '../auth/auth-client';
|
|
7
|
+
import { serviceRegistry } from '../registry/service-registry';
|
|
8
|
+
|
|
9
|
+
export interface ServiceDashboardDemoProps {
|
|
10
|
+
authBaseURL: string;
|
|
11
|
+
/** Initial service to open (from URL) */
|
|
12
|
+
initialService?: string;
|
|
13
|
+
/** Initial capability to open (from URL) */
|
|
14
|
+
initialCapability?: string;
|
|
15
|
+
/** Initial file browser path (from URL) */
|
|
16
|
+
initialPath?: string;
|
|
17
|
+
/** Callback when selected cell changes (for URL sync) */
|
|
18
|
+
onCellChange?: (cell: { serviceId: string; capabilityId: string } | null) => void;
|
|
19
|
+
/** Callback when file browser path changes (for URL sync) */
|
|
20
|
+
onPathChange?: (path: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ServiceDashboardDemo: React.FC<ServiceDashboardDemoProps> = observer(({ authBaseURL, initialService, initialCapability, initialPath, onCellChange, onPathChange }) => {
|
|
24
|
+
const connectionManager = useMemo(() => {
|
|
25
|
+
const authClient = createConnectAuthClient(authBaseURL);
|
|
26
|
+
return new ConnectionManagerModel({ authClient });
|
|
27
|
+
}, [authBaseURL]);
|
|
28
|
+
|
|
29
|
+
const dashboardModel = useMemo(() => {
|
|
30
|
+
const model = new DashboardModel(connectionManager);
|
|
31
|
+
// Restore panel state immediately from URL if service is already connected
|
|
32
|
+
// (loadFromStorage runs synchronously in constructor, so tokens are available here)
|
|
33
|
+
if (initialService && initialCapability && connectionManager.isConnected(initialService)) {
|
|
34
|
+
if (initialPath) model.setBrowserPathSilent(initialPath);
|
|
35
|
+
model.openCell(initialService, initialCapability as any);
|
|
36
|
+
}
|
|
37
|
+
return model;
|
|
38
|
+
}, [connectionManager]);
|
|
39
|
+
|
|
40
|
+
// Wire callbacks directly (no hooks needed — observer re-renders on prop change)
|
|
41
|
+
dashboardModel.onCellChange = onCellChange;
|
|
42
|
+
dashboardModel.onPathChange = onPathChange;
|
|
43
|
+
|
|
44
|
+
// Initialize connection manager once
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
connectionManager.initialize();
|
|
47
|
+
}, [connectionManager]);
|
|
48
|
+
|
|
49
|
+
// Bidirectional URL ↔ Model sync (handles browser back/forward, late connections, and cell selection)
|
|
50
|
+
const isConnected = initialService ? connectionManager.isConnected(initialService) : false;
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// URL has no cell → close panel if open (browser back to dashboard)
|
|
53
|
+
if (!initialService || !initialCapability) {
|
|
54
|
+
if (dashboardModel.panelOpen) {
|
|
55
|
+
dashboardModel.closePanelSilent();
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wait for connection manager to be ready and service connected
|
|
61
|
+
if (!connectionManager.initialized || !isConnected) return;
|
|
62
|
+
|
|
63
|
+
// URL has a cell — open panel if it doesn't match current model state
|
|
64
|
+
const modelCell = dashboardModel.selectedCell;
|
|
65
|
+
if (!dashboardModel.panelOpen ||
|
|
66
|
+
modelCell?.serviceId !== initialService ||
|
|
67
|
+
modelCell?.capabilityId !== initialCapability) {
|
|
68
|
+
if (initialPath) dashboardModel.setBrowserPathSilent(initialPath);
|
|
69
|
+
dashboardModel.openCell(initialService, initialCapability as any);
|
|
70
|
+
}
|
|
71
|
+
}, [initialService, initialCapability, initialPath, isConnected, connectionManager.initialized, dashboardModel]);
|
|
72
|
+
|
|
73
|
+
if (connectionManager.configError) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="p-4">
|
|
76
|
+
<div className="rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/30 p-4">
|
|
77
|
+
<p className="text-sm font-medium text-red-800 dark:text-red-300">Dashboard cannot start</p>
|
|
78
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{connectionManager.configError}</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Show banner when URL requests a service+capability that isn't connected
|
|
85
|
+
const showNotConnectedBanner =
|
|
86
|
+
initialService && initialCapability &&
|
|
87
|
+
connectionManager.initialized &&
|
|
88
|
+
!connectionManager.isConnected(initialService) &&
|
|
89
|
+
!dashboardModel.panelOpen;
|
|
90
|
+
const bannerService = showNotConnectedBanner ? serviceRegistry.get(initialService) : null;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={dashboardModel.panelOpen ? 'flex-1 flex flex-col min-h-0' : 'flex-1 min-h-0 overflow-auto p-4'}>
|
|
94
|
+
{showNotConnectedBanner && bannerService && (
|
|
95
|
+
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 p-4">
|
|
96
|
+
<p className="text-sm font-medium text-amber-800 dark:text-amber-300">
|
|
97
|
+
{bannerService.name} is not connected
|
|
98
|
+
</p>
|
|
99
|
+
<p className="mt-1 text-sm text-amber-600 dark:text-amber-400">
|
|
100
|
+
Click the <strong>Connect</strong> button next to {bannerService.name} in the table below to sign in.
|
|
101
|
+
The browser will open automatically once connected.
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
<ServiceDashboard connectionManager={connectionManager} dashboardModel={dashboardModel} />
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type { CapabilityId, ServiceCapability, ServiceDefinition } from './types/service';
|
|
3
|
+
export type { ConnectionStatus, ConnectedService } from './types/connection';
|
|
4
|
+
export type { IUserProfile } from './types/user-profile';
|
|
5
|
+
|
|
6
|
+
// Registry
|
|
7
|
+
export { serviceRegistry } from './registry/service-registry';
|
|
8
|
+
export { googleService } from './registry/services/google';
|
|
9
|
+
export { dropboxService } from './registry/services/dropbox';
|
|
10
|
+
export { githubService } from './registry/services/github';
|
|
11
|
+
export { microsoftService } from './registry/services/microsoft';
|
|
12
|
+
export { s3Service } from './registry/services/s3';
|
|
13
|
+
export { webdavService } from './registry/services/webdav';
|
|
14
|
+
export { gitlabService } from './registry/services/gitlab';
|
|
15
|
+
export { bitbucketService } from './registry/services/bitbucket';
|
|
16
|
+
export { giteaService } from './registry/services/gitea';
|
|
17
|
+
export { browserFsService } from './registry/services/browser-fs';
|
|
18
|
+
export { indexeddbService } from './registry/services/indexeddb';
|
|
19
|
+
export { icloudService } from './registry/services/icloud';
|
|
20
|
+
|
|
21
|
+
// Auth
|
|
22
|
+
export { createConnectAuthClient } from './auth/auth-client';
|
|
23
|
+
export type { ConnectAuthClient } from './auth/auth-client';
|
|
24
|
+
export { TokenManager } from './auth/token-manager';
|
|
25
|
+
|
|
26
|
+
// Models
|
|
27
|
+
export { ConnectionManagerModel } from './models/ConnectionManagerModel';
|
|
28
|
+
export { DashboardModel } from './models/DashboardModel';
|
|
29
|
+
export type { SelectedCell } from './models/DashboardModel';
|
|
30
|
+
export { GitRepoBrowserModel } from './models/GitRepoBrowserModel';
|
|
31
|
+
export type { SidebarTab } from './models/GitRepoBrowserModel';
|
|
32
|
+
export { ObjectStorageBrowserModel } from './models/ObjectStorageBrowserModel';
|
|
33
|
+
export type { DisplayObject } from './models/ObjectStorageBrowserModel';
|
|
34
|
+
export { RepoPickerModel } from './models/RepoPickerModel';
|
|
35
|
+
export type { RepoInfo } from './models/RepoPickerModel';
|
|
36
|
+
export { GitHostBrowserModel } from './models/GitHostBrowserModel';
|
|
37
|
+
export type { HostTab } from './models/GitHostBrowserModel';
|
|
38
|
+
export { ActionNotificationModel } from './models/ActionNotificationModel';
|
|
39
|
+
export type { ActionRecord, ActionType } from './models/ActionNotificationModel';
|
|
40
|
+
export { CredentialFormModel } from './models/CredentialFormModel';
|
|
41
|
+
export type { CredentialServiceType } from './models/CredentialFormModel';
|
|
42
|
+
|
|
43
|
+
// Utils
|
|
44
|
+
export { showActionToast, showErrorToast, showInfoToast } from './utils/action-toast';
|
|
45
|
+
export { getScopeLabels, getScopeLabel } from './utils/scope-labels';
|
|
46
|
+
|
|
47
|
+
// Components
|
|
48
|
+
export { ServiceDashboard } from './components/ServiceDashboard';
|
|
49
|
+
export { ServiceRow } from './components/ServiceRow';
|
|
50
|
+
export { ServiceCard } from './components/ServiceCard';
|
|
51
|
+
export { CapabilityCell } from './components/CapabilityCell';
|
|
52
|
+
export { CapabilityPill } from './components/CapabilityPill';
|
|
53
|
+
export { ConnectButton } from './components/ConnectButton';
|
|
54
|
+
export { ConnectedMenu } from './components/ConnectedMenu';
|
|
55
|
+
export { CredentialForm } from './components/CredentialForm';
|
|
56
|
+
export { ServiceIcon } from './components/ServiceIcon';
|
|
57
|
+
export { CapabilityPanel } from './components/CapabilityPanel';
|
|
58
|
+
export { ConnectionStatusIndicator } from './components/ConnectionStatus';
|
|
59
|
+
export { ObjectStorageBrowser } from './components/ObjectStorageBrowser';
|
|
60
|
+
export { GitRepoBrowser, GitHostBrowser } from './components/GitBrowser';
|
|
61
|
+
export { GitHubRepoPicker } from './components/GitHubRepoPicker';
|
|
62
|
+
export { RepoPicker } from './components/RepoPicker';
|
|
63
|
+
export { FullScreenBrowser } from './components/FullScreenBrowser';
|
|
64
|
+
export { ActionHistoryPanel } from './components/ActionHistoryPanel';
|
|
65
|
+
|
|
66
|
+
// Demos
|
|
67
|
+
export { ServiceDashboardDemo } from './demos/ServiceDashboardDemo';
|
|
68
|
+
export type { ServiceDashboardDemoProps } from './demos/ServiceDashboardDemo';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { makeAutoObservable } from 'mobx';
|
|
2
|
+
|
|
3
|
+
export type ActionType = 'delete' | 'move' | 'rename' | 'upload' | 'create' | 'copy';
|
|
4
|
+
|
|
5
|
+
export interface ActionRecord {
|
|
6
|
+
id: string;
|
|
7
|
+
type: ActionType;
|
|
8
|
+
description: string;
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
/** Undo function — called when user clicks Undo */
|
|
11
|
+
undo?: () => Promise<void>;
|
|
12
|
+
/** Whether undo was executed */
|
|
13
|
+
undone: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let nextId = 1;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tracks user actions for toast notifications with undo support.
|
|
20
|
+
* Uses sonner's toast() externally — this model just manages the action log.
|
|
21
|
+
*/
|
|
22
|
+
export class ActionNotificationModel {
|
|
23
|
+
actions: ActionRecord[] = [];
|
|
24
|
+
/** Maximum actions to keep in history */
|
|
25
|
+
maxHistory = 100;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
makeAutoObservable(this, {});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Record an action. Returns the action ID for reference.
|
|
33
|
+
* The caller is responsible for showing the toast via sonner's toast().
|
|
34
|
+
*/
|
|
35
|
+
record(type: ActionType, description: string, undo?: () => Promise<void>): string {
|
|
36
|
+
const id = `action-${nextId++}`;
|
|
37
|
+
const action: ActionRecord = {
|
|
38
|
+
id,
|
|
39
|
+
type,
|
|
40
|
+
description,
|
|
41
|
+
timestamp: new Date(),
|
|
42
|
+
undo,
|
|
43
|
+
undone: false,
|
|
44
|
+
};
|
|
45
|
+
this.actions.unshift(action);
|
|
46
|
+
|
|
47
|
+
// Trim history
|
|
48
|
+
if (this.actions.length > this.maxHistory) {
|
|
49
|
+
this.actions = this.actions.slice(0, this.maxHistory);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mark an action as undone */
|
|
56
|
+
markUndone(id: string) {
|
|
57
|
+
const action = this.actions.find((a) => a.id === id);
|
|
58
|
+
if (action) action.undone = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get recentActions(): ActionRecord[] {
|
|
62
|
+
return this.actions.slice(0, 20);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get undoableActions(): ActionRecord[] {
|
|
66
|
+
return this.actions.filter((a) => a.undo && !a.undone);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear() {
|
|
70
|
+
this.actions = [];
|
|
71
|
+
}
|
|
72
|
+
}
|