diff --git a/CHANGELOG.md b/CHANGELOG.md
index 448a15af..ea6a16c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/resources/js/forms/classic/layout/Block.test.ts b/resources/js/forms/classic/layout/Block.test.ts
index 893e49af..fea683b5 100644
--- a/resources/js/forms/classic/layout/Block.test.ts
+++ b/resources/js/forms/classic/layout/Block.test.ts
@@ -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);
+ });
+});
diff --git a/resources/js/forms/classic/layout/Block.vue b/resources/js/forms/classic/layout/Block.vue
index 02fe28e3..01d9f5e1 100644
--- a/resources/js/forms/classic/layout/Block.vue
+++ b/resources/js/forms/classic/layout/Block.vue
@@ -24,8 +24,8 @@
{{ t(validator.message ?? "Something went wrong validating your input") }}
-
- {{ t("submit_failed") }}
+
+ {{ t(store.tooLongBlock ? "submit_too_long" : "submit_failed") }}
{
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) {
diff --git a/resources/js/stores/conversation.ts b/resources/js/stores/conversation.ts
index bb66ae20..0142a74f 100644
--- a/resources/js/stores/conversation.ts
+++ b/resources/js/stores/conversation.ts
@@ -1,4 +1,5 @@
import { defineStore } from "pinia";
+import type { AxiosError } from "axios";
import {
callCreateFormSession,
callGetFormStoryboard,
@@ -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;
@@ -45,6 +48,7 @@ export const useConversation = defineStore("form", {
isProcessing: false,
isSubmitted: false,
submitFailed: false,
+ tooLongBlock: null,
isInputMode: false,
isPreview: false,
uploads: {},
@@ -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);
@@ -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 {
@@ -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."
+ 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;
+ }
return Promise.resolve(false);
} finally {
diff --git a/resources/locales/de.json b/resources/locales/de.json
index 36c631e7..e5a1cb7b 100644
--- a/resources/locales/de.json
+++ b/resources/locales/de.json
@@ -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",
diff --git a/resources/locales/en.json b/resources/locales/en.json
index 3af8711d..c1540022 100644
--- a/resources/locales/en.json
+++ b/resources/locales/en.json
@@ -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",
diff --git a/resources/locales/fr.json b/resources/locales/fr.json
index 3ae032f4..806019b3 100644
--- a/resources/locales/fr.json
+++ b/resources/locales/fr.json
@@ -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",
diff --git a/resources/locales/no.json b/resources/locales/no.json
index 9c62b45f..c3b38bad 100644
--- a/resources/locales/no.json
+++ b/resources/locales/no.json
@@ -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",
diff --git a/resources/locales/pl.json b/resources/locales/pl.json
index d7559132..cd59520d 100644
--- a/resources/locales/pl.json
+++ b/resources/locales/pl.json
@@ -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",
diff --git a/resources/locales/sk.json b/resources/locales/sk.json
index d3b4e2b8..52af2efc 100644
--- a/resources/locales/sk.json
+++ b/resources/locales/sk.json
@@ -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",
diff --git a/resources/locales/uk.json b/resources/locales/uk.json
index deba1bd8..4bdb517e 100644
--- a/resources/locales/uk.json
+++ b/resources/locales/uk.json
@@ -29,6 +29,7 @@
"type": "Введіть свою відповідь",
"submit": "Надіслати",
"submit_failed": "Не вдалося надіслати ваші відповіді. Спробуйте ще раз.",
+ "submit_too_long": "Ця відповідь задовга. Скоротіть її та надішліть ще раз.",
"privacy_policy": "Політика конфіденційності",
"other": "Інше",
"next": "Наступне",
diff --git a/resources/locales/zh.json b/resources/locales/zh.json
index eb6a24d4..efb38f71 100644
--- a/resources/locales/zh.json
+++ b/resources/locales/zh.json
@@ -29,6 +29,7 @@
"type": "输入您的答案",
"submit": "提交",
"submit_failed": "无法提交您的回答,请重试。",
+ "submit_too_long": "此回答过长,请缩短后重新提交。",
"privacy_policy": "隐私政策",
"other": "其他",
"next": "下一步",