diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34100282..43ac60c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ Lines marked **Upgrade step** need you to act after updating.
- The form list loads faster when a team has many forms. It now needs the same few database queries for one form or a hundred. Before, every form added 3 queries. (#256)
- On screens narrower than 1280 px, a form's editor bar now sits in two rows under the app header: the form with Dashboard and Preview, then Editor, Settings, Integrations and Submissions as tabs. Nothing in it scrolls sideways any more, and the header keeps the logo and your team name. On every screen, keyboard focus in the editor bar and the account menu now shows a blue ring. (#257)
- A form's pages have one top bar instead of two: the form's name with its menu, a Draft or Live label, the pages, Preview, Publish and your account. Publish first checks for questions without text, rules that point to a deleted question and a missing privacy link. Once the form is live, Share has its link, Open form and Unpublish. On a phone the pages sit in a tab bar at the bottom. (#265)
+- The form editor shows your whole form as one column. Each question shows its number, answer type, text, and if it is required, has rules or is hidden. The question you work on opens right there, also on a phone, where you couldn't edit questions before. Hiding a question, random order and custom answers are now under the question's Settings. (#268)
- Deleting a question, a group, an answer choice, a consent policy or a logic rule in the form editor now asks first in the app's own dialog. It names the question, how many answers will be deleted with it, and the questions whose rules will stop working. A choice nobody picked, or a policy nobody agreed to, is still deleted at once. (#269)
### Fixed
diff --git a/resources/js/Pages/Forms/Edit.test.ts b/resources/js/Pages/Forms/Edit.test.ts
index 698a0c87..fba82d18 100644
--- a/resources/js/Pages/Forms/Edit.test.ts
+++ b/resources/js/Pages/Forms/Edit.test.ts
@@ -11,9 +11,8 @@ const blocks = [
const stubs = {
AppLayout: { props: ["title"], template: "
+ Drag questions here, or + . +
+Which days can you help?
", + is_required: true, + is_disabled: true, + title: "days", + logics: [{ id: 1 }], + interactions: [ + { type: "button", is_editable: true }, + { type: "button", is_editable: true }, + { type: "button", is_editable: false }, + ], +} as unknown as FormBlockModel; + +const mountQuestion = (block = question) => + mount(DocumentQuestion, { + props: { block, number: 3 }, + global: { stubs: { QuestionCard: { template: "+ {{ countLine }} +
+ +Your name?
", + interactions: [], +} as unknown as FormBlockModel; + +let wrapper: VueWrapper | undefined; + +describe("an open question", () => { + beforeEach(() => { + useWorkbench().$reset(); + useForm().$reset(); + }); + + afterEach(() => wrapper?.unmount()); + + it("makes its answer field without opening Settings", async () => { + const workbench = useWorkbench(); + useForm().mapping = { "input-short": "input" } as Record< + FormBlockType, + FormBlockInteractionType + >; + workbench.block = { ...block, interactions: [] }; + vi.mocked(workbench.createInteraction).mockResolvedValue({ + id: 9, + type: "input", + label: "", + } as unknown as FormBlockInteractionModel); + + wrapper = mount(QuestionCard, { + props: { block: workbench.block, number: 4 }, + global: { stubs: { Editor: true, BlockMenu: true } }, + }); + await flushPromises(); + + expect(workbench.createInteraction).toHaveBeenCalledWith("input"); + }); + + it("shows Required only when the question takes an answer", () => { + const stubs = { Editor: true, BlockMenu: true, QuestionSettings: true }; + wrapper = mount(QuestionCard, { + props: { block: { ...block, type: "none" }, number: 1 }, + global: { stubs }, + }); + expect(wrapper.text()).not.toContain("Required"); + + wrapper.unmount(); + wrapper = mount(QuestionCard, { + props: { block, number: 4 }, + global: { stubs }, + }); + expect(wrapper.text()).toContain("Required"); + }); + + it("points the Settings button at its panel", () => { + wrapper = mount(QuestionCard, { + props: { block, number: 4 }, + global: { stubs: { Editor: true, BlockMenu: true } }, + }); + const button = wrapper + .findAll("button") + .find((b) => b.text() === "Settings")!; + + const panel = wrapper.find(`#${button.attributes("aria-controls")}`); + expect(panel.exists()).toBe(true); + expect(panel.attributes("role")).toBe("group"); + }); +}); diff --git a/resources/js/components/Factory/Document/QuestionCard.vue b/resources/js/components/Factory/Document/QuestionCard.vue new file mode 100644 index 00000000..f48cbae1 --- /dev/null +++ b/resources/js/components/Factory/Document/QuestionCard.vue @@ -0,0 +1,127 @@ + ++ This name is used when submitting your form through webhooks and in the + export. +
++ Hidden questions don't show in the form. +
+How was it?
"), + ).toBe("Hi & welcome How was it?"); + }); + + it("is empty for no text or only an image", () => { + expect(plainText(null)).toBe(""); + expect(plainText('
')).toBe("");
+ });
+});
+
+describe("question numbers", () => {
+ it("counts on through groups and skips the group itself", () => {
+ const numbers = questionNumbers([
+ block("a"),
+ block("g", "group"),
+ block("c"),
+ block("b", "radio", "g"),
+ block("b2", "date", "g"),
+ ]);
+
+ expect(numbers).toEqual({ a: 1, b: 2, b2: 3, c: 4 });
+ });
+
+ it("gives an empty group nothing", () => {
+ expect(questionNumbers([block("g", "group"), block("a")])).toEqual({
+ a: 1,
+ });
+ });
+});
diff --git a/resources/js/components/Factory/Document/questionSummary.ts b/resources/js/components/Factory/Document/questionSummary.ts
new file mode 100644
index 00000000..a34648a0
--- /dev/null
+++ b/resources/js/components/Factory/Document/questionSummary.ts
@@ -0,0 +1,32 @@
+// the text a closed question shows: no HTML, one space where a paragraph ended
+export function plainText(html: string | null): string {
+ if (!html) {
+ return "";
+ }
+
+ const spaced = html.replace(/<\/(p|h[1-6]|li|blockquote)>|Hi
", bare: true }, + global: { directives: { tooltip: {} } }, + }); + await nextTick(); + + expect(wrapper.find("[role=group]").classes()).not.toContain("border"); + const editor = wrapper + .findComponent(EditorContent) + .props("editor") as TiptapEditor; + expect(editor.view.dom.classList).not.toContain("border"); + expect(wrapper.text()).not.toContain("chars"); + }); +}); diff --git a/resources/js/components/Factory/Shared/Editor.vue b/resources/js/components/Factory/Shared/Editor.vue index 8264e918..180df07a 100644 --- a/resources/js/components/Factory/Shared/Editor.vue +++ b/resources/js/components/Factory/Shared/Editor.vue @@ -1,7 +1,10 @@Hello