canopycms 0.12.1 → 0.13.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/CHANGELOG.md +211 -49
- package/dist/api/branch-review.d.ts +20 -2
- package/dist/api/branch-review.js +112 -23
- package/dist/api/branch-status.d.ts +11 -2
- package/dist/api/branch-status.js +3 -2
- package/dist/api/branch-update-from-base.js +57 -6
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +27 -2
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.js +2 -2
- package/dist/api/content.js +1 -1
- package/dist/api/merge-conflicts.d.ts +10 -1
- package/dist/api/merge-conflicts.js +42 -7
- package/dist/api/types.d.ts +3 -2
- package/dist/base-update.d.ts +6 -0
- package/dist/branch-metadata-file.d.ts +15 -0
- package/dist/branch-metadata-file.js +5 -1
- package/dist/cli/cli.js +238 -108
- package/dist/cli/generate-ai-content.js +20 -6
- package/dist/cli/github-app-manifest.d.ts +4 -1
- package/dist/cli/github-app-manifest.js +5 -1
- package/dist/cli/init.js +7 -1
- package/dist/editor/Editor.js +39 -16
- package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
- package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
- package/dist/editor/admin/SystemHealthPanel.js +4 -3
- package/dist/editor/base-update-wait.d.ts +4 -0
- package/dist/editor/base-update-wait.js +7 -0
- package/dist/editor/components/ConflictsGroup.d.ts +13 -1
- package/dist/editor/components/ConflictsGroup.js +52 -12
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +11 -7
- package/dist/editor/components/EntryChangesDrawer.d.ts +1 -1
- package/dist/editor/components/EntryChangesDrawer.js +58 -6
- package/dist/editor/conflict-cards.d.ts +4 -1
- package/dist/editor/conflict-cards.js +14 -9
- package/dist/editor/hooks/useBaseUpdate.d.ts +2 -0
- package/dist/editor/hooks/useBaseUpdate.js +12 -2
- package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
- package/dist/editor/hooks/useBranchChangesData.js +13 -0
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +80 -6
- package/dist/editor/hooks/useDraftManager.d.ts +8 -1
- package/dist/editor/hooks/useDraftManager.js +16 -1
- package/dist/editor/hooks/useEntryManager.d.ts +8 -3
- package/dist/editor/hooks/useEntryManager.js +6 -5
- package/dist/git-manager.d.ts +1 -0
- package/dist/git-manager.js +8 -3
- package/dist/merge/entry-merge.js +4 -3
- package/dist/merge/list-alignment.d.ts +9 -6
- package/dist/merge/list-alignment.js +65 -16
- package/dist/merge/rekey.d.ts +11 -0
- package/dist/merge/rekey.js +75 -0
- package/dist/operating-mode/client-safe-strategy.js +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +10 -2
- package/dist/services.d.ts +12 -4
- package/dist/services.js +56 -0
- package/dist/types.d.ts +35 -3
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.js +35 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +27 -3
- package/dist/worker/branch-merge.js +106 -5
- package/dist/worker/branch-policy.d.ts +18 -2
- package/dist/worker/branch-policy.js +36 -0
- package/dist/worker/cms-worker.d.ts +21 -0
- package/dist/worker/cms-worker.js +29 -4
- package/dist/worker/conflicts-status.d.ts +20 -0
- package/dist/worker/conflicts-status.js +204 -0
- package/dist/worker/field-merge.d.ts +17 -2
- package/dist/worker/field-merge.js +95 -2
- package/dist/worker/git-sync.d.ts +3 -2
- package/dist/worker/git-sync.js +9 -0
- package/dist/worker/github-commits.d.ts +3 -3
- package/dist/worker/github-commits.js +192 -40
- package/dist/worker/github-errors.d.ts +15 -0
- package/dist/worker/github-errors.js +47 -0
- package/dist/worker/github-gateway-http.js +7 -0
- package/dist/worker/github-gateway-wire.d.ts +30 -10
- package/dist/worker/github-gateway-wire.js +14 -2
- package/dist/worker/github-gateway.d.ts +17 -0
- package/dist/worker/github-gateway.js +37 -1
- package/dist/worker/history-rewrite.d.ts +40 -8
- package/dist/worker/history-rewrite.js +51 -23
- package/dist/worker/publish-decision.d.ts +37 -0
- package/dist/worker/publish-decision.js +16 -0
- package/dist/worker/rebase.d.ts +25 -5
- package/dist/worker/rebase.js +225 -81
- package/dist/worker/task-runner.js +16 -39
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
|
@@ -1214,7 +1214,7 @@ var init_client_safe_strategy = __esm({
|
|
|
1214
1214
|
return false;
|
|
1215
1215
|
}
|
|
1216
1216
|
syncsBranchesWithBase() {
|
|
1217
|
-
return
|
|
1217
|
+
return true;
|
|
1218
1218
|
}
|
|
1219
1219
|
getPermissionsFileName() {
|
|
1220
1220
|
return "permissions.json";
|
|
@@ -1405,6 +1405,9 @@ var init_client_unsafe_strategy = __esm({
|
|
|
1405
1405
|
autoDetectRemotePath: path9.join(this.getWorkspaceRoot(), "remote.git")
|
|
1406
1406
|
};
|
|
1407
1407
|
}
|
|
1408
|
+
baseUpdateRunsInRequest() {
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1408
1411
|
requiresExistingRepo() {
|
|
1409
1412
|
return false;
|
|
1410
1413
|
}
|
|
@@ -1457,6 +1460,9 @@ var init_client_unsafe_strategy = __esm({
|
|
|
1457
1460
|
envVarName: "CANOPYCMS_REMOTE_URL"
|
|
1458
1461
|
};
|
|
1459
1462
|
}
|
|
1463
|
+
baseUpdateRunsInRequest() {
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1460
1466
|
requiresExistingRepo() {
|
|
1461
1467
|
return false;
|
|
1462
1468
|
}
|
|
@@ -8031,6 +8037,7 @@ var branchMetadataShape = {
|
|
|
8031
8037
|
archivedAt: z7.string().optional(),
|
|
8032
8038
|
rebaseFailure: z7.object({ message: z7.string(), firstAt: z7.string(), lastAt: z7.string() }).optional(),
|
|
8033
8039
|
historyRewrittenFrom: z7.string().optional(),
|
|
8040
|
+
historyRewrittenTo: z7.string().optional(),
|
|
8034
8041
|
lastGitHubTip: z7.string().optional(),
|
|
8035
8042
|
keptOverGitHub: z7.array(z7.string()).optional(),
|
|
8036
8043
|
rewrittenOnGitHub: z7.string().optional(),
|
|
@@ -8045,13 +8052,16 @@ var branchMetadataShape = {
|
|
|
8045
8052
|
z7.object({ kind: z7.literal("conflicts") }),
|
|
8046
8053
|
z7.object({ kind: z7.literal("retargeted"), base: z7.string() })
|
|
8047
8054
|
]).optional().catch(void 0),
|
|
8048
|
-
// Dropped when malformed: it
|
|
8055
|
+
// Dropped when malformed, never failing a read: it decorates the PR body, and without `commit`
|
|
8056
|
+
// the worker judges a move against the clone's head before it (worker/github-commits.ts).
|
|
8049
8057
|
review: z7.object({
|
|
8050
8058
|
state: z7.enum(["approved", "changes-requested"]),
|
|
8051
8059
|
by: z7.object({ userId: z7.string(), name: z7.string().optional() }),
|
|
8052
|
-
at: z7.string()
|
|
8060
|
+
at: z7.string(),
|
|
8061
|
+
commit: z7.string().optional()
|
|
8053
8062
|
}).optional().catch(void 0),
|
|
8054
8063
|
baseUpdateRequestedAt: z7.string().optional().catch(void 0),
|
|
8064
|
+
latestChangesWaiting: z7.string().optional().catch(void 0),
|
|
8055
8065
|
// Dropped when malformed: it only ends the editor's wait, which then times out instead.
|
|
8056
8066
|
lastBaseUpdate: z7.object({
|
|
8057
8067
|
at: z7.string(),
|
|
@@ -10312,10 +10322,14 @@ var GitManager = class _GitManager {
|
|
|
10312
10322
|
const status = await this.status();
|
|
10313
10323
|
return status.files.map((f) => f.path);
|
|
10314
10324
|
}
|
|
10325
|
+
/** The remote's URL as configured; `git remote -v`'s listing would split a path with a space. */
|
|
10315
10326
|
async getRemoteUrl() {
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10327
|
+
try {
|
|
10328
|
+
const url = (await this.git.raw(["remote", "get-url", this.remote])).trim();
|
|
10329
|
+
return url === "" ? void 0 : url;
|
|
10330
|
+
} catch {
|
|
10331
|
+
return void 0;
|
|
10332
|
+
}
|
|
10319
10333
|
}
|
|
10320
10334
|
/**
|
|
10321
10335
|
* Excludes `pattern` (e.g. `.canopy-meta/`) from this workspace's git, so it
|
|
@@ -11,7 +11,7 @@ export type PermissionSet = Readonly<Record<string, PermissionLevel>>;
|
|
|
11
11
|
* The App's entire security surface. Every entry names the call site that
|
|
12
12
|
* forces it — a permission justified only in a commit message can't be
|
|
13
13
|
* re-checked. Enumerated from `github-service.ts`,
|
|
14
|
-
* `worker/{task-runner,rebase}.ts`, and every git call reaching github.com;
|
|
14
|
+
* `worker/{task-runner,rebase,conflicts-status}.ts`, and every git call reaching github.com;
|
|
15
15
|
* `canopycms-cdk` makes no REST calls at all.
|
|
16
16
|
* - `contents: write` — git over HTTPS (first-boot clone, per-cycle branch
|
|
17
17
|
* fetch, pushes) and `octokit.git.deleteRef`. GitHub's reference puts ref
|
|
@@ -21,6 +21,9 @@ export type PermissionSet = Readonly<Record<string, PermissionLevel>>;
|
|
|
21
21
|
* - `pull_requests: write` — `pulls.create`/`pulls.update`, and the GraphQL
|
|
22
22
|
* mutations `markPullRequestReadyForReview`/`convertPullRequestToDraft`,
|
|
23
23
|
* derived by analogy since GitHub's reference has no line for them.
|
|
24
|
+
* - `statuses: write` — `repos.createCommitStatus`, in worker/github-gateway.ts
|
|
25
|
+
* `setConflictsStatus`: the "CanopyCMS conflicts" status the git sync sets on
|
|
26
|
+
* each pull request's head (worker/conflicts-status.ts).
|
|
24
27
|
* - `metadata: read` — implied by any repository permission; declared so
|
|
25
28
|
* this object states the whole surface, not just the non-automatic part.
|
|
26
29
|
* Nothing else: no `issues`, `administration`, `actions`, or organisation
|
|
@@ -9,7 +9,7 @@ const PERMISSION_LEVELS = ['read', 'write', 'admin'];
|
|
|
9
9
|
* The App's entire security surface. Every entry names the call site that
|
|
10
10
|
* forces it — a permission justified only in a commit message can't be
|
|
11
11
|
* re-checked. Enumerated from `github-service.ts`,
|
|
12
|
-
* `worker/{task-runner,rebase}.ts`, and every git call reaching github.com;
|
|
12
|
+
* `worker/{task-runner,rebase,conflicts-status}.ts`, and every git call reaching github.com;
|
|
13
13
|
* `canopycms-cdk` makes no REST calls at all.
|
|
14
14
|
* - `contents: write` — git over HTTPS (first-boot clone, per-cycle branch
|
|
15
15
|
* fetch, pushes) and `octokit.git.deleteRef`. GitHub's reference puts ref
|
|
@@ -19,6 +19,9 @@ const PERMISSION_LEVELS = ['read', 'write', 'admin'];
|
|
|
19
19
|
* - `pull_requests: write` — `pulls.create`/`pulls.update`, and the GraphQL
|
|
20
20
|
* mutations `markPullRequestReadyForReview`/`convertPullRequestToDraft`,
|
|
21
21
|
* derived by analogy since GitHub's reference has no line for them.
|
|
22
|
+
* - `statuses: write` — `repos.createCommitStatus`, in worker/github-gateway.ts
|
|
23
|
+
* `setConflictsStatus`: the "CanopyCMS conflicts" status the git sync sets on
|
|
24
|
+
* each pull request's head (worker/conflicts-status.ts).
|
|
22
25
|
* - `metadata: read` — implied by any repository permission; declared so
|
|
23
26
|
* this object states the whole surface, not just the non-automatic part.
|
|
24
27
|
* Nothing else: no `issues`, `administration`, `actions`, or organisation
|
|
@@ -30,6 +33,7 @@ const PERMISSION_LEVELS = ['read', 'write', 'admin'];
|
|
|
30
33
|
export const CANOPY_APP_PERMISSIONS = {
|
|
31
34
|
contents: 'write',
|
|
32
35
|
pull_requests: 'write',
|
|
36
|
+
statuses: 'write',
|
|
33
37
|
metadata: 'read',
|
|
34
38
|
};
|
|
35
39
|
/**
|
package/dist/cli/init.js
CHANGED
|
@@ -1588,7 +1588,7 @@ var DevClientSafeStrategy = class {
|
|
|
1588
1588
|
return false;
|
|
1589
1589
|
}
|
|
1590
1590
|
syncsBranchesWithBase() {
|
|
1591
|
-
return
|
|
1591
|
+
return true;
|
|
1592
1592
|
}
|
|
1593
1593
|
getPermissionsFileName() {
|
|
1594
1594
|
return "permissions.json";
|
|
@@ -1777,6 +1777,9 @@ var ProdStrategy = class extends ProdClientSafeStrategy {
|
|
|
1777
1777
|
autoDetectRemotePath: path.join(this.getWorkspaceRoot(), "remote.git")
|
|
1778
1778
|
};
|
|
1779
1779
|
}
|
|
1780
|
+
baseUpdateRunsInRequest() {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1780
1783
|
requiresExistingRepo() {
|
|
1781
1784
|
return false;
|
|
1782
1785
|
}
|
|
@@ -1829,6 +1832,9 @@ var DevStrategy = class extends DevClientSafeStrategy {
|
|
|
1829
1832
|
envVarName: "CANOPYCMS_REMOTE_URL"
|
|
1830
1833
|
};
|
|
1831
1834
|
}
|
|
1835
|
+
baseUpdateRunsInRequest() {
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1832
1838
|
requiresExistingRepo() {
|
|
1833
1839
|
return false;
|
|
1834
1840
|
}
|
package/dist/editor/Editor.js
CHANGED
|
@@ -197,6 +197,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
197
197
|
const requestLatestRef = useRef(async () => { });
|
|
198
198
|
const modifiedCountRef = useRef(0);
|
|
199
199
|
const namesBranchRef = useRef(() => false);
|
|
200
|
+
// Set once the entries and the changes read exist.
|
|
201
|
+
const rereadPinnedBranchRef = useRef(async () => { });
|
|
200
202
|
const { branchName: branchNameState, setBranchName, branches, branchSummaries, currentBranch, currentAddress, listedBranch, namesBranch, urlBranchName, branchesLoadFailed, branchesLoaded, addCreatedBranch, handleSubmit, handleWithdraw, handleApprove, handleRequestChanges, handleDelete, handleArchive, handleReloadBranchData, loadBranches, } = useBranchManager({
|
|
201
203
|
initialBranch: branchName,
|
|
202
204
|
operatingMode,
|
|
@@ -206,6 +208,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
206
208
|
getUnsaved: () => resolveUnsavedRef.current(),
|
|
207
209
|
onStamped: (stamped) => applySubmitStampsRef.current(stamped),
|
|
208
210
|
onChangesRequested: (branch) => reloadCommentsRef.current(branch),
|
|
211
|
+
onReviewPinned: (branch) => rereadPinnedBranchRef.current(branch),
|
|
209
212
|
// Only the open branch, with nothing unsaved: its wait and answer show where the user is.
|
|
210
213
|
latestChangesAfterWithdraw: (branch) => namesBranchRef.current(branch) && modifiedCountRef.current === 0
|
|
211
214
|
? () => requestLatestRef.current()
|
|
@@ -270,7 +273,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
270
273
|
// Set once the branch actions below exist; they depend on the drafts, which depend on the entries.
|
|
271
274
|
const switchBranchRef = useRef(async () => { });
|
|
272
275
|
// Set once the changes read exists; it waits on the panel state, declared after the entries.
|
|
273
|
-
const reloadChangesRef = useRef(() => { });
|
|
276
|
+
const reloadChangesRef = useRef(async () => { });
|
|
277
|
+
const rereadChangesRef = useRef(async () => { });
|
|
274
278
|
// 2. Entry manager (depends on branchNameState, owns selectedPath)
|
|
275
279
|
const { selectedPath, setSelectedPath, closeEntry, entries: entriesState, collections: collectionsFromApi, currentEntry, availableSchemas, entriesInitializing, entriesLoadFailed, navigatorOpen, setNavigatorOpen, refreshEntries, refreshContent, handleCreateEntry, renameEntry, loadEntry, readEntryValue, saveEntry, getEntryVersion, adoptEntryVersion, followMovedEntry, recreateEntry, createModalOpen, createModalCollection, createModalError, createModalCreating, createModalExistingSlugs, handleCreateModalSubmit, closeCreateModal, } = useEntryManager({
|
|
276
280
|
initialEntries: entries,
|
|
@@ -291,7 +295,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
291
295
|
// A short name in the URL is no switch to another branch.
|
|
292
296
|
sameBranch: namesBranch,
|
|
293
297
|
urlBranchName,
|
|
294
|
-
onContentRefresh: (branch) => reloadChangesRef.current(branch),
|
|
298
|
+
onContentRefresh: (branch) => void reloadChangesRef.current(branch),
|
|
295
299
|
conflictResolutions: {
|
|
296
300
|
pending: conflictChoices.pending,
|
|
297
301
|
onSaved: (branch, contentId, sent) => {
|
|
@@ -376,7 +380,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
376
380
|
setPreviewError(null);
|
|
377
381
|
}, [currentEntry?.contentId]);
|
|
378
382
|
// 3. Draft manager (depends on branchNameState, selectedPath from useEntryManager)
|
|
379
|
-
const { drafts, setDrafts, loadedValues, setLoadedValues, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, singleSaving, lastSaved, handleSaveAll, saveAllProgress, isSaveAllRunning, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, isDirtyForEntry, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
|
|
383
|
+
const { drafts, setDrafts, loadedValues, setLoadedValues, forgetLoadedValues, readGeneration, loadedValue, effectiveValue, modifiedCount, editedFiles, handleSave, singleSaving, lastSaved, handleSaveAll, saveAllProgress, isSaveAllRunning, handleDiscardDrafts, handleDiscardFileDraft, handleReload, isSelectedDirty, isDirtyForEntry, resolveUnsaved, fieldErrors, draftStorageFailed, applySubmitStamps, } = useDraftManager({
|
|
380
384
|
branchName: branchNameState,
|
|
381
385
|
selectedPath,
|
|
382
386
|
currentEntry,
|
|
@@ -420,6 +424,17 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
420
424
|
onShowConflicts: () => showConflictsRef.current(),
|
|
421
425
|
});
|
|
422
426
|
requestLatestRef.current = baseUpdate.request;
|
|
427
|
+
// The open branch was pinned for its reviews: its entries, their changes and every clean entry
|
|
428
|
+
// are read again, every one while it is locked, which shows the saved copy over a draft.
|
|
429
|
+
rereadPinnedBranchRef.current = async (branch) => {
|
|
430
|
+
if (!namesBranch(branch, branchNameState))
|
|
431
|
+
return;
|
|
432
|
+
forgetLoadedValues((id) => !branchContentLocked && draftsRef.current[id] !== undefined);
|
|
433
|
+
await Promise.all([
|
|
434
|
+
refreshEntries(branchNameState, { withChanges: false }),
|
|
435
|
+
rereadChangesRef.current(),
|
|
436
|
+
]);
|
|
437
|
+
};
|
|
423
438
|
modifiedCountRef.current = modifiedCount;
|
|
424
439
|
namesBranchRef.current = (branch) => namesBranch(branch, branchNameState);
|
|
425
440
|
// The request in flight counts as waiting: a save landing before the worker reads the branch
|
|
@@ -792,6 +807,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
792
807
|
// write still the state of the branch on screen?".
|
|
793
808
|
const requestBranch = branchNameState;
|
|
794
809
|
const inFlightKey = `${requestBranch}:${contentId ?? ''}`;
|
|
810
|
+
const generation = readGeneration.current;
|
|
811
|
+
const loadingKey = `${inFlightKey}#${generation}`;
|
|
795
812
|
const load = async () => {
|
|
796
813
|
if (!currentEntry || !contentId || loadedValues[contentId] !== undefined)
|
|
797
814
|
return;
|
|
@@ -801,13 +818,13 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
801
818
|
// Guard against overlapping renders re-firing the same in-flight load
|
|
802
819
|
// (e.g. `currentEntry` getting a new object reference from an entries
|
|
803
820
|
// refresh while the fetch below is still pending).
|
|
804
|
-
if (loadingEntryIdsRef.current.has(
|
|
821
|
+
if (loadingEntryIdsRef.current.has(loadingKey))
|
|
805
822
|
return;
|
|
806
|
-
loadingEntryIdsRef.current.add(
|
|
823
|
+
loadingEntryIdsRef.current.add(loadingKey);
|
|
807
824
|
setEntriesLoading(true);
|
|
808
825
|
setLoadFailedKey((failed) => (failed === inFlightKey ? null : failed));
|
|
809
826
|
try {
|
|
810
|
-
const loaded = await loadEntry(currentEntry);
|
|
827
|
+
const loaded = await loadEntry(currentEntry, () => readGeneration.current === generation);
|
|
811
828
|
// A branch switch during the await makes this response another
|
|
812
829
|
// branch's content. Writing it would (a) show it as the new branch's
|
|
813
830
|
// content and (b) satisfy the skip gate above, permanently suppressing
|
|
@@ -816,6 +833,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
816
833
|
// instead; the new branch has its own load running under its own key.
|
|
817
834
|
if (currentBranchRef.current !== requestBranch)
|
|
818
835
|
return;
|
|
836
|
+
if (readGeneration.current !== generation)
|
|
837
|
+
return;
|
|
819
838
|
setLoadedValues((prev) => ({ ...prev, [contentId]: loaded }));
|
|
820
839
|
// No draft is seeded here. `effectiveValue` is
|
|
821
840
|
// `drafts[id] ?? loadedValues[id]`, so the form renders from the line
|
|
@@ -825,7 +844,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
825
844
|
// session as a dirty, stale full-content snapshot.
|
|
826
845
|
}
|
|
827
846
|
finally {
|
|
828
|
-
loadingEntryIdsRef.current.delete(
|
|
847
|
+
loadingEntryIdsRef.current.delete(loadingKey);
|
|
829
848
|
// Clear the shared flag if this load's entry is still selected on the
|
|
830
849
|
// branch it was loaded for, OR if no other load remains in flight. The
|
|
831
850
|
// second arm matters when the user navigates from a slow-loading entry
|
|
@@ -834,7 +853,8 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
834
853
|
// this stale settle would leave `entriesLoading` (and therefore
|
|
835
854
|
// `busy`) stuck true with nothing left to clear it.
|
|
836
855
|
if ((currentBranchRef.current === requestBranch &&
|
|
837
|
-
currentContentIdRef.current === contentId
|
|
856
|
+
currentContentIdRef.current === contentId &&
|
|
857
|
+
readGeneration.current === generation) ||
|
|
838
858
|
loadingEntryIdsRef.current.size === 0) {
|
|
839
859
|
setEntriesLoading(false);
|
|
840
860
|
}
|
|
@@ -850,7 +870,9 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
850
870
|
// Same staleness guard as above: don't tell the user an entry failed
|
|
851
871
|
// to load when they've already navigated to a different one (or to a
|
|
852
872
|
// different branch).
|
|
853
|
-
if (currentBranchRef.current === requestBranch &&
|
|
873
|
+
if (currentBranchRef.current === requestBranch &&
|
|
874
|
+
currentContentIdRef.current === contentId &&
|
|
875
|
+
readGeneration.current === generation) {
|
|
854
876
|
notifications.show({ message: 'Failed to load entry', color: 'red' });
|
|
855
877
|
setLoadFailedKey(inFlightKey);
|
|
856
878
|
}
|
|
@@ -1348,10 +1370,11 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1348
1370
|
const changesAvailable = !!currentBranch && currentBranch.isProtected === false;
|
|
1349
1371
|
const changesRead = (contentPanelShown || !!currentEntry) && changesAvailable;
|
|
1350
1372
|
const branchChanges = useBranchChangesData(apiClient, branchNameState, changesRead);
|
|
1351
|
-
reloadChangesRef.current = (branch) => {
|
|
1373
|
+
reloadChangesRef.current = async (branch) => {
|
|
1352
1374
|
if (branch === branchNameState)
|
|
1353
|
-
|
|
1375
|
+
await branchChanges.reload();
|
|
1354
1376
|
};
|
|
1377
|
+
rereadChangesRef.current = branchChanges.reread;
|
|
1355
1378
|
// Submit, withdraw and a review move the branch's status, which no entries refresh follows.
|
|
1356
1379
|
const changesStatusRef = useRef({ branch: branchNameState, status: currentBranch?.status });
|
|
1357
1380
|
useEffect(() => {
|
|
@@ -1424,7 +1447,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1424
1447
|
focusFieldTarget(target.element);
|
|
1425
1448
|
});
|
|
1426
1449
|
};
|
|
1427
|
-
const resolveEntryConflict = async (logicalPath, id, mergeSha, choice) => {
|
|
1450
|
+
const resolveEntryConflict = async (logicalPath, id, mergeSha, choice, entryGone) => {
|
|
1428
1451
|
const branch = branchNameState;
|
|
1429
1452
|
const failed = {
|
|
1430
1453
|
tone: 'error',
|
|
@@ -1432,7 +1455,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1432
1455
|
};
|
|
1433
1456
|
let answer;
|
|
1434
1457
|
try {
|
|
1435
|
-
answer = await apiClient.conflicts.resolveEntry({ branch, entryPath: logicalPath }, { id: id, mergeSha, choice });
|
|
1458
|
+
answer = await apiClient.conflicts.resolveEntry({ branch, entryPath: logicalPath }, { id: id, mergeSha, choice, ...(entryGone && { entryGone }) });
|
|
1436
1459
|
}
|
|
1437
1460
|
catch (err) {
|
|
1438
1461
|
if (!isUnanswered(err))
|
|
@@ -1513,7 +1536,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1513
1536
|
});
|
|
1514
1537
|
};
|
|
1515
1538
|
const changesResolveEntry = changesSubject && entryConflict.data
|
|
1516
|
-
? async (choice) => {
|
|
1539
|
+
? async (choice, { entryGone }) => {
|
|
1517
1540
|
const { contentId: id, mergeSha, kind } = entryConflict.data ?? {};
|
|
1518
1541
|
if (!id || !mergeSha)
|
|
1519
1542
|
return undefined;
|
|
@@ -1528,7 +1551,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
1528
1551
|
setChangesOpen(false);
|
|
1529
1552
|
return undefined;
|
|
1530
1553
|
}
|
|
1531
|
-
return resolveEntryConflict(changesSubject.logicalPath, id, mergeSha, choice);
|
|
1554
|
+
return resolveEntryConflict(changesSubject.logicalPath, id, mergeSha, choice, entryGone);
|
|
1532
1555
|
}
|
|
1533
1556
|
: undefined;
|
|
1534
1557
|
const confirmEntryChoice = (target) => {
|
|
@@ -2077,7 +2100,7 @@ const EditorContent = ({ entries, title, siteTitle = title, branchName = '', col
|
|
|
2077
2100
|
// fail-closed computation of "are writes blocked" in this component.
|
|
2078
2101
|
branchIsProtected: currentBranch?.isProtected ?? true, branchServerProtected: currentBranch?.isProtected, branchReadOnly: currentBranch?.readOnly, branchHold: currentBranch?.hold, openConflicts: openConflictCount, conflictRecordHeld: branchConflicts.data?.held
|
|
2079
2102
|
? { ...(userIsAdmin && { onRelease: handleReleaseConflictsHold }) }
|
|
2080
|
-
: undefined, branchApproveBlocked: currentBranch?.approveBlocked, branchWriteBlocked: branchContentLocked, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, branchToggleRef: branchToggleRef, baseUpdate: baseUpdateControl, latestWait: latestWait, onBranchReloadData: () => {
|
|
2103
|
+
: undefined, branchApproveBlocked: currentBranch?.approveBlocked, branchWriteBlocked: branchContentLocked, branchLoading: branchLoading, branchPending: branchPending, branchMissing: branchMissing, branchToggleRef: branchToggleRef, baseUpdate: baseUpdateControl, latestWait: latestWait, latestChangesWaiting: baseUpdate.latestChangesWaiting, onBranchReloadData: () => {
|
|
2081
2104
|
void Promise.all([handleReloadBranchData(), refreshEntries(branchNameState)]).catch(console.error);
|
|
2082
2105
|
}, onBranchDiscardDrafts: () => handleDiscardDrafts(() => branchToggleRef.current), onBranchManagerOpen: openBranchManager, onNewBranch: () => openBranchManager({ create: true }), onSaveAll: runSaveAll, saveAllProgress: saveAllProgress,
|
|
2083
2106
|
// On any entry: EntryHeader's `saving` is the open entry's alone.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConflictsStatusFailure } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The last git sync could not set the "CanopyCMS conflicts" status on some pull requests
|
|
4
|
+
* (worker/conflicts-status.ts).
|
|
5
|
+
*/
|
|
6
|
+
export declare function ConflictsStatusAlert({ failure }: {
|
|
7
|
+
failure: ConflictsStatusFailure;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Alert, Code, Group, List, Stack, Text } from '@mantine/core';
|
|
4
|
+
import { IconAlertTriangle } from '@tabler/icons-react';
|
|
5
|
+
import { escapeForLog } from '../../utils/escape-text.js';
|
|
6
|
+
import { RelativeTime } from '../components/RelativeTime.js';
|
|
7
|
+
/** A long unbroken name wraps inside the alert rather than widening it. */
|
|
8
|
+
const WRAP = { overflowWrap: 'anywhere' };
|
|
9
|
+
const WHY = {
|
|
10
|
+
permission: (_jsxs(_Fragment, { children: [_jsx(Text, { size: "sm", children: "GitHub refused it: the worker's GitHub credential lacks the \"Commit statuses\" permission." }), _jsxs(List, { size: "sm", spacing: 2, children: [_jsxs(List.Item, { children: ["A GitHub App: its owner adds \"Commit statuses: read and write\" in the App's permissions on GitHub, then an owner of the account it is installed on accepts the request in the installation's settings.", ' ', _jsx(Code, { style: { whiteSpace: 'nowrap' }, children: "canopycms init-github-app verify" }), " confirms it."] }), _jsx(List.Item, { children: "A fine-grained token: give it \"Commit statuses: read and write\". A classic token's repo scope already has it." })] })] })),
|
|
11
|
+
unreachable: (_jsx(Text, { size: "sm", children: "GitHub couldn't be reached, or was busy. The next git sync tries again." })),
|
|
12
|
+
other: (_jsx(Text, { size: "sm", children: "GitHub refused it, or the branch's record of conflicts couldn't be read. The next git sync tries again; if it keeps failing, the worker log names each branch and why." })),
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The last git sync could not set the "CanopyCMS conflicts" status on some pull requests
|
|
16
|
+
* (worker/conflicts-status.ts).
|
|
17
|
+
*/
|
|
18
|
+
export function ConflictsStatusAlert({ failure }) {
|
|
19
|
+
const unlisted = failure.count - failure.branches.length;
|
|
20
|
+
const pullRequests = `${failure.count} pull request${failure.count === 1 ? '' : 's'}`;
|
|
21
|
+
return (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertTriangle, { size: 16 }), title: `Conflicts status not set on ${pullRequests}`, "data-testid": "conflicts-status-alert", children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: "The \"CanopyCMS conflicts\" check on the pull request of each branch below keeps its last result, or has none yet. Where it is a required check, merging waits on it. Saving, submitting and pull request updates are unaffected." }), WHY[failure.reason], _jsx(Group, { gap: 4, children: failure.branches.map((name) => (_jsx(Code, { style: WRAP, children: escapeForLog(name) }, name))) }), unlisted > 0 && _jsxs(Text, { size: "sm", children: ["And ", unlisted, " more."] }), _jsxs(Text, { size: "xs", c: "dimmed", style: WRAP, children: ["The last error: \u201C", escapeForLog(failure.message), "\u201D"] }), _jsx(RelativeTime, { iso: failure.at, prefix: "Last tried ", size: "xs", c: "dimmed" })] }) }));
|
|
22
|
+
}
|
|
@@ -21,6 +21,7 @@ import { ageDeletionHold, archivedBefore, archivedCutoff, deletableByAge, } from
|
|
|
21
21
|
import { escapeForLog } from '../../utils/escape-text.js';
|
|
22
22
|
import { TooltipDetail } from '../components/TooltipDetail.js';
|
|
23
23
|
import { DISMISS_UNPUBLISHED_ATTRIBUTE, UnpublishedAtMergeAlert } from './UnpublishedAtMergeAlert.js';
|
|
24
|
+
import { ConflictsStatusAlert } from './ConflictsStatusAlert.js';
|
|
24
25
|
import { HOLD_BADGE, branchHoldMessage, branchStatusPresentation, endSentence, } from '../branch-status.js';
|
|
25
26
|
// ============================================================================
|
|
26
27
|
// Small pure helpers
|
|
@@ -132,7 +133,7 @@ function BaseHoldAlert({ hold }) {
|
|
|
132
133
|
* and never listed. Names come from GitHub and are attacker-chosen, so each is shown escaped.
|
|
133
134
|
*/
|
|
134
135
|
function DivergedBranchesAlert({ names }) {
|
|
135
|
-
return (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertTriangle, { size: 16 }), title: `${plural(names.length, 'branch')} diverged from GitHub`, "data-testid": "diverged-branches-alert", children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: "This deployment's copy of each branch below holds editor work that conflicts with the branch on GitHub, usually because the GitHub branch was rebased or force-pushed, or because another CanopyCMS deployment sharing this repository uses the same branch name. Neither copy has been changed, and publishing the branch fails until they agree. A branch that only gained commits on GitHub clears on its own at the next sync." }), _jsx(Text, { size: "sm", children: "A developer decides which version to keep. To keep GitHub's, first push it to a new branch name on GitHub, because \"Delete branch\" in the editor also deletes a published branch on GitHub; then delete this branch in the editor (withdraw it from review first if it is in review). The editor's changes cannot be copied to another branch: to keep them, leave this branch as it is, or re-enter them by hand in a new branch. For the base branch or a settings branch, ask a developer to check what changed on GitHub." }), _jsx(Group, { gap: 4, children: names.map((name) => (_jsx(Code, { children: escapeForLog(name) }, name))) })] }) }));
|
|
136
|
+
return (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertTriangle, { size: 16 }), title: `${plural(names.length, 'branch')} diverged from GitHub`, "data-testid": "diverged-branches-alert", children: _jsxs(Stack, { gap: 4, children: [_jsx(Text, { size: "sm", children: "This deployment's copy of each branch below holds editor work that conflicts with the branch on GitHub, usually because the GitHub branch was rebased or force-pushed, or because another CanopyCMS deployment sharing this repository uses the same branch name. Neither copy has been changed, and publishing the branch fails until they agree. A branch that only gained commits on GitHub clears on its own at the next sync." }), _jsx(Text, { size: "sm", children: "A developer decides which version to keep. To keep GitHub's, first push it to a new branch name on GitHub, because \"Delete branch\" in the editor also deletes a published branch on GitHub; then delete this branch in the editor (withdraw it from review first if it is in review). The editor's changes cannot be copied to another branch: to keep them, leave this branch as it is, or re-enter them by hand in a new branch. For the base branch or a settings branch, ask a developer to check what changed on GitHub." }), _jsx(Group, { gap: 4, children: names.map((name) => (_jsx(Code, { style: { overflowWrap: 'anywhere' }, children: escapeForLog(name) }, name))) })] }) }));
|
|
136
137
|
}
|
|
137
138
|
/**
|
|
138
139
|
* The branch namespace the CMS creates branches in, against the one the GitHub gateway writes. A
|
|
@@ -150,7 +151,7 @@ function BranchNamespaceAlert({ own, gateway, }) {
|
|
|
150
151
|
}
|
|
151
152
|
/** Branch refs from metadata the CMS Lambda can write, so each is shown escaped. */
|
|
152
153
|
function BranchRefs({ branches }) {
|
|
153
|
-
return (_jsx(Group, { gap: 4, children: branches.map((branch, i) => (_jsx(Code, { children: escapeForLog(branch.name) }, i))) }));
|
|
154
|
+
return (_jsx(Group, { gap: 4, children: branches.map((branch, i) => (_jsx(Code, { style: { overflowWrap: 'anywhere' }, children: escapeForLog(branch.name) }, i))) }));
|
|
154
155
|
}
|
|
155
156
|
/**
|
|
156
157
|
* Editor branches the GitHub gateway will not publish, by ref, which is what a developer lists
|
|
@@ -307,7 +308,7 @@ function OverviewTab({ health, overviewTabRef, }) {
|
|
|
307
308
|
workerVersion !== build.canopycmsVersion;
|
|
308
309
|
return (_jsxs(Stack, { ref: overviewRef, gap: "md", children: [error && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), variant: "light", children: error })), _jsxs(Group, { gap: "sm", children: [_jsx(Badge, { color: liveness.color, variant: "light", size: "lg", children: liveness.label }), status.mode === 'dev' && (_jsx(Text, { size: "xs", c: "dimmed", children: "No worker runs in dev mode" }))] }), isRecentFatalError && lastFatalError && (_jsxs(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: lastFatalError.phase === 'startup'
|
|
309
310
|
? 'The worker failed to start'
|
|
310
|
-
: 'The worker stopped while running', children: [_jsx(Text, { size: "sm", children: lastFatalError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", lastFatalError.at] })] })), status.settingsWorkspaceError && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Settings workspace unavailable: groups and path rules are not loading", children: _jsx(Text, { size: "sm", children: status.settingsWorkspaceError }) })), status.schemaIssues && status.schemaIssues.length > 0 && (_jsx(Alert, { color: "yellow", icon: _jsx(IconAlertTriangle, { size: 16 }), title: "Content types this editor version doesn't know", "data-testid": "schema-issues-alert", children: _jsx(Stack, { gap: 4, children: status.schemaIssues.map((issue) => (_jsx(Text, { size: "sm", children: issue.kind === 'unknown-schema' ? (_jsxs(_Fragment, { children: [_jsx(Code, { children: issue.metaFile }), " names ", _jsx(Code, { children: issue.schemaRef }), ", so entry type ", _jsx(Code, { children: issue.entryType }), " is unavailable"] })) : (issue.message) }, `${issue.kind}:${issue.message}`))) }) })), _jsx(BranchNamespaceAlert, { own: status.branchNamespace, gateway: status.workerStatus?.gatewayBranchNamespace }), status.workerStatus?.baseHold && _jsx(BaseHoldAlert, { hold: status.workerStatus.baseHold }), unpublished.length > 0 && (_jsx(UnpublishedAtMergeAlert, { branches: unpublished, onDismiss: handleDismissUnpublished })), status.statusReadError && (_jsxs(Text, { size: "xs", c: "orange", children: ["Warning: could not read worker status (", status.statusReadError, ")"] })), lastGitSync?.tracked && lastGitSync.tracked.diverged.length > 0 && (_jsx(DivergedBranchesAlert, { names: lastGitSync.tracked.diverged })), status.workerStatus?.unpublishableBranches && (_jsx(UnpublishableBranchesAlert, { report: status.workerStatus.unpublishableBranches, namespace: status.workerStatus.gatewayBranchNamespace })), status.workerStatus?.lastGitSyncError && (_jsxs(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Last git sync failed", children: [_jsx(Text, { size: "sm", children: status.workerStatus.lastGitSyncError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", status.workerStatus.lastGitSyncError.at] })] })), versionSkew && (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "API and worker versions differ", "data-testid": "version-skew-warning", children: _jsxs(Text, { size: "sm", children: ["The API runs canopycms ", build.canopycmsVersion, " but the worker runs canopycms", ' ', workerVersion, ". They were deployed from different builds."] }) })), _jsxs(Paper, { withBorder: true, p: "sm", children: [_jsx(Text, { size: "sm", fw: 600, children: "Build" }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-api-version", children: ["API: canopycms ", build.canopycmsVersion] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-source-revision", children: ["Source revision:", ' ', build.sourceRevision ? (_jsx(Tooltip, { label: build.sourceRevision, children: _jsx(Code, { children: build.sourceRevision.slice(0, 12) }) })) : ('not set (pass the CANOPY_SOURCE_SHA build arg to the image build)')] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-worker-version", children: ["Worker version: ", workerVersion ? `canopycms ${workerVersion}` : 'unknown'] }), lastShutdown && (_jsxs(Text, { size: "xs", c: lastShutdown.outcome === 'drained' ? 'dimmed' : 'orange', "data-testid": "build-last-shutdown", children: ["Last worker shutdown: ", lastShutdown.reason, " at ", lastShutdown.at, lastShutdown.outcome === 'deadline' &&
|
|
311
|
+
: 'The worker stopped while running', children: [_jsx(Text, { size: "sm", children: lastFatalError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", lastFatalError.at] })] })), status.settingsWorkspaceError && (_jsx(Alert, { color: "red", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Settings workspace unavailable: groups and path rules are not loading", children: _jsx(Text, { size: "sm", children: status.settingsWorkspaceError }) })), status.schemaIssues && status.schemaIssues.length > 0 && (_jsx(Alert, { color: "yellow", icon: _jsx(IconAlertTriangle, { size: 16 }), title: "Content types this editor version doesn't know", "data-testid": "schema-issues-alert", children: _jsx(Stack, { gap: 4, children: status.schemaIssues.map((issue) => (_jsx(Text, { size: "sm", children: issue.kind === 'unknown-schema' ? (_jsxs(_Fragment, { children: [_jsx(Code, { children: issue.metaFile }), " names ", _jsx(Code, { children: issue.schemaRef }), ", so entry type ", _jsx(Code, { children: issue.entryType }), " is unavailable"] })) : (issue.message) }, `${issue.kind}:${issue.message}`))) }) })), _jsx(BranchNamespaceAlert, { own: status.branchNamespace, gateway: status.workerStatus?.gatewayBranchNamespace }), status.workerStatus?.baseHold && _jsx(BaseHoldAlert, { hold: status.workerStatus.baseHold }), unpublished.length > 0 && (_jsx(UnpublishedAtMergeAlert, { branches: unpublished, onDismiss: handleDismissUnpublished })), status.statusReadError && (_jsxs(Text, { size: "xs", c: "orange", children: ["Warning: could not read worker status (", status.statusReadError, ")"] })), lastGitSync?.tracked && lastGitSync.tracked.diverged.length > 0 && (_jsx(DivergedBranchesAlert, { names: lastGitSync.tracked.diverged })), status.workerStatus?.unpublishableBranches && (_jsx(UnpublishableBranchesAlert, { report: status.workerStatus.unpublishableBranches, namespace: status.workerStatus.gatewayBranchNamespace })), status.workerStatus?.conflictsStatusFailure && (_jsx(ConflictsStatusAlert, { failure: status.workerStatus.conflictsStatusFailure })), status.workerStatus?.lastGitSyncError && (_jsxs(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "Last git sync failed", children: [_jsx(Text, { size: "sm", children: status.workerStatus.lastGitSyncError.message }), _jsxs(Text, { size: "xs", c: "dimmed", mt: 4, children: ["at ", status.workerStatus.lastGitSyncError.at] })] })), versionSkew && (_jsx(Alert, { color: "orange", icon: _jsx(IconAlertCircle, { size: 16 }), title: "API and worker versions differ", "data-testid": "version-skew-warning", children: _jsxs(Text, { size: "sm", children: ["The API runs canopycms ", build.canopycmsVersion, " but the worker runs canopycms", ' ', workerVersion, ". They were deployed from different builds."] }) })), _jsxs(Paper, { withBorder: true, p: "sm", children: [_jsx(Text, { size: "sm", fw: 600, children: "Build" }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-api-version", children: ["API: canopycms ", build.canopycmsVersion] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-source-revision", children: ["Source revision:", ' ', build.sourceRevision ? (_jsx(Tooltip, { label: build.sourceRevision, children: _jsx(Code, { children: build.sourceRevision.slice(0, 12) }) })) : ('not set (pass the CANOPY_SOURCE_SHA build arg to the image build)')] }), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-worker-version", children: ["Worker version: ", workerVersion ? `canopycms ${workerVersion}` : 'unknown'] }), lastShutdown && (_jsxs(Text, { size: "xs", c: lastShutdown.outcome === 'drained' ? 'dimmed' : 'orange', "data-testid": "build-last-shutdown", children: ["Last worker shutdown: ", lastShutdown.reason, " at ", lastShutdown.at, lastShutdown.outcome === 'deadline' &&
|
|
311
312
|
` · drain deadline hit, aborted ${lastShutdown.abandoned?.join(', ') ?? 'in-flight work'}`, lastShutdown.outcome === 'drained' &&
|
|
312
313
|
lastShutdown.drainMs !== undefined &&
|
|
313
314
|
` · drained in ${(lastShutdown.drainMs / 1000).toFixed(1)}s`, lastShutdown.outcome === 'not-drained' && ' (a crash or a forced stop)'] })), lastFatalError?.phase === 'startup' && (_jsxs(Text, { size: "xs", c: "orange", "data-testid": "build-failed-start", children: ["Last start failed at ", lastFatalError.at] })), _jsxs(Text, { size: "xs", c: "dimmed", "data-testid": "build-media", children: ["Media storage:", ' ', status.assetStore.configured ? 'configured' : 'not configured — uploads are disabled'] }), _jsxs(Text, { size: "xs", c: status.imageProcessing.available ? 'dimmed' : 'orange', "data-testid": "build-image-processing", children: ["Image processing:", ' ', status.imageProcessing.available
|
|
@@ -38,6 +38,10 @@ export declare const GET_LATEST_CHANGES_TOOLTIP = "Bring in changes made on the
|
|
|
38
38
|
/** The Tooltip; an admin also learns which branch the changes come from. */
|
|
39
39
|
export declare function getLatestChangesTooltip(baseBranch: string | undefined, isAdmin: boolean): string;
|
|
40
40
|
export declare const GETTING_LATEST_CHANGES = "Getting latest changes\u2026";
|
|
41
|
+
/** The bar's badge while the automatic sync leaves the base out for the branch's saved work. */
|
|
42
|
+
export declare const LATEST_CHANGES_WAITING = "Latest changes waiting";
|
|
43
|
+
/** The badge's detail: below `md` the action is in the branch menu, and unsaved changes hold it. */
|
|
44
|
+
export declare function latestChangesWaitingDetail(inMenu: boolean, saveFirst: boolean): string;
|
|
41
45
|
export declare const SAVE_FIRST_REASON = "Save your changes first.";
|
|
42
46
|
export declare const WAITING_NOTICE = "Getting the latest changes. You can edit again when it finishes.";
|
|
43
47
|
export declare const SLOW_NOTICE = "This is taking longer than usual. You can keep editing; the latest changes will come in when the server gets to them.";
|
|
@@ -50,6 +50,13 @@ export function getLatestChangesTooltip(baseBranch, isAdmin) {
|
|
|
50
50
|
: GET_LATEST_CHANGES_TOOLTIP;
|
|
51
51
|
}
|
|
52
52
|
export const GETTING_LATEST_CHANGES = 'Getting latest changes…';
|
|
53
|
+
/** The bar's badge while the automatic sync leaves the base out for the branch's saved work. */
|
|
54
|
+
export const LATEST_CHANGES_WAITING = 'Latest changes waiting';
|
|
55
|
+
/** The badge's detail: below `md` the action is in the branch menu, and unsaved changes hold it. */
|
|
56
|
+
export function latestChangesWaitingDetail(inMenu, saveFirst) {
|
|
57
|
+
const control = `"Get latest changes"${inMenu ? ' in the branch menu' : ''}`;
|
|
58
|
+
return `Newer changes made on the site are waiting for this branch. ${saveFirst ? `Save your changes, then choose ${control}` : `Choose ${control}`} to bring them in.`;
|
|
59
|
+
}
|
|
53
60
|
export const SAVE_FIRST_REASON = 'Save your changes first.';
|
|
54
61
|
export const WAITING_NOTICE = 'Getting the latest changes. You can edit again when it finishes.';
|
|
55
62
|
export const SLOW_NOTICE = 'This is taking longer than usual. You can keep editing; the latest changes will come in when the server gets to them.';
|
|
@@ -56,6 +56,8 @@ export declare const LATEST_UNPLACEABLE_REASON = "The latest version of this can
|
|
|
56
56
|
* @internal Exported for tests.
|
|
57
57
|
*/
|
|
58
58
|
export declare function conflictHeading(card: ConflictCard): string;
|
|
59
|
+
/** @internal Exported for tests: how long a settled entry card waits for the reload to take it. */
|
|
60
|
+
export declare const SETTLED_HOLD_MS = 10000;
|
|
59
61
|
/**
|
|
60
62
|
* Why an entry-level choice didn't act: `warning` where nothing is lost or the outcome is unknown,
|
|
61
63
|
* `error` for a failure a retry may pass.
|
|
@@ -64,6 +66,14 @@ export interface EntryChoiceRefusal {
|
|
|
64
66
|
message: string;
|
|
65
67
|
tone: 'warning' | 'error';
|
|
66
68
|
}
|
|
69
|
+
/** An entry-level choice settled in place: what to say, and whether the entry stays deleted. */
|
|
70
|
+
export interface EntryChoiceDone {
|
|
71
|
+
message: string;
|
|
72
|
+
entryGone: boolean;
|
|
73
|
+
}
|
|
74
|
+
type ResolveEntryChoice = (choice: EntryConflictChoice, options: {
|
|
75
|
+
entryGone?: true;
|
|
76
|
+
}) => Promise<EntryChoiceRefusal | undefined>;
|
|
67
77
|
/** The open entry's form, which field choices change. */
|
|
68
78
|
interface ConflictsForm {
|
|
69
79
|
value: FormValue;
|
|
@@ -85,7 +95,9 @@ export interface ConflictsGroupProps {
|
|
|
85
95
|
/** Closes the drawer and focuses the field in the form. */
|
|
86
96
|
onEdit?: (field: FieldConflict) => void;
|
|
87
97
|
/** Settles an entry-level conflict; resolves with why it couldn't, when it couldn't. */
|
|
88
|
-
onResolveEntry?:
|
|
98
|
+
onResolveEntry?: ResolveEntryChoice;
|
|
99
|
+
/** An entry-level choice settled in place, its card about to go. */
|
|
100
|
+
onEntryDone?: (done: EntryChoiceDone) => void;
|
|
89
101
|
/** Why this user can't change the entry: the choices are hidden. */
|
|
90
102
|
noEditReason?: string;
|
|
91
103
|
/** Why the choices can't act now: they stay, disabled, saying so. */
|