@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,89 @@
|
|
|
1
|
+
import { makeAutoObservable, flow } from 'mobx';
|
|
2
|
+
import type { IGitHost, PullRequest, Issue } from '@anymux/file-system';
|
|
3
|
+
|
|
4
|
+
export type HostTab = 'prs' | 'issues';
|
|
5
|
+
|
|
6
|
+
const PAGE_SIZE = 25;
|
|
7
|
+
|
|
8
|
+
export class GitHostBrowserModel {
|
|
9
|
+
gitHost: IGitHost;
|
|
10
|
+
|
|
11
|
+
activeTab: HostTab = 'prs';
|
|
12
|
+
prs: PullRequest[] = [];
|
|
13
|
+
issues: Issue[] = [];
|
|
14
|
+
hasMorePrs = false;
|
|
15
|
+
hasMoreIssues = false;
|
|
16
|
+
page = 0;
|
|
17
|
+
loading = false;
|
|
18
|
+
error: string | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(gitHost: IGitHost) {
|
|
21
|
+
this.gitHost = gitHost;
|
|
22
|
+
makeAutoObservable(this, { gitHost: false });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get currentItems() {
|
|
26
|
+
return this.activeTab === 'prs' ? this.prs : this.issues;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get hasMore() {
|
|
30
|
+
return this.activeTab === 'prs' ? this.hasMorePrs : this.hasMoreIssues;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get showPagination() {
|
|
34
|
+
return this.page > 0 || this.hasMore;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setActiveTab(tab: HostTab) {
|
|
38
|
+
this.activeTab = tab;
|
|
39
|
+
this.page = 0;
|
|
40
|
+
this.loadData();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
nextPage() {
|
|
44
|
+
if (!this.hasMore) return;
|
|
45
|
+
this.page += 1;
|
|
46
|
+
this.loadData();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
prevPage() {
|
|
50
|
+
if (this.page <= 0) return;
|
|
51
|
+
this.page -= 1;
|
|
52
|
+
this.loadData();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
retry() {
|
|
56
|
+
this.error = null;
|
|
57
|
+
this.page = 0;
|
|
58
|
+
this.loadData();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
loadData = flow(function* loadData(this: GitHostBrowserModel) {
|
|
62
|
+
this.loading = true;
|
|
63
|
+
this.error = null;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (this.activeTab === 'prs') {
|
|
67
|
+
const data: PullRequest[] = yield this.gitHost.listPullRequests({
|
|
68
|
+
state: 'all',
|
|
69
|
+
maxResults: PAGE_SIZE * (this.page + 1),
|
|
70
|
+
});
|
|
71
|
+
const start = this.page * PAGE_SIZE;
|
|
72
|
+
this.prs = data.slice(start, start + PAGE_SIZE);
|
|
73
|
+
this.hasMorePrs = data.length > start + PAGE_SIZE;
|
|
74
|
+
} else {
|
|
75
|
+
const data: Issue[] = yield this.gitHost.listIssues({
|
|
76
|
+
state: 'all',
|
|
77
|
+
maxResults: PAGE_SIZE * (this.page + 1),
|
|
78
|
+
});
|
|
79
|
+
const start = this.page * PAGE_SIZE;
|
|
80
|
+
this.issues = data.slice(start, start + PAGE_SIZE);
|
|
81
|
+
this.hasMoreIssues = data.length > start + PAGE_SIZE;
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
this.error = err instanceof Error ? err.message : 'Failed to load data';
|
|
85
|
+
} finally {
|
|
86
|
+
this.loading = false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { makeAutoObservable, flow } from 'mobx';
|
|
2
|
+
import type {
|
|
3
|
+
IGitRepo,
|
|
4
|
+
IGitHost,
|
|
5
|
+
GitBranch,
|
|
6
|
+
GitTag,
|
|
7
|
+
GitCommit,
|
|
8
|
+
GitDiffEntry,
|
|
9
|
+
IFileSystem,
|
|
10
|
+
PullRequest,
|
|
11
|
+
Issue,
|
|
12
|
+
} from '@anymux/file-system';
|
|
13
|
+
|
|
14
|
+
export type SidebarTab = 'files' | 'branches' | 'commits' | 'prs' | 'issues';
|
|
15
|
+
|
|
16
|
+
const HOST_PAGE_SIZE = 25;
|
|
17
|
+
const COMMITS_PAGE_SIZE = 25;
|
|
18
|
+
|
|
19
|
+
export class GitRepoBrowserModel {
|
|
20
|
+
// --- Core repo state ---
|
|
21
|
+
branches: GitBranch[] = [];
|
|
22
|
+
tags: GitTag[] = [];
|
|
23
|
+
currentRef = '';
|
|
24
|
+
fileSystem: IFileSystem | null = null;
|
|
25
|
+
loading = true;
|
|
26
|
+
error: string | null = null;
|
|
27
|
+
|
|
28
|
+
// --- Tab state ---
|
|
29
|
+
activeTab: SidebarTab = 'files';
|
|
30
|
+
|
|
31
|
+
// --- Commits state ---
|
|
32
|
+
commits: GitCommit[] = [];
|
|
33
|
+
commitsLoading = false;
|
|
34
|
+
commitsPage = 0;
|
|
35
|
+
hasMoreCommits = false;
|
|
36
|
+
|
|
37
|
+
// --- Diff state ---
|
|
38
|
+
selectedCommitSha: string | undefined = undefined;
|
|
39
|
+
diffEntries: GitDiffEntry[] = [];
|
|
40
|
+
diffLoading = false;
|
|
41
|
+
|
|
42
|
+
// --- Host data (PRs / Issues) ---
|
|
43
|
+
prs: PullRequest[] = [];
|
|
44
|
+
issues: Issue[] = [];
|
|
45
|
+
hostLoading = false;
|
|
46
|
+
hostError: string | null = null;
|
|
47
|
+
prPage = 0;
|
|
48
|
+
issuePage = 0;
|
|
49
|
+
hasMorePrs = false;
|
|
50
|
+
hasMoreIssues = false;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
private gitRepo: IGitRepo,
|
|
54
|
+
private _gitHost: IGitHost | undefined,
|
|
55
|
+
private createFileSystem: (branch: string) => Promise<IFileSystem>,
|
|
56
|
+
private onError?: (err: { message: string }) => void,
|
|
57
|
+
) {
|
|
58
|
+
makeAutoObservable(this, {
|
|
59
|
+
// Mark constructor deps as non-observable (they are stable references)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Update the gitHost adapter (e.g. when loaded asynchronously after construction) */
|
|
64
|
+
setGitHost(host: IGitHost): void {
|
|
65
|
+
this._gitHost = host;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Computed ---
|
|
69
|
+
|
|
70
|
+
get headSha(): string | undefined {
|
|
71
|
+
return this.branches.find((b) => b.name === this.currentRef)?.sha;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get hasGitHost(): boolean {
|
|
75
|
+
return !!this._gitHost;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Actions ---
|
|
79
|
+
|
|
80
|
+
setActiveTab(tab: SidebarTab): void {
|
|
81
|
+
this.activeTab = tab;
|
|
82
|
+
|
|
83
|
+
// Trigger data loading for the newly active tab
|
|
84
|
+
if (tab === 'commits' && this.currentRef) {
|
|
85
|
+
this.loadCommits();
|
|
86
|
+
} else if (tab === 'prs' && this._gitHost) {
|
|
87
|
+
this.loadPRs();
|
|
88
|
+
} else if (tab === 'issues' && this._gitHost) {
|
|
89
|
+
this.loadIssues();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Async flows ---
|
|
94
|
+
|
|
95
|
+
initialize = flow(function* (this: GitRepoBrowserModel) {
|
|
96
|
+
this.loading = true;
|
|
97
|
+
this.error = null;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const [branchList, tagList]: [GitBranch[], GitTag[]] = yield Promise.all([
|
|
101
|
+
this.gitRepo.listBranches(),
|
|
102
|
+
this.gitRepo.listTags().catch(() => [] as GitTag[]),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
this.branches = branchList;
|
|
106
|
+
this.tags = tagList;
|
|
107
|
+
|
|
108
|
+
// Pick default branch
|
|
109
|
+
const defaultBranch = branchList.find((b) => b.isDefault);
|
|
110
|
+
const initialRef = defaultBranch?.name ?? branchList[0]?.name ?? 'main';
|
|
111
|
+
this.currentRef = initialRef;
|
|
112
|
+
|
|
113
|
+
// Create file system for the default branch
|
|
114
|
+
const fs: IFileSystem = yield this.createFileSystem(initialRef);
|
|
115
|
+
this.fileSystem = fs;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const msg = err instanceof Error ? err.message : 'Failed to load repository';
|
|
118
|
+
this.error = msg;
|
|
119
|
+
this.onError?.({ message: msg });
|
|
120
|
+
} finally {
|
|
121
|
+
this.loading = false;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
switchRef = flow(function* (this: GitRepoBrowserModel, ref: string) {
|
|
126
|
+
if (ref === this.currentRef) return;
|
|
127
|
+
|
|
128
|
+
this.currentRef = ref;
|
|
129
|
+
this.loading = true;
|
|
130
|
+
this.error = null;
|
|
131
|
+
this.selectedCommitSha = undefined;
|
|
132
|
+
this.diffEntries = [];
|
|
133
|
+
this.commitsPage = 0;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const fs: IFileSystem = yield this.createFileSystem(ref);
|
|
137
|
+
this.fileSystem = fs;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : 'Failed to switch ref';
|
|
140
|
+
this.error = msg;
|
|
141
|
+
this.onError?.({ message: msg });
|
|
142
|
+
} finally {
|
|
143
|
+
this.loading = false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Reload commits if the commits tab is active
|
|
147
|
+
if (this.activeTab === 'commits') {
|
|
148
|
+
this.loadCommits();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
loadCommits = flow(function* (this: GitRepoBrowserModel) {
|
|
153
|
+
if (!this.currentRef) return;
|
|
154
|
+
|
|
155
|
+
this.commitsLoading = true;
|
|
156
|
+
try {
|
|
157
|
+
const fetchCount = COMMITS_PAGE_SIZE * (this.commitsPage + 1);
|
|
158
|
+
const commitList: GitCommit[] = yield this.gitRepo.listCommits({
|
|
159
|
+
ref: this.currentRef,
|
|
160
|
+
maxCount: fetchCount + 1, // Fetch one extra to detect if more exist
|
|
161
|
+
});
|
|
162
|
+
const start = this.commitsPage * COMMITS_PAGE_SIZE;
|
|
163
|
+
this.commits = commitList.slice(start, start + COMMITS_PAGE_SIZE);
|
|
164
|
+
this.hasMoreCommits = commitList.length > start + COMMITS_PAGE_SIZE;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error('[GitRepoBrowserModel] Failed to load commits:', err);
|
|
167
|
+
this.commits = [];
|
|
168
|
+
this.hasMoreCommits = false;
|
|
169
|
+
} finally {
|
|
170
|
+
this.commitsLoading = false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
selectCommit = flow(function* (this: GitRepoBrowserModel, commit: GitCommit) {
|
|
175
|
+
this.selectedCommitSha = commit.sha;
|
|
176
|
+
|
|
177
|
+
if (commit.parents.length === 0) {
|
|
178
|
+
this.diffEntries = [];
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.diffLoading = true;
|
|
183
|
+
try {
|
|
184
|
+
const entries: GitDiffEntry[] = yield this.gitRepo.diff(
|
|
185
|
+
commit.parents[0]!,
|
|
186
|
+
commit.sha,
|
|
187
|
+
);
|
|
188
|
+
this.diffEntries = entries;
|
|
189
|
+
} catch {
|
|
190
|
+
this.diffEntries = [];
|
|
191
|
+
} finally {
|
|
192
|
+
this.diffLoading = false;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
loadPRs = flow(function* (this: GitRepoBrowserModel) {
|
|
197
|
+
if (!this._gitHost) return;
|
|
198
|
+
|
|
199
|
+
this.hostLoading = true;
|
|
200
|
+
this.hostError = null;
|
|
201
|
+
try {
|
|
202
|
+
const data: PullRequest[] = yield this._gitHost.listPullRequests({
|
|
203
|
+
state: 'all',
|
|
204
|
+
maxResults: HOST_PAGE_SIZE * (this.prPage + 1),
|
|
205
|
+
});
|
|
206
|
+
const start = this.prPage * HOST_PAGE_SIZE;
|
|
207
|
+
this.prs = data.slice(start, start + HOST_PAGE_SIZE);
|
|
208
|
+
this.hasMorePrs = data.length > start + HOST_PAGE_SIZE;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
this.hostError = err instanceof Error ? err.message : 'Failed to load';
|
|
211
|
+
} finally {
|
|
212
|
+
this.hostLoading = false;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
loadIssues = flow(function* (this: GitRepoBrowserModel) {
|
|
217
|
+
if (!this._gitHost) return;
|
|
218
|
+
|
|
219
|
+
this.hostLoading = true;
|
|
220
|
+
this.hostError = null;
|
|
221
|
+
try {
|
|
222
|
+
const data: Issue[] = yield this._gitHost.listIssues({
|
|
223
|
+
state: 'all',
|
|
224
|
+
maxResults: HOST_PAGE_SIZE * (this.issuePage + 1),
|
|
225
|
+
});
|
|
226
|
+
const start = this.issuePage * HOST_PAGE_SIZE;
|
|
227
|
+
this.issues = data.slice(start, start + HOST_PAGE_SIZE);
|
|
228
|
+
this.hasMoreIssues = data.length > start + HOST_PAGE_SIZE;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
this.hostError = err instanceof Error ? err.message : 'Failed to load';
|
|
231
|
+
} finally {
|
|
232
|
+
this.hostLoading = false;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// --- Pagination helpers ---
|
|
237
|
+
|
|
238
|
+
nextCommitsPage(): void {
|
|
239
|
+
this.commitsPage += 1;
|
|
240
|
+
this.loadCommits();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
prevCommitsPage(): void {
|
|
244
|
+
this.commitsPage = Math.max(0, this.commitsPage - 1);
|
|
245
|
+
this.loadCommits();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
nextPrPage(): void {
|
|
249
|
+
this.prPage += 1;
|
|
250
|
+
this.loadPRs();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
prevPrPage(): void {
|
|
254
|
+
this.prPage = Math.max(0, this.prPage - 1);
|
|
255
|
+
this.loadPRs();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
nextIssuePage(): void {
|
|
259
|
+
this.issuePage += 1;
|
|
260
|
+
this.loadIssues();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
prevIssuePage(): void {
|
|
264
|
+
this.issuePage = Math.max(0, this.issuePage - 1);
|
|
265
|
+
this.loadIssues();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
retryPRs(): void {
|
|
269
|
+
this.hostError = null;
|
|
270
|
+
this.prPage = 0;
|
|
271
|
+
this.loadPRs();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
retryIssues(): void {
|
|
275
|
+
this.hostError = null;
|
|
276
|
+
this.issuePage = 0;
|
|
277
|
+
this.loadIssues();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- Branch selection from BranchList tab ---
|
|
281
|
+
|
|
282
|
+
selectBranch(branch: GitBranch): void {
|
|
283
|
+
this.switchRef(branch.name);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { makeAutoObservable, flow } from 'mobx';
|
|
2
|
+
import type { IObjectStorage } from '@anymux/file-system';
|
|
3
|
+
|
|
4
|
+
const OBJ_PAGE_SIZE = 100;
|
|
5
|
+
|
|
6
|
+
export interface DisplayObject {
|
|
7
|
+
key: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
lastModified?: Date;
|
|
10
|
+
isPrefix: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ObjectStorageBrowserModel {
|
|
14
|
+
storage: IObjectStorage;
|
|
15
|
+
bucket: string;
|
|
16
|
+
|
|
17
|
+
prefix = '';
|
|
18
|
+
objects: DisplayObject[] = [];
|
|
19
|
+
loading = false;
|
|
20
|
+
error: string | null = null;
|
|
21
|
+
continuationToken: string | undefined = undefined;
|
|
22
|
+
tokenHistory: (string | undefined)[] = [];
|
|
23
|
+
page = 0;
|
|
24
|
+
hasMore = false;
|
|
25
|
+
|
|
26
|
+
constructor(storage: IObjectStorage, bucket: string) {
|
|
27
|
+
this.storage = storage;
|
|
28
|
+
this.bucket = bucket;
|
|
29
|
+
makeAutoObservable(this, { storage: false });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get breadcrumbs(): Array<{ label: string; prefix: string }> {
|
|
33
|
+
return this.prefix
|
|
34
|
+
.split('/')
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.reduce<Array<{ label: string; prefix: string }>>((acc, part, i) => {
|
|
37
|
+
const prev = acc[i - 1]?.prefix || '';
|
|
38
|
+
acc.push({ label: part, prefix: prev + part + '/' });
|
|
39
|
+
return acc;
|
|
40
|
+
}, []);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
displayName(key: string): string {
|
|
44
|
+
return key.slice(this.prefix.length).replace(/\/$/, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loadObjects = flow(function* loadObjects(this: ObjectStorageBrowserModel, token?: string) {
|
|
48
|
+
this.loading = true;
|
|
49
|
+
this.error = null;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = yield this.storage.listObjects(this.bucket, {
|
|
53
|
+
prefix: this.prefix || undefined,
|
|
54
|
+
delimiter: '/',
|
|
55
|
+
maxKeys: OBJ_PAGE_SIZE,
|
|
56
|
+
continuationToken: token,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const prefixItems: DisplayObject[] = result.commonPrefixes.map((p: string) => ({
|
|
60
|
+
key: p,
|
|
61
|
+
isPrefix: true,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const objectItems: DisplayObject[] = result.objects.map((obj: any) => ({
|
|
65
|
+
key: obj.key,
|
|
66
|
+
size: obj.metadata.size,
|
|
67
|
+
lastModified: obj.metadata.lastModified,
|
|
68
|
+
isPrefix: false,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
this.objects = [...prefixItems, ...objectItems];
|
|
72
|
+
this.hasMore = result.isTruncated;
|
|
73
|
+
this.continuationToken = result.nextContinuationToken;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
this.error = err instanceof Error ? err.message : 'Failed to load objects';
|
|
76
|
+
} finally {
|
|
77
|
+
this.loading = false;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
initialize = flow(function* initialize(this: ObjectStorageBrowserModel) {
|
|
82
|
+
this.page = 0;
|
|
83
|
+
this.tokenHistory = [];
|
|
84
|
+
this.continuationToken = undefined;
|
|
85
|
+
yield this.loadObjects();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
navigateToPrefix(newPrefix: string) {
|
|
89
|
+
this.prefix = newPrefix;
|
|
90
|
+
this.page = 0;
|
|
91
|
+
this.tokenHistory = [];
|
|
92
|
+
this.continuationToken = undefined;
|
|
93
|
+
this.loadObjects();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
navigateUp() {
|
|
97
|
+
const parts = this.prefix.replace(/\/$/, '').split('/');
|
|
98
|
+
parts.pop();
|
|
99
|
+
this.navigateToPrefix(parts.length > 0 ? parts.join('/') + '/' : '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
nextPage() {
|
|
103
|
+
if (!this.hasMore || !this.continuationToken) return;
|
|
104
|
+
this.tokenHistory = [...this.tokenHistory, this.continuationToken];
|
|
105
|
+
this.page += 1;
|
|
106
|
+
this.loadObjects(this.continuationToken);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
prevPage() {
|
|
110
|
+
if (this.page <= 0) return;
|
|
111
|
+
const newHistory = [...this.tokenHistory];
|
|
112
|
+
newHistory.pop();
|
|
113
|
+
const prevToken = newHistory.length > 0 ? newHistory[newHistory.length - 1] : undefined;
|
|
114
|
+
this.tokenHistory = newHistory;
|
|
115
|
+
this.page -= 1;
|
|
116
|
+
this.loadObjects(prevToken);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
download = flow(function* download(this: ObjectStorageBrowserModel, key: string) {
|
|
120
|
+
try {
|
|
121
|
+
const url: string = yield this.storage.getPresignedUrl(this.bucket, key);
|
|
122
|
+
window.open(url, '_blank');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
this.error = err instanceof Error ? err.message : 'Failed to generate download URL';
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
refresh() {
|
|
129
|
+
this.loadObjects();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { makeAutoObservable, flow, computed } from 'mobx';
|
|
2
|
+
import { match } from 'ts-pattern';
|
|
3
|
+
|
|
4
|
+
export interface RepoInfo {
|
|
5
|
+
fullName: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
language: string | null;
|
|
8
|
+
isPrivate: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class RepoPickerModel {
|
|
12
|
+
serviceId: string;
|
|
13
|
+
accessToken: string;
|
|
14
|
+
|
|
15
|
+
repos: RepoInfo[] = [];
|
|
16
|
+
loading = true;
|
|
17
|
+
error: string | null = null;
|
|
18
|
+
search = '';
|
|
19
|
+
|
|
20
|
+
constructor(serviceId: string, accessToken: string) {
|
|
21
|
+
this.serviceId = serviceId;
|
|
22
|
+
this.accessToken = accessToken;
|
|
23
|
+
makeAutoObservable(this, {});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get filtered(): RepoInfo[] {
|
|
27
|
+
const q = this.search.toLowerCase();
|
|
28
|
+
if (!q) return this.repos;
|
|
29
|
+
return this.repos.filter((r) => r.fullName.toLowerCase().includes(q));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setSearch(value: string) {
|
|
33
|
+
this.search = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
loadRepos = flow(function* loadRepos(this: RepoPickerModel) {
|
|
37
|
+
this.loading = true;
|
|
38
|
+
this.error = null;
|
|
39
|
+
try {
|
|
40
|
+
const data: RepoInfo[] = yield fetchRepos(this.serviceId, this.accessToken);
|
|
41
|
+
this.repos = data;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
this.error = err instanceof Error ? err.message : 'Failed to fetch repos';
|
|
44
|
+
} finally {
|
|
45
|
+
this.loading = false;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Fetch helpers ---
|
|
51
|
+
|
|
52
|
+
async function fetchRepos(serviceId: string, token: string): Promise<RepoInfo[]> {
|
|
53
|
+
return match(serviceId)
|
|
54
|
+
.with('github', () => fetchGitHubRepos(token))
|
|
55
|
+
.with('gitlab', () => fetchGitLabRepos(token))
|
|
56
|
+
.with('bitbucket', () => fetchBitbucketRepos(token))
|
|
57
|
+
.with('gitea', () => fetchGiteaRepos(token))
|
|
58
|
+
.otherwise(() => { throw new Error(`Repo picker not supported for: ${serviceId}`); });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function fetchGitHubRepos(token: string): Promise<RepoInfo[]> {
|
|
62
|
+
const res = await fetch('https://api.github.com/user/repos?sort=updated&per_page=50', {
|
|
63
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
|
|
66
|
+
const data: Array<{ full_name: string; description: string | null; language: string | null; private: boolean }> = await res.json();
|
|
67
|
+
return data.map((r) => ({
|
|
68
|
+
fullName: r.full_name,
|
|
69
|
+
description: r.description,
|
|
70
|
+
language: r.language,
|
|
71
|
+
isPrivate: r.private,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fetchGitLabRepos(token: string): Promise<RepoInfo[]> {
|
|
76
|
+
const res = await fetch('https://gitlab.com/api/v4/projects?membership=true&order_by=updated_at&per_page=50', {
|
|
77
|
+
headers: { 'PRIVATE-TOKEN': token },
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) throw new Error(`GitLab API error: ${res.status}`);
|
|
80
|
+
const data: Array<{ path_with_namespace: string; description: string | null; visibility: string }> = await res.json();
|
|
81
|
+
return data.map((r) => ({
|
|
82
|
+
fullName: r.path_with_namespace,
|
|
83
|
+
description: r.description,
|
|
84
|
+
language: null,
|
|
85
|
+
isPrivate: r.visibility === 'private',
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchBitbucketRepos(token: string): Promise<RepoInfo[]> {
|
|
90
|
+
const res = await fetch('https://api.bitbucket.org/2.0/repositories?role=member&pagelen=50', {
|
|
91
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) throw new Error(`Bitbucket API error: ${res.status}`);
|
|
94
|
+
const data: { values: Array<{ full_name: string; description: string; language: string; is_private: boolean }> } = await res.json();
|
|
95
|
+
return data.values.map((r) => ({
|
|
96
|
+
fullName: r.full_name,
|
|
97
|
+
description: r.description || null,
|
|
98
|
+
language: r.language || null,
|
|
99
|
+
isPrivate: r.is_private,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function fetchGiteaRepos(token: string): Promise<RepoInfo[]> {
|
|
104
|
+
const creds = JSON.parse(token);
|
|
105
|
+
const headers: HeadersInit = {
|
|
106
|
+
'Accept': 'application/json',
|
|
107
|
+
};
|
|
108
|
+
// Use token auth if available, otherwise Basic auth
|
|
109
|
+
if (creds.token) {
|
|
110
|
+
headers['Authorization'] = `token ${creds.token}`;
|
|
111
|
+
} else if (creds.username && creds.password) {
|
|
112
|
+
headers['Authorization'] = 'Basic ' + btoa(`${creds.username}:${creds.password}`);
|
|
113
|
+
}
|
|
114
|
+
// Route through CORS proxy when in browser
|
|
115
|
+
const proxyUrl = typeof window !== 'undefined' ? window.location.origin : undefined;
|
|
116
|
+
let url: string;
|
|
117
|
+
if (proxyUrl) {
|
|
118
|
+
url = `${proxyUrl}/api/gitea/api/v1/user/repos?limit=50`;
|
|
119
|
+
headers['x-gitea-url'] = creds.url;
|
|
120
|
+
} else {
|
|
121
|
+
url = `${creds.url}/api/v1/user/repos?limit=50`;
|
|
122
|
+
}
|
|
123
|
+
const res = await fetch(url, { headers });
|
|
124
|
+
if (!res.ok) throw new Error(`Gitea API error: ${res.status}`);
|
|
125
|
+
const data: Array<{ full_name: string; description: string; language: string; private: boolean }> = await res.json();
|
|
126
|
+
return data.map((r) => ({
|
|
127
|
+
fullName: r.full_name,
|
|
128
|
+
description: r.description || null,
|
|
129
|
+
language: r.language || null,
|
|
130
|
+
isPrivate: r.private,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CapabilityId, ServiceDefinition } from '../types/service';
|
|
2
|
+
import { googleService } from './services/google';
|
|
3
|
+
import { dropboxService } from './services/dropbox';
|
|
4
|
+
import { githubService } from './services/github';
|
|
5
|
+
import { s3Service } from './services/s3';
|
|
6
|
+
import { webdavService } from './services/webdav';
|
|
7
|
+
import { gitlabService } from './services/gitlab';
|
|
8
|
+
import { bitbucketService } from './services/bitbucket';
|
|
9
|
+
import { giteaService } from './services/gitea';
|
|
10
|
+
import { browserFsService } from './services/browser-fs';
|
|
11
|
+
import { indexeddbService } from './services/indexeddb';
|
|
12
|
+
import { boxService } from './services/box';
|
|
13
|
+
|
|
14
|
+
const services = new Map<string, ServiceDefinition>();
|
|
15
|
+
|
|
16
|
+
function register(service: ServiceDefinition) {
|
|
17
|
+
services.set(service.id, service);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
register(googleService);
|
|
21
|
+
register(dropboxService);
|
|
22
|
+
register(githubService);
|
|
23
|
+
register(s3Service);
|
|
24
|
+
register(webdavService);
|
|
25
|
+
register(gitlabService);
|
|
26
|
+
register(bitbucketService);
|
|
27
|
+
register(giteaService);
|
|
28
|
+
register(browserFsService);
|
|
29
|
+
register(indexeddbService);
|
|
30
|
+
register(boxService);
|
|
31
|
+
|
|
32
|
+
export const serviceRegistry = {
|
|
33
|
+
get(id: string): ServiceDefinition | undefined {
|
|
34
|
+
return services.get(id);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
getAll(): ServiceDefinition[] {
|
|
38
|
+
return Array.from(services.values());
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
getByCapability(capability: CapabilityId): ServiceDefinition[] {
|
|
42
|
+
return Array.from(services.values()).filter((s) =>
|
|
43
|
+
s.capabilities.some((c) => c.id === capability && c.supported)
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ServiceDefinition } from '../../types/service';
|
|
2
|
+
|
|
3
|
+
export const appleService: ServiceDefinition = {
|
|
4
|
+
id: 'apple',
|
|
5
|
+
name: 'Apple',
|
|
6
|
+
icon: 'Apple',
|
|
7
|
+
color: '#000000',
|
|
8
|
+
authProvider: 'apple',
|
|
9
|
+
grantsUrl: 'https://appleid.apple.com/account/manage/security',
|
|
10
|
+
capabilities: [
|
|
11
|
+
{ id: 'file-system', supported: false },
|
|
12
|
+
{ id: 'object-storage', supported: false },
|
|
13
|
+
{ id: 'git-repo', supported: false },
|
|
14
|
+
{ id: 'git-host', supported: false },
|
|
15
|
+
{ id: 'media', supported: false },
|
|
16
|
+
{ id: 'contacts', supported: false },
|
|
17
|
+
{ id: 'calendar', supported: false },
|
|
18
|
+
],
|
|
19
|
+
// Apple currently only provides identity/auth.
|
|
20
|
+
// Future: CalDAV for calendar, CardDAV for contacts, iCloud Drive for file-system
|
|
21
|
+
scopes: {},
|
|
22
|
+
};
|