Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@
</section>
</div>

<InteractionHoverActions @onDelete="workbench.deleteInteraction(item)" />
<InteractionHoverActions @onDelete="askDelete" />
<DeleteChoiceDialog
:choice="asking"
:answers="answers"
@close="asking = null"
@confirm="deleteNow"
/>
</div>
</template>

<script setup lang="ts">
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";

Expand Down Expand Up @@ -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;

Expand All @@ -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();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,23 @@
</section>
</div>

<InteractionHoverActions @onDelete="workbench.deleteInteraction(item)" />
<InteractionHoverActions @onDelete="askDelete" />
<DeleteChoiceDialog
policy
:choice="asking"
:answers="answers"
@close="asking = null"
@confirm="deleteNow"
/>
</div>
</template>

<script setup lang="ts">
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";

Expand Down Expand Up @@ -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();
};
Expand Down
104 changes: 104 additions & 0 deletions resources/js/components/Factory/Shared/DeleteBlockDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<template>
<DeleteDialog
:show="check !== null"
:title="title"
:text="text"
:note="note"
:confirm-label="isGroup ? 'Delete group' : 'Delete question'"
@close="emit('close')"
@confirm="emit('confirm')"
/>
</template>

<script setup lang="ts">
import { computed } from "vue";
import DeleteDialog from "./DeleteDialog.vue";
import { DeleteCheck } from "./useDeleteBlock";
import { useForm } from "@/stores";
import { getTextFromHtml } from "@/utils";

const props = defineProps<{
block: FormBlockModel;
// open while set
check: DeleteCheck | null;
}>();

const emit = defineEmits<{
(e: "close"): void;
(e: "confirm"): void;
}>();

const form = useForm();
const isGroup = computed(() => props.block.type === "group");
const groupName = (block: FormBlockModel) =>
`“${block.title || "Unnamed group"}”`;

// as the editor counts: every block that isn't a group
const questions = (nodes: TreeNode[]): FormBlockModel[] =>
nodes.flatMap((node) =>
node.block.type === "group" ? questions(node.children) : [node.block],
);
const numberOf = (uuid: string) =>
questions(form.blocksTree).findIndex((item) => item.uuid === uuid) + 1;

const place = (uuid: string) => {
const block = form.blocks?.find((item) => item.uuid === uuid);

if (block?.type === "group") {
return `group ${groupName(block)}`;
}

return block ? `question ${numberOf(uuid)}` : "";
};

const title = computed(() =>
isGroup.value
? `Delete group ${groupName(props.block)}?`
: "Delete question?",
);

const text = computed(() => {
if (isGroup.value) {
const count = props.check?.questions ?? 0;
const kids =
count === 1
? "1 question and its answers"
: `${count} questions and their answers`;

return count
? `Its ${kids} will be deleted too. This can't be undone.`
: "The group is empty. This can't be undone.";
}

const plain = getTextFromHtml(props.block.message ?? "").trim();
const short = plain.length > 80 ? `${plain.slice(0, 78).trim()}…` : plain;
const name = short
? `“${short}”`
: `Question ${numberOf(props.block.uuid) || ""}`.trim();
const answers = props.check?.answers;
// unknown when the counts couldn't load: there may still be answers
const withAnswers =
answers === undefined
? " and its answers"
: answers
? ` and its ${answers} ${answers === 1 ? "answer" : "answers"}`
: "";

return `${name}${withAnswers} will be deleted. This can't be undone.`;
});

const note = computed(() => {
const rules = props.check?.usedBy ?? [];

if (!rules.length) {
return "";
}

const places = [...new Set(rules.map((rule) => place(rule.block)))]
.filter(Boolean)
.join(", ");
const uses = rules.length === 1 ? "1 rule uses" : `${rules.length} rules use`;

return `${uses} this ${isGroup.value ? "group" : "question"} and will stop working${places ? `: ${places}` : ""}.`;
});
</script>
51 changes: 51 additions & 0 deletions resources/js/components/Factory/Shared/DeleteChoiceDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<template>
<DeleteDialog
:show="choice !== null"
:title="title"
:text="text"
:confirm-label="policy ? 'Delete policy' : 'Delete choice'"
@close="emit('close')"
@confirm="emit('confirm')"
/>
</template>

<script setup lang="ts">
import { computed } from "vue";
import DeleteDialog from "./DeleteDialog.vue";

const props = defineProps<{
// open while set
choice: Pick<FormBlockInteractionModel, "id" | "label"> | null;
// missing when the server couldn't say
answers?: number;
// a consent policy: people agree to it
policy?: boolean;
}>();

const emit = defineEmits<{
(e: "close"): void;
(e: "confirm"): void;
}>();

const title = computed(() => {
const label = props.choice?.label;

if (props.policy) {
return label ? `Delete policy “${label}”?` : "Delete this policy?";
}

return label ? `Delete “${label}”?` : "Delete this choice?";
});

const text = computed(() => {
const picked = props.policy ? "agreed to" : "picked";

if (props.answers === undefined) {
return `If people ${picked} it, their answers to it will be deleted.`;
}

const people = props.answers === 1 ? "1 person" : `${props.answers} people`;

return `${people} ${picked} it. Their answers to it will be deleted.`;
});
</script>
63 changes: 63 additions & 0 deletions resources/js/components/Factory/Shared/DeleteDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<ConfirmationModal :show="show" max-width="md" danger @close="emit('close')">
<template #title>
<span class="font-semibold [overflow-wrap:anywhere]">{{ title }}</span>
</template>
<template #content>
<p class="text-pretty text-sm text-grey-600 [overflow-wrap:anywhere]">
{{ text }}
</p>
<p v-if="note" class="mt-2 text-pretty text-sm text-grey-600">
{{ note }}
</p>
</template>
<template #footer>
<D9Button
label="Cancel"
color="light"
class="h-11 !bg-white shadow-[inset_0_0_0_1px_theme(colors.grey.300)] hover:!bg-blue-100 lg:h-9"
@click="emit('close')"
/>
<!-- shaped like the D9 Cancel next to it -->
<DangerButton
class="ml-2 h-11 !rounded-lg !border-0 !px-5 !text-sm !font-medium !normal-case !leading-4 !tracking-normal lg:h-9"
@click="emit('confirm')"
>
{{ confirmLabel }}
</DangerButton>
</template>
</ConfirmationModal>
</template>

<script setup lang="ts">
import { D9Button } from "@deck9/ui";
import { useEventListener } from "@vueuse/core";
import ConfirmationModal from "@/Jetstream/ConfirmationModal.vue";
import DangerButton from "@/Jetstream/DangerButton.vue";

const props = defineProps<{
show: boolean;
title: string;
text: string;
note?: string;
confirmLabel: string;
}>();

const emit = defineEmits<{
(e: "close"): void;
(e: "confirm"): void;
}>();

// Escape closes only this dialog, not the logic panel under it
useEventListener(
window,
"keydown",
(event: KeyboardEvent) => {
if (props.show && event.key === "Escape") {
event.stopPropagation();
emit("close");
}
},
{ capture: true },
);
</script>
Loading
Loading