@gogitcms/editor 0.26.0 → 0.29.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/app/src/App.tsx +163 -1
- package/app/src/forms.ts +77 -0
- package/app/src/history.ts +50 -0
- package/app/src/queries.ts +163 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.forms.test.tsx +157 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.history.test.tsx +385 -0
- package/app/vendor/design-system/src/__tests__/ContentBrowser.historycollab.test.tsx +433 -0
- package/app/vendor/design-system/src/__tests__/FormsBrowser.test.tsx +253 -0
- package/app/vendor/design-system/src/__tests__/ProtectedBranchModal.test.tsx +68 -0
- package/app/vendor/design-system/src/components/ChangeDetail.tsx +94 -11
- package/app/vendor/design-system/src/components/CollabField.tsx +71 -0
- package/app/vendor/design-system/src/components/ContentBrowser.tsx +366 -61
- package/app/vendor/design-system/src/components/DocumentHistory.tsx +408 -0
- package/app/vendor/design-system/src/components/FormsBrowser.tsx +543 -0
- package/app/vendor/design-system/src/components/ProtectedBranchModal.tsx +136 -0
- package/app/vendor/design-system/src/components/primitives.tsx +46 -1
- package/app/vendor/design-system/src/forms.ts +118 -0
- package/app/vendor/design-system/src/history.ts +82 -0
- package/app/vendor/design-system/src/index.ts +18 -1
- package/app/vendor/markdown-editor/src/MarkdownEditor.tsx +23 -0
- package/app/vendor/markdown-editor/src/field.tsx +6 -1
- package/app/vendor/markdown-editor/src/types.ts +13 -0
- package/npm-shrinkwrap.json +155 -168
- package/package.json +6 -6
package/app/src/App.tsx
CHANGED
|
@@ -51,9 +51,12 @@ import { beginAuthorize, beginLogout, beginGitHub, brokeredToken, handleCallback
|
|
|
51
51
|
import { useNavigation, useRoute, StackActions } from "@react-navigation/native";
|
|
52
52
|
import { NavigationRoot, RootStack, EMPTY_SELECTION, browserUrl, type BrowserParams, type CmsSelection } from "./navigation";
|
|
53
53
|
import { isMediaNavKey, MEDIA_NAV_KEY } from "@gogitcms/design-system";
|
|
54
|
-
import {
|
|
54
|
+
import { FORMS_NAV_KEY, isFormsNavKey, parseFormsNavKey } from "@gogitcms/design-system";
|
|
55
|
+
import { LOGIN, WORKSPACES, REPOSITORIES, COLLECTIONS, DOCUMENTS, DOCUMENT_COUNT, DOCUMENT, ME, NOTIFICATIONS, MARK_NOTIFICATIONS_READ, UPDATE_DOCUMENT, CREATE_DOCUMENT, DELETE_DOCUMENTS, RENAME_DOCUMENT, MOVE_DOCUMENTS, BRANCH_CHANGE_SUMMARY, BRANCH_CHANGES, BRANCH_CHANGE, BRANCH_MEDIA_CHANGES, CREATE_BRANCH, APPLY_CHANGES, MERGE_RUN_PROGRESS, RESOLVE_MERGE_CONFLICT, OPEN_CHANGE_REQUEST, CHANGE_REQUEST, CHANGE_REQUEST_FOR_BRANCH, CREATE_CHANGE_REQUEST, BRANCH_IMPORT, BRANCH_IMPORT_PROGRESS, SAVE_DOCUMENT_ON_NEW_BRANCH } from "./queries";
|
|
55
56
|
import type { EntryField, FacetField, DocFilter, SegmentItem, BranchRef, ProjectRef, DocumentChange, FieldChange, FieldChangeKind, MergeProgress, FieldConflict as FieldConflictT } from "@gogitcms/design-system";
|
|
56
57
|
import { useMediaApi } from "./media";
|
|
58
|
+
import { useFormsApi } from "./forms";
|
|
59
|
+
import { useHistoryApi } from "./history";
|
|
57
60
|
|
|
58
61
|
// How many documents to fetch per page of the (server-paginated) entry list.
|
|
59
62
|
const PAGE_SIZE = 50;
|
|
@@ -226,6 +229,26 @@ type Collection = {
|
|
|
226
229
|
filename?: string | null; canCreate: boolean; canUpdate: boolean; canDelete: boolean; fields: FieldDef[];
|
|
227
230
|
};
|
|
228
231
|
type DocumentT = { id: string; path: string; label: string; fields: Record<string, unknown> | null; body?: string | null };
|
|
232
|
+
|
|
233
|
+
// A save a protected branch refused, held open while the author names a branch
|
|
234
|
+
// to put it on. `resolve`/`reject` are the editor's own save promise: finishing
|
|
235
|
+
// it settles the save the editor is still showing as in flight.
|
|
236
|
+
type ProtectedSave = {
|
|
237
|
+
change: { id: string; fields: Record<string, unknown>; body: string | null };
|
|
238
|
+
documentLabel?: string;
|
|
239
|
+
suggestedName: string;
|
|
240
|
+
resolve: () => void;
|
|
241
|
+
reject: (e: Error) => void;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// A branch name to offer for a refused save, derived from the document's file
|
|
245
|
+
// name — "content/blog/hello-world.md" → "edit/hello-world". A suggestion only:
|
|
246
|
+
// the field is editable, and an empty one just means the author types their own.
|
|
247
|
+
const suggestBranchName = (path?: string): string => {
|
|
248
|
+
const base = (path ?? "").split("/").pop() ?? "";
|
|
249
|
+
const slug = base.replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
250
|
+
return slug ? `edit/${slug}` : "";
|
|
251
|
+
};
|
|
229
252
|
// An open desktop tab: a document id + the collection it belongs to.
|
|
230
253
|
// An open desktop column. `plugin` set → the column renders a plugin route
|
|
231
254
|
// instead of a document, and `id` is the synthetic plugin tab id rather than a
|
|
@@ -714,9 +737,21 @@ function CmsView({
|
|
|
714
737
|
// The media data seam handed to the design system. Undefined when the server
|
|
715
738
|
// has no object store configured, which renders media fields read-only.
|
|
716
739
|
const media = useMediaApi(repo.id, branch.id, projectName);
|
|
740
|
+
// Undefined when the branch's config declares no forms, which is what keeps
|
|
741
|
+
// the editor byte-identical for every repository that has none.
|
|
742
|
+
const forms = useFormsApi(repo.id, branch.id, projectName);
|
|
743
|
+
// Document history reads the provider's commit log through the repository's
|
|
744
|
+
// GitHub App installation. Local mode serves a working tree with no
|
|
745
|
+
// installation behind it, so the seam is undefined there and the history
|
|
746
|
+
// button never renders — a feature the user never sees beats one that errors.
|
|
747
|
+
const history = useHistoryApi(repo.id, !config.local);
|
|
717
748
|
const navigation = useNavigation<any>();
|
|
718
749
|
const route = useRoute();
|
|
719
750
|
const params = route.params as BrowserParams;
|
|
751
|
+
// Forms live in the collection segment (a prefixed nav key), and a submission
|
|
752
|
+
// in the item segment — so a submission is deep-linkable with no change to the
|
|
753
|
+
// URL grammar. See navigation.tsx.
|
|
754
|
+
const formsShowing = isFormsNavKey(params.collection);
|
|
720
755
|
// Selection changes keep the current repo/branch fixed and only vary the CMS
|
|
721
756
|
// portion of the route. Callers may pass a partial selection; the rest resets.
|
|
722
757
|
// The surface is deliberately carried forward rather than reset — selecting a
|
|
@@ -1599,6 +1634,27 @@ function CmsView({
|
|
|
1599
1634
|
// rather than one per typing pause, which is what keeps a git-backed history
|
|
1600
1635
|
// readable.
|
|
1601
1636
|
const [updateDocument] = useMutation(UPDATE_DOCUMENT);
|
|
1637
|
+
// A save the branch refused because it is protected, suspended on the prompt.
|
|
1638
|
+
//
|
|
1639
|
+
// The save's own promise is held here rather than rejected, so from the
|
|
1640
|
+
// editor's point of view the save is still in flight while the author names a
|
|
1641
|
+
// branch. That is what keeps the draft machinery honest: a rejection would put
|
|
1642
|
+
// the column into its error state, and the successful save that follows would
|
|
1643
|
+
// be a save of a document the column is no longer showing.
|
|
1644
|
+
const [protectedSave, setProtectedSave] = useState<ProtectedSave | null>(null);
|
|
1645
|
+
const [creatingProtectedBranch, setCreatingProtectedBranch] = useState(false);
|
|
1646
|
+
const [protectedBranchError, setProtectedBranchError] = useState<string | null>(null);
|
|
1647
|
+
// A held-open save has to be settled if the prompt goes away without an answer
|
|
1648
|
+
// — leaving the promise pending would leave the editor reporting "saving" for
|
|
1649
|
+
// a save that nothing is going to finish. A branch switch is the reachable
|
|
1650
|
+
// case: it is a navigation, so the modal disappears with the branch it was
|
|
1651
|
+
// about, and the pending edit no longer belongs to what is on screen.
|
|
1652
|
+
const pendingSaveRef = useRef<ProtectedSave | null>(null);
|
|
1653
|
+
pendingSaveRef.current = protectedSave;
|
|
1654
|
+
useEffect(() => () => {
|
|
1655
|
+
pendingSaveRef.current?.reject(new Error("Save cancelled."));
|
|
1656
|
+
}, [branch.id]);
|
|
1657
|
+
|
|
1602
1658
|
const onSaveEntry = async (change: { id: string; fields: Record<string, unknown>; body: string | null }) => {
|
|
1603
1659
|
// Merge against the tab's loaded full field map (not the active-collection
|
|
1604
1660
|
// list, which is now display-only) so cross-collection saves preserve keys.
|
|
@@ -1609,11 +1665,85 @@ function CmsView({
|
|
|
1609
1665
|
variables: { repositoryId: repo.id, id: change.id, fields: merged, body: change.body },
|
|
1610
1666
|
});
|
|
1611
1667
|
} catch (e: any) {
|
|
1668
|
+
// A protected branch has one remedy and the editor knows it: put the edit
|
|
1669
|
+
// on a branch that takes writes. Rather than reporting a failure the author
|
|
1670
|
+
// then has to plan around, hold the save open on the prompt and finish it
|
|
1671
|
+
// there (or fail it, if they'd rather keep editing here).
|
|
1672
|
+
if (e?.graphQLErrors?.[0]?.extensions?.code === "BRANCH_PROTECTED") {
|
|
1673
|
+
setProtectedBranchError(null);
|
|
1674
|
+
return new Promise<void>((resolve, reject) => {
|
|
1675
|
+
setProtectedSave({
|
|
1676
|
+
change: { ...change, fields: merged },
|
|
1677
|
+
documentLabel: doc?.label,
|
|
1678
|
+
suggestedName: suggestBranchName(doc?.path),
|
|
1679
|
+
resolve,
|
|
1680
|
+
reject,
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1612
1684
|
// Rethrow the server's message so the editor's autosave surfaces it.
|
|
1613
1685
|
throw new Error(e?.graphQLErrors?.[0]?.message || e?.message || "Couldn’t save your changes.");
|
|
1614
1686
|
}
|
|
1615
1687
|
};
|
|
1616
1688
|
|
|
1689
|
+
// Finish a refused save on a new branch: one mutation creates the branch, finds
|
|
1690
|
+
// the same document there and applies the edit.
|
|
1691
|
+
const [saveOnNewBranch] = useMutation(SAVE_DOCUMENT_ON_NEW_BRANCH);
|
|
1692
|
+
const onCreateBranchAndSave = async (name: string) => {
|
|
1693
|
+
if (!protectedSave) return;
|
|
1694
|
+
const pending = protectedSave;
|
|
1695
|
+
setCreatingProtectedBranch(true);
|
|
1696
|
+
setProtectedBranchError(null);
|
|
1697
|
+
let created: { branch: Branch; document: DocumentT } | undefined;
|
|
1698
|
+
try {
|
|
1699
|
+
const res = await saveOnNewBranch({
|
|
1700
|
+
variables: {
|
|
1701
|
+
repositoryId: repo.id,
|
|
1702
|
+
fromBranchId: branch.id,
|
|
1703
|
+
name,
|
|
1704
|
+
documentId: pending.change.id,
|
|
1705
|
+
fields: pending.change.fields,
|
|
1706
|
+
body: pending.change.body,
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1709
|
+
created = res.data?.saveDocumentOnNewBranch;
|
|
1710
|
+
} catch (e: any) {
|
|
1711
|
+
setProtectedBranchError(e?.graphQLErrors?.[0]?.message || e?.message || "Couldn’t create the branch.");
|
|
1712
|
+
setCreatingProtectedBranch(false);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
setCreatingProtectedBranch(false);
|
|
1716
|
+
setProtectedSave(null);
|
|
1717
|
+
// The branch list is cached on the repository; the switcher needs the new one.
|
|
1718
|
+
await apollo.refetchQueries({ include: [REPOSITORIES] });
|
|
1719
|
+
// Resolve BEFORE navigating. The editor clears the document's stored draft in
|
|
1720
|
+
// the microtask that follows this resolve, and the column it does that from
|
|
1721
|
+
// unmounts the moment we navigate — so the deferred navigation below is what
|
|
1722
|
+
// lets the draft be cleared rather than left behind under the old id.
|
|
1723
|
+
pending.resolve();
|
|
1724
|
+
if (!created) return;
|
|
1725
|
+
const col = collections.find((c) => c.name === (openTabs.find((t: TabRef) => t.id === pending.change.id)?.collection ?? activeNavKey)) ?? active;
|
|
1726
|
+
setTimeout(() => {
|
|
1727
|
+
navigation.navigate("Browser", {
|
|
1728
|
+
workspaceId: params.workspaceId, repositoryId: repo.id, branchId: created!.branch.id,
|
|
1729
|
+
defaultBranchId, projectName, soleProjectName,
|
|
1730
|
+
...EMPTY_SELECTION,
|
|
1731
|
+
collection: col?.name ?? activeNavKey,
|
|
1732
|
+
itemPath: col ? toRelItemPath(col.path, created!.document.path) : "",
|
|
1733
|
+
});
|
|
1734
|
+
}, 0);
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1737
|
+
// Dismissed: the save genuinely failed, and saying so is what leaves the column
|
|
1738
|
+
// in its error state with the draft intact — the author keeps their work and
|
|
1739
|
+
// can try again (or switch branches themselves).
|
|
1740
|
+
const onCancelProtectedSave = () => {
|
|
1741
|
+
protectedSave?.reject(new Error(`${branch.name} is protected — save this to a new branch instead.`));
|
|
1742
|
+
setProtectedSave(null);
|
|
1743
|
+
setProtectedBranchError(null);
|
|
1744
|
+
setCreatingProtectedBranch(false);
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1617
1747
|
// Create: split the form values into a body-source field (if any) + frontmatter
|
|
1618
1748
|
// fields, persist, then route to the new document's edit URL.
|
|
1619
1749
|
const [createDocument] = useMutation(CREATE_DOCUMENT);
|
|
@@ -1798,6 +1928,20 @@ function CmsView({
|
|
|
1798
1928
|
icon: "settings" as const,
|
|
1799
1929
|
})),
|
|
1800
1930
|
},
|
|
1931
|
+
// Forms, when the branch declares any. One row, like Media: the content
|
|
1932
|
+
// area is what lists the individual forms, because a form is a folder of
|
|
1933
|
+
// submissions rather than a collection of documents.
|
|
1934
|
+
{
|
|
1935
|
+
title: "",
|
|
1936
|
+
items: forms
|
|
1937
|
+
? [{
|
|
1938
|
+
key: FORMS_NAV_KEY,
|
|
1939
|
+
label: "Forms",
|
|
1940
|
+
icon: "listTree" as const,
|
|
1941
|
+
count: forms.forms.reduce((n, f) => n + f.submissionCount, 0),
|
|
1942
|
+
}]
|
|
1943
|
+
: [],
|
|
1944
|
+
},
|
|
1801
1945
|
// Plugin-contributed links, grouped by their declared section title
|
|
1802
1946
|
// (default "Plugins"), keyed so onSelectNav can route them.
|
|
1803
1947
|
...pluginRegistry.sidebar().reduce<CmsNavSection[]>((acc, link) => {
|
|
@@ -2196,6 +2340,14 @@ function CmsView({
|
|
|
2196
2340
|
}
|
|
2197
2341
|
renderField={renderField}
|
|
2198
2342
|
media={media}
|
|
2343
|
+
forms={forms}
|
|
2344
|
+
history={history}
|
|
2345
|
+
// The submission id rides in the item segment, exactly as a document path
|
|
2346
|
+
// does, so selection survives a reload and is shareable.
|
|
2347
|
+
selectedSubmissionId={formsShowing ? params.itemPath || null : null}
|
|
2348
|
+
onSelectSubmission={(id) =>
|
|
2349
|
+
go({ configure: false, collection: params.collection, itemPath: id ?? "" })
|
|
2350
|
+
}
|
|
2199
2351
|
// Nothing on the changes surface is editable; nor is a frozen branch (one
|
|
2200
2352
|
// handed to a developer in an open change request).
|
|
2201
2353
|
onSaveEntry={changesMode || branchFrozen ? undefined : onSaveEntry}
|
|
@@ -2214,6 +2366,16 @@ function CmsView({
|
|
|
2214
2366
|
readOnlyNotice={branchFrozen ? "This branch is in an open change request and is read-only until it merges or is rejected." : undefined}
|
|
2215
2367
|
readOnlyNoticeActionLabel="Learn more"
|
|
2216
2368
|
onReadOnlyNoticeAction={branchFrozen ? openChangeRequestModal : undefined}
|
|
2369
|
+
// The protected-branch prompt. Opened by a refused save (not by the branch
|
|
2370
|
+
// being protected): a protected branch is perfectly readable and editable,
|
|
2371
|
+
// and interrupting before the author has decided to save would be noise.
|
|
2372
|
+
protectedBranch={protectedSave ? branch.name : undefined}
|
|
2373
|
+
protectedBranchDocument={protectedSave?.documentLabel}
|
|
2374
|
+
protectedBranchSuggestedName={protectedSave?.suggestedName}
|
|
2375
|
+
creatingProtectedBranch={creatingProtectedBranch}
|
|
2376
|
+
protectedBranchError={protectedBranchError}
|
|
2377
|
+
onCreateBranchAndSave={onCreateBranchAndSave}
|
|
2378
|
+
onCancelProtectedSave={onCancelProtectedSave}
|
|
2217
2379
|
onDeleteEntry={onDeleteEntry}
|
|
2218
2380
|
selectable={!changesMode && !!active?.canDelete && !active?.singleton}
|
|
2219
2381
|
onBulkDelete={onBulkDelete}
|
package/app/src/forms.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useApolloClient, useQuery } from "@apollo/client";
|
|
3
|
+
import type { FormInfo, FormsApi, SubmissionInfo } from "@gogitcms/design-system";
|
|
4
|
+
import {
|
|
5
|
+
DELETE_FORM_SUBMISSIONS,
|
|
6
|
+
FORMS,
|
|
7
|
+
FORM_SUBMISSIONS,
|
|
8
|
+
FORM_SUBMISSION_COUNT,
|
|
9
|
+
} from "./queries";
|
|
10
|
+
|
|
11
|
+
// The Apollo implementation of the design system's forms seam. The DS owns the
|
|
12
|
+
// browsing UI and knows nothing about transport; this file is the only place
|
|
13
|
+
// form queries live on the web client — the same split media.ts draws.
|
|
14
|
+
|
|
15
|
+
// Submissions are read uncached. Unlike a content model's documents, they arrive
|
|
16
|
+
// from outside the editor at any moment, so a cached page would show an inbox
|
|
17
|
+
// that quietly stopped updating — the one thing an inbox must not do.
|
|
18
|
+
const NO_CACHE = { fetchPolicy: "no-cache" as const };
|
|
19
|
+
|
|
20
|
+
export function useFormsApi(
|
|
21
|
+
repositoryId?: string,
|
|
22
|
+
branchId?: string,
|
|
23
|
+
project?: string,
|
|
24
|
+
): FormsApi | undefined {
|
|
25
|
+
const client = useApolloClient();
|
|
26
|
+
const skip = !repositoryId || !branchId;
|
|
27
|
+
|
|
28
|
+
// The form list comes from the branch's config plus a count per form. It is
|
|
29
|
+
// cached: the definitions change only on re-import, and the counts are a
|
|
30
|
+
// summary rather than the inbox itself.
|
|
31
|
+
const { data } = useQuery(FORMS, {
|
|
32
|
+
variables: { repositoryId, branchId, project: project ?? null },
|
|
33
|
+
skip,
|
|
34
|
+
});
|
|
35
|
+
const forms: FormInfo[] = data?.forms ?? [];
|
|
36
|
+
|
|
37
|
+
return useMemo(() => {
|
|
38
|
+
if (skip) return undefined;
|
|
39
|
+
// A branch whose config declares no forms has no Forms surface at all, which
|
|
40
|
+
// is what keeps the editor identical for every repository that has none.
|
|
41
|
+
if (forms.length === 0) return undefined;
|
|
42
|
+
|
|
43
|
+
const scope = { repositoryId, branchId, project: project ?? null };
|
|
44
|
+
return {
|
|
45
|
+
forms,
|
|
46
|
+
list: async ({ form, status, search, limit, offset }) => {
|
|
47
|
+
const res = await client.query({
|
|
48
|
+
query: FORM_SUBMISSIONS,
|
|
49
|
+
variables: {
|
|
50
|
+
...scope,
|
|
51
|
+
form,
|
|
52
|
+
status: status ?? null,
|
|
53
|
+
search: search ?? null,
|
|
54
|
+
limit: limit ?? null,
|
|
55
|
+
offset: offset ?? null,
|
|
56
|
+
},
|
|
57
|
+
...NO_CACHE,
|
|
58
|
+
});
|
|
59
|
+
return (res.data?.formSubmissions ?? []) as SubmissionInfo[];
|
|
60
|
+
},
|
|
61
|
+
count: async ({ form, status, search }) => {
|
|
62
|
+
const res = await client.query({
|
|
63
|
+
query: FORM_SUBMISSION_COUNT,
|
|
64
|
+
variables: { ...scope, form, status: status ?? null, search: search ?? null },
|
|
65
|
+
...NO_CACHE,
|
|
66
|
+
});
|
|
67
|
+
return (res.data?.formSubmissionCount ?? 0) as number;
|
|
68
|
+
},
|
|
69
|
+
remove: async (ids) => {
|
|
70
|
+
await client.mutate({
|
|
71
|
+
mutation: DELETE_FORM_SUBMISSIONS,
|
|
72
|
+
variables: { repositoryId, ids },
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}, [client, skip, forms, repositoryId, branchId, project]);
|
|
77
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useApolloClient } from "@apollo/client";
|
|
3
|
+
import type { DocumentChange, DocumentVersion, HistoryApi } from "@gogitcms/design-system";
|
|
4
|
+
import { DOCUMENT_VERSION, DOCUMENT_VERSIONS } from "./queries";
|
|
5
|
+
|
|
6
|
+
// The Apollo implementation of the design system's document-history seam. The
|
|
7
|
+
// DS owns the version browser and knows nothing about transport; this file is
|
|
8
|
+
// the only place history queries live on the web client — the same split
|
|
9
|
+
// forms.ts and media.ts draw.
|
|
10
|
+
|
|
11
|
+
// History reads are cached normally. A commit is immutable, so a version fetched
|
|
12
|
+
// once can be re-shown for free — which is the whole point of clicking back up a
|
|
13
|
+
// list you have already read. What can change is the newest end of the list, and
|
|
14
|
+
// that is re-fetched whenever the pane is opened, because the DS mounts fresh.
|
|
15
|
+
const CACHED = { fetchPolicy: "cache-first" as const };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* useHistoryApi builds the seam, or returns undefined when history is
|
|
19
|
+
* unavailable — which hides the affordance entirely rather than offering a
|
|
20
|
+
* button that errors.
|
|
21
|
+
*
|
|
22
|
+
* `available` is the deployment's answer to "is there a provider behind this
|
|
23
|
+
* editor". Local mode edits a working tree that may not even be a git
|
|
24
|
+
* repository, and has no GitHub App installation to read a commit log through.
|
|
25
|
+
*/
|
|
26
|
+
export function useHistoryApi(repositoryId: string | undefined, available: boolean): HistoryApi | undefined {
|
|
27
|
+
const client = useApolloClient();
|
|
28
|
+
|
|
29
|
+
return useMemo(() => {
|
|
30
|
+
if (!repositoryId || !available) return undefined;
|
|
31
|
+
return {
|
|
32
|
+
list: async ({ documentId, limit, offset }) => {
|
|
33
|
+
const res = await client.query({
|
|
34
|
+
query: DOCUMENT_VERSIONS,
|
|
35
|
+
variables: { repositoryId, id: documentId, limit, offset },
|
|
36
|
+
...CACHED,
|
|
37
|
+
});
|
|
38
|
+
return (res.data?.documentVersions ?? []) as DocumentVersion[];
|
|
39
|
+
},
|
|
40
|
+
get: async ({ documentId, sha }) => {
|
|
41
|
+
const res = await client.query({
|
|
42
|
+
query: DOCUMENT_VERSION,
|
|
43
|
+
variables: { repositoryId, id: documentId, sha },
|
|
44
|
+
...CACHED,
|
|
45
|
+
});
|
|
46
|
+
return (res.data?.documentVersion ?? null) as DocumentChange | null;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}, [client, repositoryId, available]);
|
|
50
|
+
}
|
package/app/src/queries.ts
CHANGED
|
@@ -111,6 +111,51 @@ export const BRANCH_CHANGE = gql`
|
|
|
111
111
|
}
|
|
112
112
|
`;
|
|
113
113
|
|
|
114
|
+
// ---- document history -----------------------------------------------------
|
|
115
|
+
// One page of the commits that touched a document, newest first. Each page is a
|
|
116
|
+
// provider request, so the page size stays modest and the list pages as it
|
|
117
|
+
// scrolls rather than asking for everything at once.
|
|
118
|
+
export const DOCUMENT_VERSIONS = gql`
|
|
119
|
+
query DocumentVersions($repositoryId: ID!, $id: ID!, $limit: Int, $offset: Int) {
|
|
120
|
+
documentVersions(repositoryId: $repositoryId, id: $id, limit: $limit, offset: $offset) {
|
|
121
|
+
sha
|
|
122
|
+
shortSha
|
|
123
|
+
message
|
|
124
|
+
authorName
|
|
125
|
+
authorEmail
|
|
126
|
+
authoredAt
|
|
127
|
+
url
|
|
128
|
+
filesChanged
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
// One version: the document as it was at that commit, diffed against what it is
|
|
134
|
+
// now. The selection shape is BranchChange's, deliberately — the history pane
|
|
135
|
+
// renders through the same ChangeDetail component the changes surface uses.
|
|
136
|
+
export const DOCUMENT_VERSION = gql`
|
|
137
|
+
query DocumentVersion($repositoryId: ID!, $id: ID!, $sha: String!) {
|
|
138
|
+
documentVersion(repositoryId: $repositoryId, id: $id, sha: $sha) {
|
|
139
|
+
sha
|
|
140
|
+
path
|
|
141
|
+
collection
|
|
142
|
+
label
|
|
143
|
+
status
|
|
144
|
+
added
|
|
145
|
+
removed
|
|
146
|
+
bodyBefore
|
|
147
|
+
bodyAfter
|
|
148
|
+
fields {
|
|
149
|
+
name
|
|
150
|
+
label
|
|
151
|
+
kind
|
|
152
|
+
before
|
|
153
|
+
after
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
|
|
114
159
|
// The merge run's fields, shared by the mutation, query and subscription so the
|
|
115
160
|
// Apollo cache normalizes them onto one MergeRun by id.
|
|
116
161
|
const MERGE_RUN_FIELDS = gql`
|
|
@@ -239,6 +284,43 @@ export const CREATE_BRANCH = gql`
|
|
|
239
284
|
createBranch(repositoryId: $repositoryId, fromBranchId: $fromBranchId, name: $name) {
|
|
240
285
|
id
|
|
241
286
|
name
|
|
287
|
+
protected
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
// The recovery from a BRANCH_PROTECTED save: one call that creates the branch,
|
|
293
|
+
// finds the same document on it (a document's cross-branch identity is its path,
|
|
294
|
+
// so it is a different row there) and applies the edit.
|
|
295
|
+
export const SAVE_DOCUMENT_ON_NEW_BRANCH = gql`
|
|
296
|
+
mutation SaveDocumentOnNewBranch(
|
|
297
|
+
$repositoryId: ID!
|
|
298
|
+
$fromBranchId: ID!
|
|
299
|
+
$name: String!
|
|
300
|
+
$documentId: ID!
|
|
301
|
+
$fields: JSON!
|
|
302
|
+
$body: String
|
|
303
|
+
) {
|
|
304
|
+
saveDocumentOnNewBranch(
|
|
305
|
+
repositoryId: $repositoryId
|
|
306
|
+
fromBranchId: $fromBranchId
|
|
307
|
+
name: $name
|
|
308
|
+
documentId: $documentId
|
|
309
|
+
fields: $fields
|
|
310
|
+
body: $body
|
|
311
|
+
) {
|
|
312
|
+
branch {
|
|
313
|
+
id
|
|
314
|
+
name
|
|
315
|
+
protected
|
|
316
|
+
}
|
|
317
|
+
document {
|
|
318
|
+
id
|
|
319
|
+
path
|
|
320
|
+
label
|
|
321
|
+
fields
|
|
322
|
+
body
|
|
323
|
+
}
|
|
242
324
|
}
|
|
243
325
|
}
|
|
244
326
|
`;
|
|
@@ -589,3 +671,84 @@ export const RENAME_DOCUMENT = gql`
|
|
|
589
671
|
}
|
|
590
672
|
}
|
|
591
673
|
`;
|
|
674
|
+
|
|
675
|
+
// ── Forms ───────────────────────────────────────────────────────────────────
|
|
676
|
+
|
|
677
|
+
export const FORMS = gql`
|
|
678
|
+
query Forms($repositoryId: ID!, $branchId: ID!, $project: String) {
|
|
679
|
+
forms(repositoryId: $repositoryId, branchId: $branchId, project: $project) {
|
|
680
|
+
name
|
|
681
|
+
label
|
|
682
|
+
description
|
|
683
|
+
fieldCount
|
|
684
|
+
submissionCount
|
|
685
|
+
lastSubmissionAt
|
|
686
|
+
versioned
|
|
687
|
+
storePath
|
|
688
|
+
canDelete
|
|
689
|
+
fields {
|
|
690
|
+
name
|
|
691
|
+
label
|
|
692
|
+
type
|
|
693
|
+
component
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
`;
|
|
698
|
+
|
|
699
|
+
export const FORM_SUBMISSIONS = gql`
|
|
700
|
+
query FormSubmissions(
|
|
701
|
+
$repositoryId: ID!
|
|
702
|
+
$branchId: ID!
|
|
703
|
+
$project: String
|
|
704
|
+
$form: String!
|
|
705
|
+
$status: String
|
|
706
|
+
$search: String
|
|
707
|
+
$limit: Int
|
|
708
|
+
$offset: Int
|
|
709
|
+
) {
|
|
710
|
+
formSubmissions(
|
|
711
|
+
repositoryId: $repositoryId
|
|
712
|
+
branchId: $branchId
|
|
713
|
+
project: $project
|
|
714
|
+
form: $form
|
|
715
|
+
status: $status
|
|
716
|
+
search: $search
|
|
717
|
+
limit: $limit
|
|
718
|
+
offset: $offset
|
|
719
|
+
) {
|
|
720
|
+
id
|
|
721
|
+
form
|
|
722
|
+
fields
|
|
723
|
+
submittedAt
|
|
724
|
+
status
|
|
725
|
+
path
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
`;
|
|
729
|
+
|
|
730
|
+
export const FORM_SUBMISSION_COUNT = gql`
|
|
731
|
+
query FormSubmissionCount(
|
|
732
|
+
$repositoryId: ID!
|
|
733
|
+
$branchId: ID!
|
|
734
|
+
$project: String
|
|
735
|
+
$form: String!
|
|
736
|
+
$status: String
|
|
737
|
+
$search: String
|
|
738
|
+
) {
|
|
739
|
+
formSubmissionCount(
|
|
740
|
+
repositoryId: $repositoryId
|
|
741
|
+
branchId: $branchId
|
|
742
|
+
project: $project
|
|
743
|
+
form: $form
|
|
744
|
+
status: $status
|
|
745
|
+
search: $search
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
`;
|
|
749
|
+
|
|
750
|
+
export const DELETE_FORM_SUBMISSIONS = gql`
|
|
751
|
+
mutation DeleteFormSubmissions($repositoryId: ID!, $ids: [ID!]!) {
|
|
752
|
+
deleteFormSubmissions(repositoryId: $repositoryId, ids: $ids)
|
|
753
|
+
}
|
|
754
|
+
`;
|