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 @@ -60,6 +60,7 @@ Lines marked **Upgrade step** need you to act after updating.
- While your forms load, the form list shows placeholder rows. When the list can't load, it says so and offers "Try again". A search or status without forms says so and offers a way back, and screen readers hear how many forms a search finds. Before, all of these said you hadn't created a survey yet. A team without any forms now sees "Create your first form". A form you publish, unpublish, archive or restore in the list moves to its tab right away, and the tab counts follow. (#249)
- A form template with a description, language, color, button label, completion headline or link that is too long is now refused with a message that names the field, and the form stays as it was. Before, the import failed with a server error on MySQL and MariaDB. (#251)
- With a dialog open, Tab and Shift+Tab now stay inside the dialog, and closing it puts you back on the button that opened it. Before, Tab moved on to the page behind the dialog. (#252)
- When a visitor's answer is too long to store, the form now says so and asks them to shorten it, in the form's language. Before, it only said "Your answers could not be sent". (#294)
- The form list's dates and blue dots now update while the page stays open, every minute and when you come back to the tab. Before, "12 min ago" stayed until you reloaded the page. The status tab counts and the search count now write numbers like the rest of the list, also in a browser set to another language. (#254)
- A submission where every question was skipped now counts. Before, a form with only such submissions showed "0 submissions" next to a recent submission date in the form list, and "No submissions found" on its submissions page. Forms with such submissions now show a higher count and completion rate. (#253)
- A form settings page now saves only its own settings. Before, a bad value saved on another settings page could block the save, with an error about a field that isn't on the page. When a save is refused, the editor keeps what was last saved, so the refused change no longer shows on the other settings pages. A good image upload after a failed one no longer shows the old error. (#255)
Expand Down
115 changes: 115 additions & 0 deletions resources/js/forms/classic/layout/Block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,118 @@ describe("Block", () => {
).toBe(false);
});
});

describe("Block submit errors", () => {
const submitWith = async (error: unknown) => {
const pinia = createPinia();
setActivePinia(pinia);

const block = {
id: "block1",
type: "none",
interactions: [],
} as unknown as PublicFormBlockModel;

const store = useConversation();
store.form = { uuid: "form" } as PublicFormModel;
store.session = { token: "session" } as FormSessionModel;
store.queue = [block];
store.current = "block1";

vi.spyOn(console, "warn").mockImplementation(() => {});
vi.mocked(callSubmitForm).mockRejectedValueOnce(error);

const wrapper = mount(Block, {
props: { block },
global: { plugins: [pinia], provide: { disableFocus: true } },
});

await wrapper.find("form").trigger("submit");
await flushPromises();

return wrapper.text();
};

it("says the answer is too long when the server refuses it as too long", async () => {
const text = await submitWith({
response: {
status: 422,
data: { errors: { "payload.block1": ["Too long."] } },
},
});

expect(text).toContain(localeDE.submit_too_long);
expect(text).not.toContain(localeDE.submit_failed);
});

it.each([
["a 500", { response: { status: 500, data: {} } }],
["a lost connection", new Error("Network Error")],
[
"another 422",
{ response: { status: 422, data: { errors: { token: ["x"] } } } },
],
])("still shows the retry message for %s", async (_, error) => {
const text = await submitWith(error);

expect(text).toContain(localeDE.submit_failed);
expect(text).not.toContain(localeDE.submit_too_long);
});

it("goes back to the question whose answer is too long and says it there", async () => {
const pinia = createPinia();
setActivePinia(pinia);

const blocks = ["block1", "block2"].map(
(id) =>
({
id,
type: "none",
interactions: [],
}) as unknown as PublicFormBlockModel,
);

const store = useConversation();
store.form = { uuid: "form" } as PublicFormModel;
store.session = { token: "session" } as FormSessionModel;
store.queue = blocks;
store.path = ["block1"];
store.current = "block2";

vi.spyOn(console, "warn").mockImplementation(() => {});
vi.mocked(callSubmitForm).mockRejectedValueOnce({
response: {
status: 422,
data: { errors: { "payload.block1": ["Too long."] } },
},
});

const last = mount(Block, {
props: { block: blocks[1] },
global: { plugins: [pinia], provide: { disableFocus: true } },
});
await last.find("form").trigger("submit");
await flushPromises();

expect(store.current).toBe("block1");
expect(store.path).toEqual([]);
expect(last.text()).not.toContain(localeDE.submit_too_long);

const first = mount(Block, {
props: { block: blocks[0] },
global: { plugins: [pinia], provide: { disableFocus: true } },
});
expect(first.text()).toContain(localeDE.submit_too_long);
});

it("says it on the current question when the refused question is not on the path", async () => {
const text = await submitWith({
response: {
status: 422,
data: { errors: { "payload.hidden": ["Too long."] } },
},
});

expect(text).toContain(localeDE.submit_too_long);
});
});
11 changes: 9 additions & 2 deletions resources/js/forms/classic/layout/Block.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
{{ t(validator.message ?? "Something went wrong validating your input") }}
</div>

<div v-if="store.submitFailed" class="text-sm text-red-500">
{{ t("submit_failed") }}
<div v-if="showSubmitMessage" class="text-sm text-red-500">
{{ t(store.tooLongBlock ? "submit_too_long" : "submit_failed") }}
</div>

<FormButton
Expand Down Expand Up @@ -83,6 +83,13 @@ onMounted(() => {

const showValidationMessage = ref(false);

// a too long answer's message belongs to its own question
const showSubmitMessage = computed(
() =>
store.submitFailed &&
(!store.tooLongBlock || store.tooLongBlock === props.block.id),
);

const submitButtonLabel = computed(() => {
if (store.currentBlock?.type === "consent") {
if (!store.hasRequiredFields && store.countCurrentSelections === 0) {
Expand Down
34 changes: 33 additions & 1 deletion resources/js/stores/conversation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineStore } from "pinia";
import type { AxiosError } from "axios";
import {
callCreateFormSession,
callGetFormStoryboard,
Expand All @@ -25,6 +26,8 @@ type ConversationStore = {
isProcessing: boolean;
isSubmitted: boolean;
submitFailed: boolean;
// the question whose answer the server refused as too long
tooLongBlock: PublicFormBlockModel["id"] | null;
isInputMode: boolean;
// the editor's preview: the server stores nothing, so skip uploads and the redirect
isPreview: boolean;
Expand All @@ -45,6 +48,7 @@ export const useConversation = defineStore("form", {
isProcessing: false,
isSubmitted: false,
submitFailed: false,
tooLongBlock: null,
isInputMode: false,
isPreview: false,
uploads: {},
Expand Down Expand Up @@ -84,7 +88,8 @@ export const useConversation = defineStore("form", {
for (const block of state.queue) {
// a question in a hidden group is hidden too
if (
(block.parent_block && hidden.includes(block.parent_block)) ||
(block.parent_block &&
hidden.includes(block.parent_block)) ||
!isBlockVisible(block, answers)
) {
hidden.push(block.id);
Expand Down Expand Up @@ -516,6 +521,7 @@ export const useConversation = defineStore("form", {
this.uploads = {};
this.isProcessing = true;
this.submitFailed = false;
this.tooLongBlock = null;

if (this.form?.uuid && this.session?.token) {
try {
Expand Down Expand Up @@ -561,6 +567,32 @@ export const useConversation = defineStore("form", {
} catch (error) {
console.warn("could not submit the form", error);
this.submitFailed = true;
// the server refuses a too long answer with a 422 on the answer's own field, "payload.<block>"
const { response } = error as AxiosError<{
errors?: object;
}>;
const field =
response?.status === 422
? Object.keys(response.data?.errors ?? {}).find(
(key) => key.startsWith("payload."),
)
: undefined;
const tooLong = field?.slice("payload.".length);

// show the message on that question's page: it is on the path, or the current one
const onPath = this.path.lastIndexOf(tooLong ?? "");
if (
tooLong &&
onPath !== -1 &&
this.findBlockIndex(tooLong) !== -1
) {
this.current = tooLong;
this.path = this.path.slice(0, onPath);
this.tooLongBlock = tooLong;
} else {
// a hidden or unknown question: say it where the visitor is
this.tooLongBlock = tooLong ? this.current : null;
}
Comment on lines +584 to +595

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the jump is skipped, the visitor sees nothing. tooLongBlock then names a question that isn't on this page, so Block.vue hides the message here too. In Brave, with a faked 422 on payload.notablock, Submit on the last page stopped spinning and no text showed. Today the form only sends answers from the path, so this needs a changed key (say a nested payload.<id>.0). But then it fails silently. Say it on this page instead:

Suggested change
if (
onPath !== -1 &&
this.tooLongBlock &&
this.findBlockIndex(this.tooLongBlock) !== -1
) {
this.current = this.tooLongBlock;
this.path = this.path.slice(0, onPath);
}
if (
onPath !== -1 &&
this.tooLongBlock &&
this.findBlockIndex(this.tooLongBlock) !== -1
) {
this.current = this.tooLongBlock;
this.path = this.path.slice(0, onPath);
} else if (this.tooLongBlock) {
// no page of its own to go back to: say it on this one
this.tooLongBlock = this.current;
}

I did not run this.


return Promise.resolve(false);
} finally {
Expand Down
1 change: 1 addition & 0 deletions resources/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Gib deine Antwort ein",
"submit": "Absenden",
"submit_failed": "Deine Antworten konnten nicht gesendet werden. Bitte versuche es erneut.",
"submit_too_long": "Diese Antwort ist zu lang. Bitte kürze sie und sende sie erneut.",
"privacy_policy": "Datenschutzerklärung",
"other": "Andere",
"next": "Weiter",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Type your answer",
"submit": "Submit",
"submit_failed": "Your answers could not be sent. Please try again.",
"submit_too_long": "This answer is too long. Please shorten it and send it again.",
"privacy_policy": "Privacy Policy",
"other": "Other",
"next": "Next",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Tapez votre réponse",
"submit": "Soumettre",
"submit_failed": "Vos réponses n'ont pas pu être envoyées. Veuillez réessayer.",
"submit_too_long": "Cette réponse est trop longue. Veuillez la raccourcir et l'envoyer à nouveau.",
"privacy_policy": "Politique de confidentialité",
"other": "Autre",
"next": "Suivant",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/no.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Skriv svaret ditt",
"submit": "Send inn",
"submit_failed": "Svarene dine kunne ikke sendes. Vennligst prøv igjen.",
"submit_too_long": "Dette svaret er for langt. Vennligst korte det ned og send det på nytt.",
"privacy_policy": "Personvern",
"other": "Annet",
"next": "Neste",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Wprowadź swoją odpowiedź",
"submit": "Wyślij",
"submit_failed": "Nie udało się wysłać odpowiedzi. Spróbuj ponownie.",
"submit_too_long": "Ta odpowiedź jest za długa. Skróć ją i wyślij ponownie.",
"privacy_policy": "Polityka Prywatności",
"other": "Inne",
"next": "Następny",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/sk.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Zadajte Vašu odpoveď",
"submit": "Odoslať",
"submit_failed": "Vaše odpovede sa nepodarilo odoslať. Skúste to znova.",
"submit_too_long": "Táto odpoveď je príliš dlhá. Skráťte ju a odošlite znova.",
"privacy_policy": "Ochrana osobných údajov",
"other": "Iné",
"next": "Ďalej",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "Введіть свою відповідь",
"submit": "Надіслати",
"submit_failed": "Не вдалося надіслати ваші відповіді. Спробуйте ще раз.",
"submit_too_long": "Ця відповідь задовга. Скоротіть її та надішліть ще раз.",
"privacy_policy": "Політика конфіденційності",
"other": "Інше",
"next": "Наступне",
Expand Down
1 change: 1 addition & 0 deletions resources/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"type": "输入您的答案",
"submit": "提交",
"submit_failed": "无法提交您的回答,请重试。",
"submit_too_long": "此回答过长,请缩短后重新提交。",
"privacy_policy": "隐私政策",
"other": "其他",
"next": "下一步",
Expand Down
Loading