@gogitcms/design-system 0.16.0-next.0 → 0.16.0-next.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gogitcms/design-system",
3
- "version": "0.16.0-next.0",
3
+ "version": "0.16.0-next.1",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "// exports": "The root entry is the react-native source the SPAs, desktop app and mobile app consume. ./web is the plain-DOM build for server-rendered surfaces (the Astro docs site) that don't run react-native-web, and ./css ships the tokens as custom properties. The trailing wildcard keeps deep paths resolvable — plugin bundling and the Vite aliases reach into src/ directly.",
@@ -0,0 +1,111 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { ThemeProvider } from "../ThemeProvider";
4
+ import { ContentBrowser, type CmsEntry, type CmsNavSection } from "../components/ContentBrowser";
5
+ import { FORMS_NAV_KEY, formsNavKey, type FormInfo, type FormsApi } from "../forms";
6
+
7
+ // Force the desktop layout (jsdom reports width 0 → mobile otherwise).
8
+ jest.mock("../ThemeProvider", () => {
9
+ const actual = jest.requireActual("../ThemeProvider");
10
+ return { ...actual, useResponsive: () => ({ width: 1300, height: 900, isDesktop: true, isMobile: false }) };
11
+ });
12
+
13
+ const wrap = (ui: React.ReactElement) => <ThemeProvider>{ui}</ThemeProvider>;
14
+
15
+ const contact: FormInfo = {
16
+ name: "contact",
17
+ label: "Contact us",
18
+ fields: [{ name: "email", type: "string" }],
19
+ fieldCount: 1,
20
+ submissionCount: 2,
21
+ versioned: true,
22
+ canDelete: true,
23
+ };
24
+
25
+ const newsletter: FormInfo = {
26
+ name: "newsletter",
27
+ label: "Newsletter",
28
+ fields: [{ name: "email", type: "string" }],
29
+ fieldCount: 1,
30
+ submissionCount: 308,
31
+ versioned: false,
32
+ canDelete: false,
33
+ };
34
+
35
+ function makeApi(over: Partial<FormsApi> = {}): FormsApi {
36
+ return {
37
+ forms: [contact, newsletter],
38
+ list: jest.fn(async () => []),
39
+ count: jest.fn(async () => 0),
40
+ ...over,
41
+ };
42
+ }
43
+
44
+ const sections: CmsNavSection[] = [
45
+ { title: "Content", items: [{ key: "posts", label: "Posts", icon: "newspaper" }] },
46
+ ];
47
+
48
+ const entries: CmsEntry[] = [
49
+ { id: "a", path: "content/posts/a.md", title: "Alpha", body: "" },
50
+ ];
51
+
52
+ function renderForms(activeNavKey: string, onSelectNav = jest.fn()) {
53
+ render(
54
+ wrap(
55
+ <ContentBrowser
56
+ workspace={{ name: "acme/site", initials: "AC", branch: "main", changed: 0 }}
57
+ sections={sections}
58
+ activeNavKey={activeNavKey}
59
+ onSelectNav={onSelectNav}
60
+ entries={entries}
61
+ userInitials="ED"
62
+ forms={makeApi()}
63
+ />,
64
+ ),
65
+ );
66
+ return onSelectNav;
67
+ }
68
+
69
+ // The forms list is a content column, like the documents of a collection or the
70
+ // sets under Media — not the whole area after the sidebar. jsdom computes no
71
+ // layout, so this asserts the mechanism: a fixed width rather than a flex.
72
+ test("the forms list sits in a content column, not the full content area", () => {
73
+ renderForms(FORMS_NAV_KEY);
74
+
75
+ const pane = screen.getByTestId("pane-forms");
76
+ expect(pane).toHaveStyle({ width: "340px" });
77
+ // A flexing pane is the bug: it would fill everything after the sidebar.
78
+ expect(pane).not.toHaveStyle({ flexGrow: 1 });
79
+
80
+ // Headed like every other content pane, and with the seam that resizes it.
81
+ expect(screen.getByText("Forms")).toBeInTheDocument();
82
+ expect(screen.getByTestId("resize-forms")).toBeInTheDocument();
83
+
84
+ // And the area beside it says what to do, the way Media and Changes do.
85
+ expect(screen.getByTestId("forms-empty")).toBeInTheDocument();
86
+ });
87
+
88
+ test("the column lists the forms and opens one through the nav key", () => {
89
+ const onSelectNav = renderForms(FORMS_NAV_KEY);
90
+
91
+ expect(screen.getByText("Contact us")).toBeInTheDocument();
92
+ expect(screen.getByText("Newsletter")).toBeInTheDocument();
93
+
94
+ fireEvent.click(screen.getByTestId("form-row-contact"));
95
+ expect(onSelectNav).toHaveBeenCalledWith(formsNavKey("contact"));
96
+ });
97
+
98
+ // The other half of the rule: submissions are two levels deeper than a content
99
+ // column can express, so opening a form does hand the whole area to
100
+ // FormsBrowser. Pinned so the fix above is not later applied to both states.
101
+ test("an open form takes the whole content area", async () => {
102
+ renderForms(formsNavKey("contact"));
103
+ // Let the submissions load settle before asserting, so the empty inbox — not
104
+ // a pending fetch — is what is on screen.
105
+ await screen.findByText("No submissions yet.");
106
+
107
+ const pane = screen.getByTestId("pane-forms");
108
+ expect(pane).not.toHaveStyle({ width: "340px" });
109
+ expect(screen.queryByTestId("forms-empty")).not.toBeInTheDocument();
110
+ expect(screen.queryByTestId("resize-forms")).not.toBeInTheDocument();
111
+ });
@@ -99,15 +99,20 @@ describe("the forms nav-key space", () => {
99
99
  // ── the forms list ──────────────────────────────────────────────────────────
100
100
 
101
101
  describe("the forms list", () => {
102
- it("shows each form's name, description, field count and submission count", () => {
102
+ it("shows each form's name, description and submission count", () => {
103
103
  renderBrowser();
104
104
  expect(screen.getByText("Contact us")).toBeTruthy();
105
105
  expect(screen.getByText("General enquiries.")).toBeTruthy();
106
- expect(screen.getByText("3 fields")).toBeTruthy();
107
106
  expect(screen.getByText("2")).toBeTruthy();
108
107
  expect(screen.getByText("308")).toBeTruthy();
109
108
  });
110
109
 
110
+ it("does not carry the field count, which describes the form and not its inbox", () => {
111
+ renderBrowser();
112
+ expect(screen.queryByText("3 fields")).toBeNull();
113
+ expect(screen.queryByText(/\bfields?\b/)).toBeNull();
114
+ });
115
+
111
116
  it("opens a form when its row is pressed", () => {
112
117
  const { onSelectForm } = renderBrowser();
113
118
  fireEvent.click(screen.getByTestId("form-row-contact"));
@@ -4143,11 +4143,41 @@ function DesktopBrowser(props: ContentBrowserProps) {
4143
4143
  // lists the media sets exactly where a collection's documents would be, and the
4144
4144
  // details pane holds the browser for whichever set is selected. Selecting a set
4145
4145
  // goes through onSelectNav, so it lands in the URL like any other selection.
4146
- // Forms take the whole content area rather than the three-pane model: the
4147
- // surface is already two levels deep (forms submissions one submission),
4148
- // and threading that through panes designed for collection document editor
4149
- // would mean a pane whose meaning changes with the level.
4146
+ // Forms enter through the same content column every other surface uses: the
4147
+ // list of forms sits exactly where a collection's documents would, at the
4148
+ // list width, with the empty state beside it. A list of half a dozen rows
4149
+ // stretched across everything after the sidebar reads as a different kind of
4150
+ // screen than Edit, Changes and Media, when it is the same kind of screen.
4151
+ //
4152
+ // Opening a form is where forms stop fitting the three-pane model, and so it
4153
+ // is where they leave it: that surface is two more levels deep (submissions →
4154
+ // one submission), and threading those through panes meant for collection →
4155
+ // document → editor would give a pane whose meaning changes with the level.
4156
+ // FormsBrowser owns its own split from there.
4150
4157
  if (formsShowing && props.forms) {
4158
+ if (!activeForm) {
4159
+ return (
4160
+ <AppShell testID="desktop-shell" topBar={topBar} banner={readOnlyBanner}>
4161
+ {navPane}
4162
+ <ResizeHandle width={navW} min={180} max={420} onChange={setNavW} testID="resize-nav" />
4163
+
4164
+ <Pane width={listW} testID="pane-forms" scroll={false} header={<PaneTitle title="Forms" />}>
4165
+ <FormsBrowser
4166
+ api={props.forms}
4167
+ form={null}
4168
+ onSelectForm={(name) => onSelectNav(name ? formsNavKey(name) : FORMS_NAV_KEY)}
4169
+ variant="desktop"
4170
+ />
4171
+ </Pane>
4172
+ <ResizeHandle width={listW} min={260} max={640} onChange={setListW} testID="resize-forms" />
4173
+
4174
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: t.space(6) }}>
4175
+ <Text variant="body" color="tertiary" testID="forms-empty">Select a form</Text>
4176
+ </View>
4177
+ {applyModal}
4178
+ </AppShell>
4179
+ );
4180
+ }
4151
4181
  return (
4152
4182
  <AppShell testID="desktop-shell" topBar={topBar} banner={readOnlyBanner}>
4153
4183
  {navPane}
@@ -130,11 +130,9 @@ function FormRow({ form, onPress }: { form: FormInfo; onPress: () => void }) {
130
130
  </Text>
131
131
  ) : null}
132
132
  </View>
133
- {/* The two numbers the request asks for: how much has come in, and how
134
- much the form asks for. */}
135
- <Text variant="monoSm" color="tertiary">
136
- {form.fieldCount} {form.fieldCount === 1 ? "field" : "fields"}
137
- </Text>
133
+ {/* How much has come in. The field count is deliberately not here: it
134
+ describes the form's shape rather than its inbox, and in a column this
135
+ narrow it competed with the number a reader actually scans for. */}
138
136
  <Text variant="monoSm" color="secondary" style={{ minWidth: 48, textAlign: "right" }}>
139
137
  {form.submissionCount}
140
138
  </Text>