@vespercms/admin 0.0.0-stage → 0.1.0-next.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/README.md +119 -2
- package/dist/BodyEditor-CAGNfofv.js +3047 -0
- package/dist/a11y/FieldNotes.d.ts +23 -0
- package/dist/a11y/NotesDrawer.d.ts +23 -0
- package/dist/a11y/pdf.d.ts +3 -0
- package/dist/a11y/run.d.ts +3 -0
- package/dist/a11y/useNotes.d.ts +28 -0
- package/dist/app/App.d.ts +2 -0
- package/dist/app/Screen.d.ts +88 -0
- package/dist/app/UserMenu.d.ts +7 -0
- package/dist/app/api.d.ts +75 -0
- package/dist/app/appearance.d.ts +10 -0
- package/dist/app/browser-api.d.ts +17 -0
- package/dist/app/collection-icon.d.ts +12 -0
- package/dist/app/context.d.ts +23 -0
- package/dist/app/entries-changed.d.ts +11 -0
- package/dist/app/file-url.d.ts +13 -0
- package/dist/app/format.d.ts +4 -0
- package/dist/app/idb.d.ts +7 -0
- package/dist/app/load.d.ts +16 -0
- package/dist/app/media.d.ts +5 -0
- package/dist/app/navigation.d.ts +4 -0
- package/dist/app/pane-context.d.ts +19 -0
- package/dist/app/panes.d.ts +14 -0
- package/dist/app/router.d.ts +62 -0
- package/dist/body/BodyEditor.d.ts +42 -0
- package/dist/body/TableView.d.ts +2 -0
- package/dist/body/catalog.d.ts +47 -0
- package/dist/body/checks.d.ts +15 -0
- package/dist/body/chips.d.ts +31 -0
- package/dist/body/commands.d.ts +31 -0
- package/dist/body/document.d.ts +42 -0
- package/dist/body/insert.d.ts +69 -0
- package/dist/body/nodes.d.ts +38 -0
- package/dist/body/reveal.d.ts +13 -0
- package/dist/body/scroll.d.ts +16 -0
- package/dist/body/settings.d.ts +19 -0
- package/dist/body/shortcuts.d.ts +13 -0
- package/dist/body/table.d.ts +31 -0
- package/dist/body/ui.d.ts +30 -0
- package/dist/body/views.d.ts +9 -0
- package/dist/checks-C-0xDu-I.js +57 -0
- package/dist/cloud/CloudGate.d.ts +14 -0
- package/dist/cloud/access.d.ts +14 -0
- package/dist/cloud/changes.d.ts +26 -0
- package/dist/cloud/connection.d.ts +100 -0
- package/dist/cloud/editor.d.ts +44 -0
- package/dist/cloud/session.d.ts +62 -0
- package/dist/cloud/storage.d.ts +45 -0
- package/dist/editor/Field.d.ts +24 -0
- package/dist/editor/PagePicker.d.ts +20 -0
- package/dist/editor/arrange.d.ts +13 -0
- package/dist/editor/context.d.ts +23 -0
- package/dist/editor/copy.d.ts +9 -0
- package/dist/editor/reveal.d.ts +7 -0
- package/dist/editor/target.d.ts +21 -0
- package/dist/editor/values.d.ts +18 -0
- package/dist/geo-map-CT3OGD7O.js +110 -0
- package/dist/github/GitHubGate.d.ts +15 -0
- package/dist/github/api.d.ts +20 -0
- package/dist/github/cache.d.ts +2 -0
- package/dist/github/session.d.ts +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/markdown-KUkv11EB.js +211 -0
- package/dist/media/MediaPicker.d.ts +28 -0
- package/dist/media/file-icon.d.ts +3 -0
- package/dist/media/formats.d.ts +23 -0
- package/dist/media/hygiene.d.ts +37 -0
- package/dist/media/upload.d.ts +31 -0
- package/dist/mount.d.ts +3 -0
- package/dist/pdf-m5s_uXJ3.js +24 -0
- package/dist/run-y4XRkg17.js +7 -0
- package/dist/schedule/ScheduleDialog.d.ts +28 -0
- package/dist/schedule/ScheduleSummary.d.ts +12 -0
- package/dist/schedule/ScheduledView.d.ts +8 -0
- package/dist/schedule/cloud.d.ts +21 -0
- package/dist/schedule/now.d.ts +5 -0
- package/dist/schedule/times.d.ts +37 -0
- package/dist/screens/EntryEditor.d.ts +13 -0
- package/dist/screens/EntryList.d.ts +18 -0
- package/dist/screens/Home.d.ts +6 -0
- package/dist/screens/ListPane.d.ts +6 -0
- package/dist/screens/MediaBrowser.d.ts +7 -0
- package/dist/screens/NotFound.d.ts +3 -0
- package/dist/screens/PageTree.d.ts +10 -0
- package/dist/screens/Redirects.d.ts +1 -0
- package/dist/screens/Unavailable.d.ts +8 -0
- package/dist/shortcuts-B_JMAvF9.js +4630 -0
- package/dist/studio.css +3 -0
- package/dist/studio.js +6312 -0
- package/dist/types.d.ts +76 -0
- package/dist/widgets/basic.d.ts +25 -0
- package/dist/widgets/crops.d.ts +43 -0
- package/dist/widgets/focal-point.d.ts +15 -0
- package/dist/widgets/geo-map.d.ts +10 -0
- package/dist/widgets/geo.d.ts +16 -0
- package/dist/widgets/group.d.ts +41 -0
- package/dist/widgets/icon.d.ts +9 -0
- package/dist/widgets/image.d.ts +16 -0
- package/dist/widgets/markdown-lazy.d.ts +2 -0
- package/dist/widgets/markdown.d.ts +2 -0
- package/dist/widgets/reference.d.ts +12 -0
- package/dist/widgets/registry.d.ts +32 -0
- package/dist/widgets/special.d.ts +20 -0
- package/dist/widgets/union.d.ts +36 -0
- package/package.json +63 -4
- package/src/a11y/FieldNotes.tsx +56 -0
- package/src/a11y/NotesDrawer.tsx +81 -0
- package/src/a11y/pdf.ts +45 -0
- package/src/a11y/run.ts +12 -0
- package/src/a11y/useNotes.ts +139 -0
- package/src/app/App.tsx +156 -0
- package/src/app/Screen.tsx +416 -0
- package/src/app/UserMenu.tsx +95 -0
- package/src/app/api.ts +176 -0
- package/src/app/appearance.ts +54 -0
- package/src/app/browser-api.ts +173 -0
- package/src/app/collection-icon.ts +15 -0
- package/src/app/context.tsx +32 -0
- package/src/app/entries-changed.ts +48 -0
- package/src/app/file-url.tsx +44 -0
- package/src/app/format.ts +39 -0
- package/src/app/idb.ts +82 -0
- package/src/app/load.ts +40 -0
- package/src/app/media.ts +20 -0
- package/src/app/navigation.ts +4 -0
- package/src/app/pane-context.ts +25 -0
- package/src/app/panes.tsx +513 -0
- package/src/app/router.ts +158 -0
- package/src/body/BodyEditor.tsx +1139 -0
- package/src/body/TableView.tsx +700 -0
- package/src/body/catalog.ts +198 -0
- package/src/body/checks.ts +83 -0
- package/src/body/chips.ts +150 -0
- package/src/body/commands.ts +191 -0
- package/src/body/document.ts +388 -0
- package/src/body/insert.tsx +301 -0
- package/src/body/nodes.ts +287 -0
- package/src/body/reveal.ts +93 -0
- package/src/body/scroll.ts +53 -0
- package/src/body/settings.tsx +510 -0
- package/src/body/shortcuts.ts +31 -0
- package/src/body/table.ts +192 -0
- package/src/body/ui.ts +77 -0
- package/src/body/views.tsx +268 -0
- package/src/build.ts +6 -0
- package/src/cloud/CloudGate.tsx +245 -0
- package/src/cloud/access.ts +52 -0
- package/src/cloud/changes.tsx +474 -0
- package/src/cloud/connection.ts +281 -0
- package/src/cloud/editor.tsx +250 -0
- package/src/cloud/session.ts +289 -0
- package/src/cloud/storage.ts +212 -0
- package/src/editor/Field.tsx +182 -0
- package/src/editor/PagePicker.tsx +102 -0
- package/src/editor/arrange.ts +98 -0
- package/src/editor/context.tsx +31 -0
- package/src/editor/copy.ts +30 -0
- package/src/editor/reveal.ts +38 -0
- package/src/editor/target.ts +104 -0
- package/src/editor/values.ts +71 -0
- package/src/github/GitHubGate.tsx +261 -0
- package/src/github/api.ts +81 -0
- package/src/github/cache.ts +64 -0
- package/src/github/session.ts +146 -0
- package/src/index.ts +10 -0
- package/src/media/MediaPicker.tsx +302 -0
- package/src/media/file-icon.ts +17 -0
- package/src/media/formats.ts +262 -0
- package/src/media/hygiene.ts +220 -0
- package/src/media/upload.ts +47 -0
- package/src/mount.tsx +20 -0
- package/src/schedule/ScheduleDialog.tsx +300 -0
- package/src/schedule/ScheduleSummary.tsx +59 -0
- package/src/schedule/ScheduledView.tsx +294 -0
- package/src/schedule/cloud.ts +66 -0
- package/src/schedule/now.ts +18 -0
- package/src/schedule/times.ts +133 -0
- package/src/screens/EntryEditor.tsx +1803 -0
- package/src/screens/EntryList.tsx +274 -0
- package/src/screens/Home.tsx +131 -0
- package/src/screens/ListPane.tsx +459 -0
- package/src/screens/MediaBrowser.tsx +253 -0
- package/src/screens/NotFound.tsx +19 -0
- package/src/screens/PageTree.tsx +718 -0
- package/src/screens/Redirects.tsx +522 -0
- package/src/screens/Unavailable.tsx +27 -0
- package/src/studio.css +163 -0
- package/src/types.ts +82 -0
- package/src/widgets/basic.tsx +311 -0
- package/src/widgets/crops.tsx +399 -0
- package/src/widgets/focal-point.tsx +240 -0
- package/src/widgets/geo-map.tsx +160 -0
- package/src/widgets/geo.tsx +170 -0
- package/src/widgets/group.tsx +827 -0
- package/src/widgets/icon.tsx +47 -0
- package/src/widgets/image.tsx +407 -0
- package/src/widgets/markdown-lazy.tsx +22 -0
- package/src/widgets/markdown.tsx +267 -0
- package/src/widgets/reference.tsx +196 -0
- package/src/widgets/registry.ts +104 -0
- package/src/widgets/special.tsx +286 -0
- package/src/widgets/union.tsx +233 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Button } from '@vespercms/ui';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { App } from '../app/App.tsx';
|
|
4
|
+
import { startSignIn as startGitHubSignIn, type GitHubStudioSettings } from '../github/session.ts';
|
|
5
|
+
import type { Account, StudioOptions } from '../types.ts';
|
|
6
|
+
import { connect } from './connection.ts';
|
|
7
|
+
import {
|
|
8
|
+
endCloudSession,
|
|
9
|
+
finishCloudSignIn,
|
|
10
|
+
resumeCloudSession,
|
|
11
|
+
startCloudSignIn,
|
|
12
|
+
type CloudSettings,
|
|
13
|
+
type CloudSignedIn,
|
|
14
|
+
type StoredSession,
|
|
15
|
+
} from './session.ts';
|
|
16
|
+
import type { KeyValueStore } from '../app/idb.ts';
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The cloud's way in (spec §6.9): finish a sign-in the app sent back here,
|
|
20
|
+
* or pick up one this browser already has, then the studio connected to
|
|
21
|
+
* the cloud. When the site also has GitHub mode, signing in with GitHub is
|
|
22
|
+
* the second way in. One primary action per screen (spec §10.4).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type GateOptions = Omit<StudioOptions, 'api' | 'github' | 'account' | 'cloud' | 'connection'>;
|
|
26
|
+
|
|
27
|
+
type State =
|
|
28
|
+
| { kind: 'checking' }
|
|
29
|
+
| { kind: 'signed-out'; message?: string }
|
|
30
|
+
| { kind: 'unreachable' }
|
|
31
|
+
| ({ kind: 'ready' } & CloudSignedIn);
|
|
32
|
+
|
|
33
|
+
const noProps = () => Promise.resolve({});
|
|
34
|
+
|
|
35
|
+
/** How often a visible studio asks for drafts and reviews: three small requests, the tree cached by commit in the cloud. */
|
|
36
|
+
const CHECK_EVERY = 60_000;
|
|
37
|
+
|
|
38
|
+
export function CloudGate({
|
|
39
|
+
settings,
|
|
40
|
+
github,
|
|
41
|
+
options,
|
|
42
|
+
fetcher,
|
|
43
|
+
store,
|
|
44
|
+
}: {
|
|
45
|
+
settings: CloudSettings;
|
|
46
|
+
github?: GitHubStudioSettings | null;
|
|
47
|
+
options: GateOptions;
|
|
48
|
+
fetcher?: typeof fetch;
|
|
49
|
+
/** Where the key and tokens are kept: IndexedDB unless a test passes its own. */
|
|
50
|
+
store?: KeyValueStore<StoredSession>;
|
|
51
|
+
}) {
|
|
52
|
+
const [state, setState] = useState<State>({ kind: 'checking' });
|
|
53
|
+
const [expired, setExpired] = useState(false);
|
|
54
|
+
const [attempt, setAttempt] = useState(0);
|
|
55
|
+
const reach = useMemo(
|
|
56
|
+
() => ({ ...(fetcher ? { fetch: fetcher } : {}), ...(store ? { store } : {}) }),
|
|
57
|
+
[fetcher, store],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let live = true;
|
|
62
|
+
const set = (next: State) => {
|
|
63
|
+
if (live) setState(next);
|
|
64
|
+
};
|
|
65
|
+
void (async () => {
|
|
66
|
+
const finished = await finishCloudSignIn(settings, reach);
|
|
67
|
+
if (finished) {
|
|
68
|
+
set(
|
|
69
|
+
finished.ok
|
|
70
|
+
? { kind: 'ready', ...finished }
|
|
71
|
+
: { kind: 'signed-out', message: finished.message },
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const resumed = await resumeCloudSession(settings, reach);
|
|
76
|
+
if (resumed.kind === 'signed-in') set({ ...resumed, kind: 'ready' });
|
|
77
|
+
else set({ kind: resumed.kind });
|
|
78
|
+
})();
|
|
79
|
+
return () => {
|
|
80
|
+
live = false;
|
|
81
|
+
};
|
|
82
|
+
// `attempt` reruns the check after "Try again".
|
|
83
|
+
}, [settings, reach, attempt]);
|
|
84
|
+
|
|
85
|
+
const signIn = useCallback(() => {
|
|
86
|
+
void startCloudSignIn(settings, window.location.hash, store);
|
|
87
|
+
}, [settings, store]);
|
|
88
|
+
const client = state.kind === 'ready' ? state.client : null;
|
|
89
|
+
const leave = useCallback(() => {
|
|
90
|
+
void endCloudSession(settings, client, store).then(() => {
|
|
91
|
+
setExpired(false);
|
|
92
|
+
setState({ kind: 'signed-out' });
|
|
93
|
+
});
|
|
94
|
+
}, [settings, client, store]);
|
|
95
|
+
|
|
96
|
+
const ready = state.kind === 'ready' ? state : null;
|
|
97
|
+
// One connection per sign-in, so its caches and drafts last the whole visit.
|
|
98
|
+
const connected = useMemo(
|
|
99
|
+
() =>
|
|
100
|
+
ready
|
|
101
|
+
? connect({
|
|
102
|
+
settings,
|
|
103
|
+
client: ready.client,
|
|
104
|
+
user: ready.user,
|
|
105
|
+
member: ready.member,
|
|
106
|
+
siteName: ready.siteName,
|
|
107
|
+
manifest: options.manifest,
|
|
108
|
+
loadMdxProps: options.loadMdxProps ?? noProps,
|
|
109
|
+
...(options.loadUrlFunctions ? { loadUrlFunctions: options.loadUrlFunctions } : {}),
|
|
110
|
+
...(options.routes ? { routes: options.routes } : {}),
|
|
111
|
+
onExpired: () => {
|
|
112
|
+
setExpired(true);
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
: null,
|
|
116
|
+
[ready, settings, options],
|
|
117
|
+
);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!connected) return;
|
|
120
|
+
const drafts = connected.connection.drafts;
|
|
121
|
+
// One check at a time: coming back into view and the timer can coincide.
|
|
122
|
+
let checking = false;
|
|
123
|
+
const check = () => {
|
|
124
|
+
if (checking) return;
|
|
125
|
+
checking = true;
|
|
126
|
+
void drafts
|
|
127
|
+
.refresh()
|
|
128
|
+
.catch(() => undefined)
|
|
129
|
+
.finally(() => {
|
|
130
|
+
checking = false;
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
check();
|
|
134
|
+
// Other people's drafts and reviews arrive while this tab is open: an
|
|
135
|
+
// approver's studio, open before a review was requested, showed nothing
|
|
136
|
+
// of it until reloaded. A hidden tab doesn't ask.
|
|
137
|
+
const whenSeen = () => {
|
|
138
|
+
if (document.visibilityState === 'visible') check();
|
|
139
|
+
};
|
|
140
|
+
document.addEventListener('visibilitychange', whenSeen);
|
|
141
|
+
const timer = window.setInterval(whenSeen, CHECK_EVERY);
|
|
142
|
+
return () => {
|
|
143
|
+
document.removeEventListener('visibilitychange', whenSeen);
|
|
144
|
+
window.clearInterval(timer);
|
|
145
|
+
};
|
|
146
|
+
}, [connected]);
|
|
147
|
+
|
|
148
|
+
const account = useMemo<Account | null>(
|
|
149
|
+
() =>
|
|
150
|
+
connected
|
|
151
|
+
? {
|
|
152
|
+
label: connected.connection.user.name,
|
|
153
|
+
name: connected.connection.user.name,
|
|
154
|
+
...(connected.connection.user.avatar
|
|
155
|
+
? { picture: connected.connection.user.avatar }
|
|
156
|
+
: {}),
|
|
157
|
+
description: `Signed in to Vesper as ${connected.connection.user.name}`,
|
|
158
|
+
signOutLabel: 'Sign out of Vesper',
|
|
159
|
+
dashboardUrl: connected.connection.dashboardUrl,
|
|
160
|
+
signOut: leave,
|
|
161
|
+
signInAgain: signIn,
|
|
162
|
+
expired,
|
|
163
|
+
}
|
|
164
|
+
: null,
|
|
165
|
+
[connected, leave, signIn, expired],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
// Signed in, each screen names the page itself.
|
|
170
|
+
if (state.kind !== 'ready')
|
|
171
|
+
document.title = `${titleOf(state)} · ${options.siteName ?? 'Vesper'}`;
|
|
172
|
+
}, [state, options.siteName]);
|
|
173
|
+
|
|
174
|
+
if (connected && account) {
|
|
175
|
+
return (
|
|
176
|
+
<App {...options} api={connected.api} account={account} connection={connected.connection} />
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="afterglow min-h-screen">
|
|
182
|
+
<main id="main" className="mx-auto flex max-w-2xl flex-col gap-4 px-4 py-16">
|
|
183
|
+
<h1 className="text-heading-1 text-content">{titleOf(state)}</h1>
|
|
184
|
+
{state.kind === 'checking' && (
|
|
185
|
+
<p role="status" className="text-content-muted">
|
|
186
|
+
Opening the studio…
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
{state.kind === 'signed-out' && (
|
|
190
|
+
<>
|
|
191
|
+
{state.message && (
|
|
192
|
+
<p role="alert" className="rounded-md border border-danger bg-surface-raised p-4">
|
|
193
|
+
{state.message}
|
|
194
|
+
</p>
|
|
195
|
+
)}
|
|
196
|
+
<p>
|
|
197
|
+
Sign in with your Vesper account. Your changes are kept as drafts until they’re
|
|
198
|
+
published.
|
|
199
|
+
</p>
|
|
200
|
+
<div className="flex flex-wrap gap-3">
|
|
201
|
+
<Button variant="primary" onPress={signIn}>
|
|
202
|
+
Sign in with Vesper
|
|
203
|
+
</Button>
|
|
204
|
+
{github && (
|
|
205
|
+
<Button onPress={() => void startGitHubSignIn(github, window.location.hash)}>
|
|
206
|
+
Sign in with GitHub
|
|
207
|
+
</Button>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
{state.kind === 'unreachable' && (
|
|
213
|
+
<>
|
|
214
|
+
<p role="alert">
|
|
215
|
+
Vesper can’t be reached. Check the connection and try again. Anything you were editing
|
|
216
|
+
is kept on this device.
|
|
217
|
+
</p>
|
|
218
|
+
<div>
|
|
219
|
+
<Button
|
|
220
|
+
variant="primary"
|
|
221
|
+
onPress={() => {
|
|
222
|
+
setState({ kind: 'checking' });
|
|
223
|
+
setAttempt((n) => n + 1);
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
Try again
|
|
227
|
+
</Button>
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
</main>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function titleOf(state: State): string {
|
|
237
|
+
switch (state.kind) {
|
|
238
|
+
case 'checking':
|
|
239
|
+
return 'Opening the studio';
|
|
240
|
+
case 'unreachable':
|
|
241
|
+
return 'Vesper couldn’t be reached';
|
|
242
|
+
default:
|
|
243
|
+
return 'Sign in to edit this site';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { editsInCollection, grantMatches, type CollectionManifest } from '@vespercms/core';
|
|
2
|
+
import type { CloudConnection } from './connection.ts';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* What the studio shows a member of a cloud site (spec §7.5): it hides what
|
|
6
|
+
* they can't do instead of showing it disabled. Viewers see everything,
|
|
7
|
+
* read-only. Without the cloud, everything is shown.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** True when a collection belongs on this member's menus and home screen. */
|
|
11
|
+
export function shownTo(cloud: CloudConnection | undefined, collection: string): boolean {
|
|
12
|
+
if (!cloud || cloud.member.role === 'viewer') return true;
|
|
13
|
+
return editsInCollection(cloud.member, collection);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when an entry file belongs in this member's lists. */
|
|
17
|
+
export function entryShownTo(cloud: CloudConnection | undefined, path: string): boolean {
|
|
18
|
+
if (!cloud || cloud.member.role === 'viewer' || cloud.wholeSite) return true;
|
|
19
|
+
return cloud.access(path).edit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True when the member may start a new entry at the top of a collection (or in `parent`, in a tree). */
|
|
23
|
+
export function mayCreateIn(
|
|
24
|
+
cloud: CloudConnection | undefined,
|
|
25
|
+
collection: CollectionManifest,
|
|
26
|
+
parent?: string,
|
|
27
|
+
): boolean {
|
|
28
|
+
if (!cloud) return true;
|
|
29
|
+
const source = collection.source;
|
|
30
|
+
if (source.loader === 'file') return cloud.access(source.path).edit;
|
|
31
|
+
if (source.loader !== 'glob') return false;
|
|
32
|
+
const folder = [source.base === '.' ? '' : source.base, parent ?? ''].filter(Boolean).join('/');
|
|
33
|
+
return cloud.access(`${folder ? `${folder}/` : ''}new-entry.${source.formats[0] ?? 'md'}`).edit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when the member may change a page at this place in a tree (spec
|
|
38
|
+
* §6.15.2, §7.5). A tree path is a grant key: `admissions` is the
|
|
39
|
+
* Admissions section page, `admissions/apply` a page under it.
|
|
40
|
+
*/
|
|
41
|
+
export function treeCovered(
|
|
42
|
+
cloud: CloudConnection | undefined,
|
|
43
|
+
collection: string,
|
|
44
|
+
path: string,
|
|
45
|
+
): boolean {
|
|
46
|
+
if (!cloud) return true;
|
|
47
|
+
if (cloud.member.role === 'viewer') return false;
|
|
48
|
+
if (cloud.wholeSite) return true;
|
|
49
|
+
return (cloud.member.grants ?? []).some(
|
|
50
|
+
(grant) => grant.collection === collection && grantMatches(grant.match, path),
|
|
51
|
+
);
|
|
52
|
+
}
|