diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a0f3c57..5e96c3c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ Lines marked **Upgrade step** need you to act after updating.
- 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)
- A logic rule with a missing field now tells you in plain words what to fill in, like "Pick a question for condition 1." (#278)
+- 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/components/Factory/Main/Interactions/ClickInteraction.vue b/resources/js/components/Factory/Main/Interactions/ClickInteraction.vue
index ba678267..13bc9d20 100644
--- a/resources/js/components/Factory/Main/Interactions/ClickInteraction.vue
+++ b/resources/js/components/Factory/Main/Interactions/ClickInteraction.vue
@@ -37,7 +37,13 @@
-
+
+
@@ -45,6 +51,8 @@
import { D9Input } from "@deck9/ui";
import InteractionHoverActions from "@/components/Factory/Main/InteractionHoverActions.vue";
import IndexItem from "@/components/Factory/Shared/IndexItem.vue";
+import DeleteChoiceDialog from "@/components/Factory/Shared/DeleteChoiceDialog.vue";
+import { useDeleteChoice } from "@/components/Factory/Shared/useDeleteBlock";
import { useWorkbench } from "@/stores";
import { Ref, ref, watch } from "vue";
@@ -97,8 +105,7 @@ const keyboardCommands = async (event: KeyboardEvent) => {
case "Backspace":
if (event.metaKey) {
- emit("onDelete", props.index);
- await workbench.deleteInteraction(props.item);
+ await askDelete();
}
break;
@@ -110,6 +117,13 @@ const keyboardCommands = async (event: KeyboardEvent) => {
}
};
+const {
+ asking,
+ answers,
+ ask: askDelete,
+ confirm: deleteNow,
+} = useDeleteChoice(props.item, () => emit("onDelete", props.index));
+
const focus = () => {
labelElement.value?.focus();
};
diff --git a/resources/js/components/Factory/Main/Interactions/ConsentInteraction.vue b/resources/js/components/Factory/Main/Interactions/ConsentInteraction.vue
index fd8151cb..961503ab 100644
--- a/resources/js/components/Factory/Main/Interactions/ConsentInteraction.vue
+++ b/resources/js/components/Factory/Main/Interactions/ConsentInteraction.vue
@@ -45,7 +45,14 @@
-
+
+
@@ -53,6 +60,8 @@
import { D9Input, D9Textarea, D9Switch } from "@deck9/ui";
import InteractionHoverActions from "@/components/Factory/Main/InteractionHoverActions.vue";
import IndexItem from "@/components/Factory/Shared/IndexItem.vue";
+import DeleteChoiceDialog from "@/components/Factory/Shared/DeleteChoiceDialog.vue";
+import { useDeleteChoice } from "@/components/Factory/Shared/useDeleteBlock";
import { useWorkbench } from "@/stores";
import { Ref, ref, watch } from "vue";
@@ -88,6 +97,13 @@ watch([label, message, isRequired], (newValues) => {
workbench.updateInteraction(update);
});
+const {
+ asking,
+ answers,
+ ask: askDelete,
+ confirm: deleteNow,
+} = useDeleteChoice(props.item);
+
const focus = () => {
labelElement.value.focus();
};
diff --git a/resources/js/components/Factory/Shared/DeleteBlockDialog.vue b/resources/js/components/Factory/Shared/DeleteBlockDialog.vue
new file mode 100644
index 00000000..309a671c
--- /dev/null
+++ b/resources/js/components/Factory/Shared/DeleteBlockDialog.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
diff --git a/resources/js/components/Factory/Shared/DeleteChoiceDialog.vue b/resources/js/components/Factory/Shared/DeleteChoiceDialog.vue
new file mode 100644
index 00000000..16b197db
--- /dev/null
+++ b/resources/js/components/Factory/Shared/DeleteChoiceDialog.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
diff --git a/resources/js/components/Factory/Shared/DeleteDialog.vue b/resources/js/components/Factory/Shared/DeleteDialog.vue
new file mode 100644
index 00000000..93e872c9
--- /dev/null
+++ b/resources/js/components/Factory/Shared/DeleteDialog.vue
@@ -0,0 +1,63 @@
+
+
+
+ {{ title }}
+
+
+
+ {{ text }}
+
+
+ {{ note }}
+
+
+
+
+
+
+ {{ confirmLabel }}
+
+
+
+
+
+
diff --git a/resources/js/components/Factory/Shared/DeleteDialogs.test.ts b/resources/js/components/Factory/Shared/DeleteDialogs.test.ts
new file mode 100644
index 00000000..561360c5
--- /dev/null
+++ b/resources/js/components/Factory/Shared/DeleteDialogs.test.ts
@@ -0,0 +1,481 @@
+import { flushPromises, mount } from "@vue/test-utils";
+import { AxiosResponse } from "axios";
+import { createPinia, setActivePinia } from "pinia";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { defineComponent } from "vue";
+import DeleteDialog from "./DeleteDialog.vue";
+import DeleteBlockDialog from "./DeleteBlockDialog.vue";
+import DeleteChoiceDialog from "./DeleteChoiceDialog.vue";
+import { checkDelete, useDeleteBlock } from "./useDeleteBlock";
+import ClickInteraction from "@/components/Factory/Main/Interactions/ClickInteraction.vue";
+import ConsentInteraction from "@/components/Factory/Main/Interactions/ConsentInteraction.vue";
+import InteractionHoverActions from "@/components/Factory/Main/InteractionHoverActions.vue";
+import BlockLogicRule from "@/components/Factory/Sidebar/BlockLogicRule.vue";
+import { callGetFormBlocks } from "@/api/blocks";
+import { useForm, useLogic, useWorkbench } from "@/stores";
+
+vi.mock("@/api/blocks", async (importOriginal) => ({
+ ...(await importOriginal()),
+ callGetFormBlocks: vi.fn(),
+}));
+
+const use = (id: number, block: string): FormBlockRuleUse => ({
+ id,
+ name: `Rule ${id}`,
+ block,
+ as: ["source"],
+});
+
+// the meetup form as the server sends it with includeSubmissions=true
+const blocks = (): FormBlockModel[] =>
+ [
+ {
+ id: 1,
+ uuid: "q1",
+ type: "radio",
+ message: "