This commit is contained in:
John Molakvoæ (skjnldsv) 2020-06-09 23:06:38 +02:00
commit 15e35f51c2
No known key found for this signature in database
GPG key ID: 60C25B8C072916CF
23 changed files with 406 additions and 185 deletions

View file

@ -1,6 +1,11 @@
OC.L10N.register(
"forms",
{
"Anonymous response" : "익명 답변",
"Shared by %s" : "%s에 의해 공유됨",
"New form" : "새로운 형식",
"Loading forms …" : "형식 로딩중 ...",
"Create a form" : "형식 생성",
"Create new form" : "새로운 양식 생성",
"An error occurred while loading the forms list" : "양식 리스트를 가져오는데 문제가 발생하였습니다.",
"Unable to create a new form" : "새 양식 생성할 수 없음",
@ -9,8 +14,11 @@ OC.L10N.register(
"Are you sure you want to delete {title}?" : "정말로 {title}을 삭제할 것입니까?",
"Error while deleting {title}" : "{title} 삭제 실패",
"Answer number {index}" : "응답 번호 {index}",
"Delete answer" : "답변 지우기",
"Error while saving the answer" : "응답 저장 실패",
"Question title" : "질문 제목",
"Delete question" : "질문 삭제",
"Delete this response" : "이 응답 지우기",
"Searching …" : "검색 ...",
"Group" : "그룹",
"Description" : "설명",
@ -21,6 +29,7 @@ OC.L10N.register(
"Expiration date" : "만료 일자",
"Sharing" : "공유",
"Share link" : "링크 공유",
"Select expiration date" : "만료일자를 고르시오.",
"Submit" : "제출",
"Thank you for completing the form!" : "양식을 작성해주셔서 감사합니다!",
"There was an error submitting the form" : "양식 제출 실패",

View file

@ -1,4 +1,9 @@
{ "translations": {
"Anonymous response" : "익명 답변",
"Shared by %s" : "%s에 의해 공유됨",
"New form" : "새로운 형식",
"Loading forms …" : "형식 로딩중 ...",
"Create a form" : "형식 생성",
"Create new form" : "새로운 양식 생성",
"An error occurred while loading the forms list" : "양식 리스트를 가져오는데 문제가 발생하였습니다.",
"Unable to create a new form" : "새 양식 생성할 수 없음",
@ -7,8 +12,11 @@
"Are you sure you want to delete {title}?" : "정말로 {title}을 삭제할 것입니까?",
"Error while deleting {title}" : "{title} 삭제 실패",
"Answer number {index}" : "응답 번호 {index}",
"Delete answer" : "답변 지우기",
"Error while saving the answer" : "응답 저장 실패",
"Question title" : "질문 제목",
"Delete question" : "질문 삭제",
"Delete this response" : "이 응답 지우기",
"Searching …" : "검색 ...",
"Group" : "그룹",
"Description" : "설명",
@ -19,6 +27,7 @@
"Expiration date" : "만료 일자",
"Sharing" : "공유",
"Share link" : "링크 공유",
"Select expiration date" : "만료일자를 고르시오.",
"Submit" : "제출",
"Thank you for completing the form!" : "양식을 작성해주셔서 감사합니다!",
"There was an error submitting the form" : "양식 제출 실패",

View file

@ -1,20 +1,76 @@
OC.L10N.register(
"forms",
{
"Anonymous response" : "Anonimni odziv",
"Forms" : "Vprašalniki",
"📝 Simple surveys and questionnaires, self-hosted" : "Enostavni vprašalnik in ankete na domačem strežniku",
"New form" : "Nov vprašalnik",
"Loading forms …" : "Poteka nalaganje vprašalnikov …",
"No forms created yet" : "Ni še ustvarjenih vprašalnikov",
"Create a form" : "Ustvarite vprašalnik",
"Select a form or create a new one" : "Izberite vprašalnik oziroma ustvarite novega",
"Create new form" : "Ustvari nov vprašalnik",
"An error occurred while loading the forms list" : "Med nalaganjem seznama vprašalnikov je prišlo do napake",
"Unable to create a new form" : "Novega vprašalnika ni mogoče ustvariti",
"Responses" : "Odzivi",
"Clone form" : "Kloniraj vprašalnik",
"Delete form" : "Izbriši vprašalnik",
"Form link copied" : "Povezava vprašalnika je kopirana",
"Cannot copy, please copy the link manually" : "Povezave ni mogoče kopirati. Storite to ročno.",
"Copy share link" : "Kopiraj povezavo souporabe",
"Are you sure you want to delete {title}?" : "Ali ste prepričani, da želite izbrisati vprašalnik {title}?",
"Delete answer" : "Izbriši odgovor",
"There was an issue deleting this option" : "Prišlo je do napake med brisanjem možnosti",
"Error while saving the answer" : "Prišlo je do napake med shranjevanjem odgovora",
"Question number {index}" : "Vprašanje številka {index}",
"Drag to reorder the questions" : "Potegnite vprašanja in jih preuredite",
"Question title" : "Naslov vprašanja",
"Title of question number {index}" : "Naslov vprašanja številka {index}",
"Mandatory" : "Obvezno",
"Delete question" : "Izbriši vprašanje",
"A long answer for the question “{text}”" : "Dolg odgovor na vprašanje »{text}«",
"Long answer text" : "Besedilo dolgega odgovora",
"Add a new answer" : "Dodaj nov odgovor",
"A short answer for the question “{text}”" : "Kratek odgovor na vprašanje »{text}«",
"Short answer text" : "Besedilo kratkega odgovora",
"Delete this response" : "Izbriši ta odziv",
"User or group name …" : "Ime uporabnika oziroma skupine ...",
"No recommendations. Start typing." : "Ni priporočil; začnite vpisovati",
"Searching …" : "Poteka iskanje ...",
"No elements found." : "Ni najdenih predmetov",
"Group" : "Skupina",
"Loading {title} …" : "Poteka nalaganje {title} …",
"Toggle settings" : "Preklopi nastavitve",
"Form title" : "Naslov vprašalnika",
"Description" : "Opis",
"Mandatory questions" : "Obvezna vprašanja",
"Add a question" : "Dodaj vprašanje",
"There was an error while adding the new question" : "Prišlo je do napake med dodajanjem novega vprašanja",
"There was an error while removing the question" : "Prišlo je do napake med odstranjevanjem vprašanja",
"Error while saving form" : "Prišlo je do napake med shranjevanjem vprašalnika",
"Loading responses …" : "Poteka nalaganje odzivov ...",
"Back to questions" : "Nazaj na vprašanja",
"Responses for {title}" : "Odzivi za vprašalnik {title}",
"Export to CSV" : "Izvozi v datoteko CSV",
"Options" : "Možnosti",
"Delete all responses" : "Izbriši vse odzive",
"No responses yet" : "Ni še odzivov",
"Settings" : "Nastavitve",
"Anonymous responses" : "Anonimni odzivi",
"Set expiration date" : "Nastavi datum preteka",
"Expiration date" : "Datum preteka",
"Sharing" : "Souporaba",
"Share link" : "Povezava za souporabo",
"Submit" : "Pošlji"
"Submit" : "Pošlji",
"Submit form" : "Objavi obrazec",
"Submitting form …" : "Poteka objavljanje obrazca ...",
"Error while saving question" : "Prišlo je do napake med shranjevanjem vprašanja",
"Multiple choice" : "Izbirno vprašanje",
"Checkboxes" : "Izbirna polja",
"Short answer" : "Kratek odgovor",
"Long text" : "Dolg odgovor",
"Form expired" : "Vprašalnik je potekel",
"Form not found" : "Vprašalnika ni mogoče najti",
"This form does not exist" : "Ta vprašalnik ne obstaja"
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);");

View file

@ -1,18 +1,74 @@
{ "translations": {
"Anonymous response" : "Anonimni odziv",
"Forms" : "Vprašalniki",
"📝 Simple surveys and questionnaires, self-hosted" : "Enostavni vprašalnik in ankete na domačem strežniku",
"New form" : "Nov vprašalnik",
"Loading forms …" : "Poteka nalaganje vprašalnikov …",
"No forms created yet" : "Ni še ustvarjenih vprašalnikov",
"Create a form" : "Ustvarite vprašalnik",
"Select a form or create a new one" : "Izberite vprašalnik oziroma ustvarite novega",
"Create new form" : "Ustvari nov vprašalnik",
"An error occurred while loading the forms list" : "Med nalaganjem seznama vprašalnikov je prišlo do napake",
"Unable to create a new form" : "Novega vprašalnika ni mogoče ustvariti",
"Responses" : "Odzivi",
"Clone form" : "Kloniraj vprašalnik",
"Delete form" : "Izbriši vprašalnik",
"Form link copied" : "Povezava vprašalnika je kopirana",
"Cannot copy, please copy the link manually" : "Povezave ni mogoče kopirati. Storite to ročno.",
"Copy share link" : "Kopiraj povezavo souporabe",
"Are you sure you want to delete {title}?" : "Ali ste prepričani, da želite izbrisati vprašalnik {title}?",
"Delete answer" : "Izbriši odgovor",
"There was an issue deleting this option" : "Prišlo je do napake med brisanjem možnosti",
"Error while saving the answer" : "Prišlo je do napake med shranjevanjem odgovora",
"Question number {index}" : "Vprašanje številka {index}",
"Drag to reorder the questions" : "Potegnite vprašanja in jih preuredite",
"Question title" : "Naslov vprašanja",
"Title of question number {index}" : "Naslov vprašanja številka {index}",
"Mandatory" : "Obvezno",
"Delete question" : "Izbriši vprašanje",
"A long answer for the question “{text}”" : "Dolg odgovor na vprašanje »{text}«",
"Long answer text" : "Besedilo dolgega odgovora",
"Add a new answer" : "Dodaj nov odgovor",
"A short answer for the question “{text}”" : "Kratek odgovor na vprašanje »{text}«",
"Short answer text" : "Besedilo kratkega odgovora",
"Delete this response" : "Izbriši ta odziv",
"User or group name …" : "Ime uporabnika oziroma skupine ...",
"No recommendations. Start typing." : "Ni priporočil; začnite vpisovati",
"Searching …" : "Poteka iskanje ...",
"No elements found." : "Ni najdenih predmetov",
"Group" : "Skupina",
"Loading {title} …" : "Poteka nalaganje {title} …",
"Toggle settings" : "Preklopi nastavitve",
"Form title" : "Naslov vprašalnika",
"Description" : "Opis",
"Mandatory questions" : "Obvezna vprašanja",
"Add a question" : "Dodaj vprašanje",
"There was an error while adding the new question" : "Prišlo je do napake med dodajanjem novega vprašanja",
"There was an error while removing the question" : "Prišlo je do napake med odstranjevanjem vprašanja",
"Error while saving form" : "Prišlo je do napake med shranjevanjem vprašalnika",
"Loading responses …" : "Poteka nalaganje odzivov ...",
"Back to questions" : "Nazaj na vprašanja",
"Responses for {title}" : "Odzivi za vprašalnik {title}",
"Export to CSV" : "Izvozi v datoteko CSV",
"Options" : "Možnosti",
"Delete all responses" : "Izbriši vse odzive",
"No responses yet" : "Ni še odzivov",
"Settings" : "Nastavitve",
"Anonymous responses" : "Anonimni odzivi",
"Set expiration date" : "Nastavi datum preteka",
"Expiration date" : "Datum preteka",
"Sharing" : "Souporaba",
"Share link" : "Povezava za souporabo",
"Submit" : "Pošlji"
"Submit" : "Pošlji",
"Submit form" : "Objavi obrazec",
"Submitting form …" : "Poteka objavljanje obrazca ...",
"Error while saving question" : "Prišlo je do napake med shranjevanjem vprašanja",
"Multiple choice" : "Izbirno vprašanje",
"Checkboxes" : "Izbirna polja",
"Short answer" : "Kratek odgovor",
"Long text" : "Dolg odgovor",
"Form expired" : "Vprašalnik je potekel",
"Form not found" : "Vprašalnika ni mogoče najti",
"This form does not exist" : "Ta vprašalnik ne obstaja"
},"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
}

View file

@ -7,4 +7,4 @@ OC.L10N.register(
"Sharing" : "ھەمبەھىر",
"Share link" : "Share link"
},
"nplurals=1; plural=0;");
"nplurals=2; plural=(n != 1);");

View file

@ -4,5 +4,5 @@
"Settings" : "تەڭشەكلەر",
"Sharing" : "ھەمبەھىر",
"Share link" : "Share link"
},"pluralForm" :"nplurals=1; plural=0;"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

View file

@ -12,6 +12,7 @@ OC.L10N.register(
"Set expiration date" : "Встановити термін дії",
"Expiration date" : "Термін дії",
"Sharing" : "Поділитись",
"Share link" : "Поширити посилання"
"Share link" : "Поширити посилання",
"Submit" : "Гаразд"
},
"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);");

View file

@ -10,6 +10,7 @@
"Set expiration date" : "Встановити термін дії",
"Expiration date" : "Термін дії",
"Sharing" : "Поділитись",
"Share link" : "Поширити посилання"
"Share link" : "Поширити посилання",
"Submit" : "Гаразд"
},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"
}

View file

@ -648,16 +648,8 @@ class ApiController extends Controller {
$submissions[] = $submission;
}
// Load question-texts, including deleted ones.
try {
$questionEntities = $this->questionMapper->findByForm($form->getId());
} catch (DoesNotExistException $e) {
//handle silently
}
$questions = [];
foreach ($questionEntities as $questionEntity) {
$questions[] = $questionEntity->read();
}
// Load currently active questions
$questions = $this->formsService->getQuestions($form->getId());
$response = [
'submissions' => $submissions,

View file

@ -85,7 +85,12 @@ class FormsService {
$this->currentUser = $userSession->getUser();
}
/**
* Load options corresponding to question
*
* @param integer $questionId
* @return array
*/
public function getOptions(int $questionId): array {
$optionList = [];
try {
@ -100,6 +105,12 @@ class FormsService {
}
}
/**
* Load questions corresponding to form
*
* @param integer $formId
* @return array
*/
public function getQuestions(int $formId): array {
$questionList = [];
try {

172
package-lock.json generated
View file

@ -2772,11 +2772,12 @@
}
},
"@nextcloud/auth": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.2.3.tgz",
"integrity": "sha512-SN0g1nyflt2H34zkCFflOky/h0r9DNHb7T8l/JILyFTCoL8f+f67V2Q4jLLfyapEXgq0b3xG7p8FtrBX5/JhWA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.3.0.tgz",
"integrity": "sha512-GfwRM9W7hat4psNdAt74UHEV+drEXQ53klCVp6JpON66ZLPeK5eJ1LQuiQDkpUxZpqNeaumXjiB98h5cug/uQw==",
"requires": {
"@nextcloud/event-bus": "^1.1.3",
"@nextcloud/typings": "^0.2.2",
"core-js": "^3.6.4"
}
},
@ -2808,9 +2809,9 @@
"dev": true
},
"@nextcloud/dialogs": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-1.3.1.tgz",
"integrity": "sha512-7mr47trvaqWPcI4agSbfapbBdcSlG/2VJ0cW4MB6HXpgzHUQFpEpHRKTpucR1sU0FJTlOVb7irmJ4aH4jZJ8pg==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-1.4.0.tgz",
"integrity": "sha512-Rx4x+al/sy+vXu2p3qvEuVeeUDm5JVwa84S21Hxa+pDV3Pd93E2dJGWlZ6h++5fSXbee1sDX9t957B20kYiP3Q==",
"requires": {
"core-js": "^3.6.4",
"toastify-js": "^1.7.0"
@ -2832,19 +2833,27 @@
}
},
"@nextcloud/event-bus": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.1.4.tgz",
"integrity": "sha512-It27KzmUaSQ7w22nHFwOn8XgeVG0HYYOSNG9gs4UkP5VqcZ16m4ydt3GkMpWcyFec4OUjJc+yf7omRc3pNxsSw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-1.2.0.tgz",
"integrity": "sha512-pNS0R6Mvgj4WnbJQ8LYjxRjCbRndpwjHNyZYm0zl8U71gbHsUvQIIzTdW7WYg6Nz/FjAlrdmDXJDFLh1DDcIFA==",
"requires": {
"@types/semver": "^6.2.1",
"@types/semver": "^7.1.0",
"core-js": "^3.6.2",
"semver": "^6.3.0"
"semver": "^7.3.2"
},
"dependencies": {
"@types/semver": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.2.0.tgz",
"integrity": "sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ==",
"requires": {
"@types/node": "*"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ=="
}
}
},
@ -2857,9 +2866,9 @@
}
},
"@nextcloud/l10n": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-1.2.3.tgz",
"integrity": "sha512-bd/bp/pk24Sl/Fjj3KJhDsGhc5EI1tIs34+sl4eaBNrj7SHl79K9bAzarxtABx6LSXn6pl6K/YYkKCidqPzuDQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-1.3.0.tgz",
"integrity": "sha512-hGk3ag9TU4cb0+ld/wziEsE+CWaS7Rpj6Y6dPv0QVfnqQ7jFCKQ62VHnuk8pFQHkmKMg3HGxkHuojumbukm42w==",
"requires": {
"core-js": "^3.6.4",
"node-gettext": "^3.0.0"
@ -2912,18 +2921,19 @@
}
},
"@nextcloud/router": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-1.0.2.tgz",
"integrity": "sha512-9cGPGZx9P3G/piM3vD8/W37DFcjj4UDUIb9BiKdAG3Ir+7+iI0O/kv7SzvVrEU74a1ohp+wmDUiEQjM02cSFRQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-1.1.0.tgz",
"integrity": "sha512-iPHpMG9kajw8D+niR4x/d8s/R9RyUNveDsNURgcZryIjIXhAzSZZra55+Y3yInDmLhCFwboj9ZcC/2S6CzoKYA==",
"requires": {
"core-js": "3.6.4"
},
"dependencies": {
"core-js": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
"integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw=="
}
"core-js": "^3.6.4"
}
},
"@nextcloud/typings": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-0.2.2.tgz",
"integrity": "sha512-LZrv1VV3vyDaKw4UKMcGM9dvLMpdI2tfMexHf/ixVn6OrsRDsRTbxByWMMdRF2ArHD5Q8RsICa72p6BcG9b80Q==",
"requires": {
"@types/jquery": "2.0.54"
}
},
"@nextcloud/vue": {
@ -3023,12 +3033,22 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/jquery": {
"version": "2.0.54",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-2.0.54.tgz",
"integrity": "sha512-D/PomKwNkDfSKD13DEVQT/pq2TUjN54c6uB341fEZanIzkjfGe7UaFuuaLZbpEiS5j7Wk2MUHAZqZIoECw29lg=="
},
"@types/minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
"dev": true
},
"@types/node": {
"version": "14.0.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.11.tgz",
"integrity": "sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@ -3794,22 +3814,10 @@
"postcss-value-parser": "^4.1.0"
},
"dependencies": {
"browserslist": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz",
"integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001043",
"electron-to-chromium": "^1.3.413",
"node-releases": "^1.1.53",
"pkg-up": "^2.0.0"
}
},
"postcss": {
"version": "7.0.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
"integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"version": "7.0.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -5194,9 +5202,9 @@
"dev": true
},
"entities": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
"dev": true
}
}
@ -7305,9 +7313,9 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"globby": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz",
"integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz",
"integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==",
"dev": true,
"requires": {
"array-union": "^2.1.0",
@ -7319,9 +7327,9 @@
},
"dependencies": {
"ignore": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.6.tgz",
"integrity": "sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA==",
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
}
}
@ -8716,9 +8724,9 @@
}
},
"merge2": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz",
"integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true
},
"micromatch": {
@ -8763,9 +8771,9 @@
"dev": true
},
"min-indent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz",
"integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
"minimalistic-assert": {
@ -9750,9 +9758,9 @@
},
"dependencies": {
"postcss": {
"version": "7.0.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
"integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"version": "7.0.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -9788,9 +9796,9 @@
},
"dependencies": {
"postcss": {
"version": "7.0.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
"integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"version": "7.0.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -11338,14 +11346,14 @@
"dev": true
},
"stylelint": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.5.0.tgz",
"integrity": "sha512-+Jy7ieKAWKTf2tmcAE7jgScxH39Urb87i0bjK/enScFaGWWaFn4kAPwepGOSk2b7CLUDVt/O6kwA0x0p/V7moQ==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.6.0.tgz",
"integrity": "sha512-55gG2pNjVr183JJM/tlr3KAua6vTVX7Ho/lgKKuCIWszTZ1gmrXjX4Wok53SI8wRYFPbwKAcJGULQ77OJxTcNw==",
"dev": true,
"requires": {
"@stylelint/postcss-css-in-js": "^0.37.1",
"@stylelint/postcss-markdown": "^0.36.1",
"autoprefixer": "^9.7.6",
"autoprefixer": "^9.8.0",
"balanced-match": "^1.0.0",
"chalk": "^4.0.0",
"cosmiconfig": "^6.0.0",
@ -11354,10 +11362,10 @@
"file-entry-cache": "^5.0.1",
"get-stdin": "^8.0.0",
"global-modules": "^2.0.0",
"globby": "^11.0.0",
"globby": "^11.0.1",
"globjoin": "^0.1.4",
"html-tags": "^3.1.0",
"ignore": "^5.1.4",
"ignore": "^5.1.8",
"import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4",
"known-css-properties": "^0.19.0",
@ -11368,7 +11376,7 @@
"meow": "^7.0.1",
"micromatch": "^4.0.2",
"normalize-selector": "^0.2.0",
"postcss": "^7.0.30",
"postcss": "^7.0.32",
"postcss-html": "^0.36.0",
"postcss-less": "^3.1.4",
"postcss-media-query-parser": "^0.2.3",
@ -11376,7 +11384,7 @@
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^4.0.2",
"postcss-sass": "^0.4.4",
"postcss-scss": "^2.0.0",
"postcss-scss": "^2.1.1",
"postcss-selector-parser": "^6.0.2",
"postcss-syntax": "^0.36.2",
"postcss-value-parser": "^4.1.0",
@ -11389,7 +11397,7 @@
"sugarss": "^2.0.0",
"svg-tags": "^1.0.0",
"table": "^5.4.6",
"v8-compile-cache": "^2.1.0",
"v8-compile-cache": "^2.1.1",
"write-file-atomic": "^3.0.3"
},
"dependencies": {
@ -11497,9 +11505,9 @@
"dev": true
},
"ignore": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.6.tgz",
"integrity": "sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA==",
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
},
"indent-string": {
@ -11584,9 +11592,9 @@
"dev": true
},
"postcss": {
"version": "7.0.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
"integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"version": "7.0.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz",
"integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -11775,9 +11783,9 @@
"dev": true
},
"v8-compile-cache": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
"integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz",
"integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==",
"dev": true
},
"yargs-parser": {

View file

@ -22,14 +22,14 @@
"stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.vue --fix"
},
"dependencies": {
"@nextcloud/auth": "^1.2.3",
"@nextcloud/auth": "^1.3.0",
"@nextcloud/axios": "^1.3.2",
"@nextcloud/dialogs": "^1.3.1",
"@nextcloud/event-bus": "^1.1.4",
"@nextcloud/dialogs": "^1.4.0",
"@nextcloud/event-bus": "^1.2.0",
"@nextcloud/initial-state": "^1.1.2",
"@nextcloud/l10n": "^1.2.3",
"@nextcloud/l10n": "^1.3.0",
"@nextcloud/moment": "^1.1.1",
"@nextcloud/router": "^1.0.2",
"@nextcloud/router": "^1.1.0",
"@nextcloud/vue": "^2.0.0",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
@ -72,7 +72,7 @@
"file-loader": "^6.0.0",
"node-sass": "^4.14.1",
"sass-loader": "^8.0.2",
"stylelint": "^13.5.0",
"stylelint": "^13.6.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.17.2",
"stylelint-webpack-plugin": "^2.0.0",

View file

@ -184,7 +184,7 @@ export default {
}
},
async copyLink() {
async copyLink(event) {
if (this.$clipboard(this.formLink)) {
this.copySuccess = true
this.copied = true
@ -193,6 +193,9 @@ export default {
this.copied = true
console.debug('Not possible to copy share link')
}
// Set back focus as clipboard removes focus
event.target.focus()
setTimeout(() => {
this.copySuccess = false
this.copied = false

View file

@ -118,21 +118,8 @@ export default {
// Dismiss delete key action
e.preventDefault()
const answer = Object.assign({}, this.answer)
const index = this.index
if (!answer.local) {
// let's not await, deleting in background
axios.delete(generateUrl('/apps/forms/api/v1/option/{id}', { id: this.answer.id }))
.catch(error => {
showError(t('forms', 'There was an issue deleting this option'))
console.error(error)
// restore option
this.$emit('restore', answer, index)
})
}
this.$emit('delete', answer.id, index)
this.$emit('delete', this.answer.id)
},
/**

View file

@ -40,7 +40,8 @@
:maxlength="maxStringLengths.answerText"
minlength="1"
@input="onInput"
@keydown="autoSizeText" />
@keypress="autoSizeText"
@keydown.ctrl.enter="onKeydownCtrlEnter" />
</div>
</Question>
</template>
@ -74,6 +75,9 @@ export default {
textarea.style.cssText = 'height:auto; padding:0'
textarea.style.cssText = `height: ${textarea.scrollHeight + 20}px`
},
onKeydownCtrlEnter(event) {
this.$emit('keydown', event)
},
},
}
</script>

View file

@ -46,7 +46,8 @@
:name="`${id}-answer`"
:required="isRequired(answer.id)"
:type="isUnique ? 'radio' : 'checkbox'"
@change="onChange($event, answer.id)">
@change="onChange($event, answer.id)"
@keydown.enter.exact.prevent="onKeydownEnter">
<label v-if="!edit"
ref="label"
:for="`${id}-answer-${answer.id}`"
@ -61,9 +62,8 @@
:index="index"
:max-option-length="maxStringLengths.optionText"
@add="addNewEntry"
@delete="deleteAnswer"
@update:answer="updateAnswer"
@restore="restoreAnswer" />
@delete="deleteOption"
@update:answer="updateAnswer" />
</template>
<li v-if="(edit && !isLastEmpty) || hasNoAnswer" class="question__item">
@ -81,6 +81,10 @@
</template>
<script>
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import AnswerInput from './AnswerInput'
import QuestionMixin from '../../mixins/QuestionMixin'
import GenRandomId from '../../utils/GenRandomId'
@ -118,9 +122,18 @@ export default {
watch: {
edit(edit) {
// When leaving edit mode, filter and delete empty options
if (!edit) {
// Filter out empty options and update question
this.$emit('update:options', this.options.filter(answer => !!answer.text))
const options = this.options.filter(option => {
if (!option.text) {
this.deleteOptionFromDatabase(option)
return false
}
return true
})
// update parent
this.updateOptions(options)
}
},
},
@ -224,39 +237,65 @@ export default {
},
/**
* Restore an answer locally
* Restore an option locally
*
* @param {Object} answer the answer
* @param {number} index the answer index in this.options
* @param {Object} option the option
* @param {number} index the options index in this.options
*/
restoreAnswer(answer, index) {
restoreOption(option, index) {
const options = this.options.slice()
options.splice(index, 0, answer)
options.splice(index, 0, option)
this.updateOptions(options)
this.focusIndex(index)
},
/**
* Delete an answer locally
* Delete an option
*
* @param {number} id the answer is
* @param {number} index the answer index in this.options
* @param {number} id the options id
*/
deleteAnswer(id, index) {
deleteOption(id) {
// Remove entry
const options = this.options.slice()
const optionIndex = options.findIndex(option => option.id === id)
const option = Object.assign({}, this.options[optionIndex])
// delete locally
options.splice(optionIndex, 1)
// delete from Db
this.deleteOptionFromDatabase(option)
// Update question
this.updateOptions(options)
this.$nextTick(() => {
this.focusIndex(index + 1)
this.focusIndex(optionIndex)
})
},
/**
* Delete the option from Db in background.
* Restore option if delete not possible
*
* @param {Object} option The option to delete
*/
deleteOptionFromDatabase(option) {
const optionIndex = this.options.findIndex(opt => opt.id === option.id)
if (!option.local) {
// let's not await, deleting in background
axios.delete(generateUrl('/apps/forms/api/v1/option/{id}', { id: option.id }))
.catch(error => {
showError(t('forms', 'There was an issue deleting this option'))
console.error(error)
// restore option
this.restoreOption(option, optionIndex)
})
}
},
/**
* Focus the input matching the index
*

View file

@ -40,7 +40,8 @@
:maxlength="maxStringLengths.answerText"
minlength="1"
type="text"
@input="onInput">
@input="onInput"
@keydown.enter.exact.prevent="onKeydownEnter">
</div>
</Question>
</template>

View file

@ -23,9 +23,9 @@
<template>
<div class="answer">
<h4 class="question-text">
{{ question.text }}
{{ questionText }}
</h4>
<p>{{ answer.text }}</p>
<p>{{ answerText }}</p>
</div>
</template>
@ -34,12 +34,12 @@ export default {
name: 'Answer',
props: {
answer: {
type: Object,
answerText: {
type: String,
required: true,
},
question: {
type: Object,
questionText: {
type: String,
required: true,
},
},

View file

@ -35,10 +35,10 @@
</p>
<Answer
v-for="answer in squashedAnswers"
:key="answer.questionId"
:answer="answer"
:question="questionToAnswer(answer.questionId)" />
v-for="question in answeredQuestions"
:key="question.id"
:answer-text="question.squashedAnswers"
:question-text="question.text" />
</div>
</template>
@ -70,30 +70,37 @@ export default {
},
computed: {
// Format submission-timestamp to DateTime
submissionDateTime() {
return moment(this.submission.timestamp, 'X').format('LLLL')
},
squashedAnswers() {
const squashedArray = []
this.submission.answers.forEach(answer => {
const index = squashedArray.findIndex(ansSq => ansSq.questionId === answer.questionId)
if (index > -1) {
squashedArray[index].text = squashedArray[index].text.concat('; ' + answer.text)
} else {
squashedArray.push(answer)
/**
* Join answered Questions with corresponding answers.
* Multiple answers to a question are squashed into one string.
* @returns {Array}
*/
answeredQuestions() {
const answeredQuestionsArray = []
this.questions.forEach(question => {
const answers = this.submission.answers.filter(answer => answer.questionId === question.id)
if (!answers.length) {
return // no answers, go to next question
}
})
const squashedAnswers = answers.map(answer => answer.text).join('; ')
return squashedArray
answeredQuestionsArray.push({
'id': question.id,
'text': question.text,
'squashedAnswers': squashedAnswers,
})
})
return answeredQuestionsArray
},
},
methods: {
questionToAnswer(questionId) {
return this.questions.find(question => question.id === questionId)
},
onDelete() {
this.$emit('delete')
},

View file

@ -136,6 +136,15 @@ export default {
this.$emit('delete')
},
/**
* Don't automatically submit form on Enter, parent will handle that
* To be called with prevent: @keydown.enter.prevent="onKeydownEnter"
* @param {Object} event The fired event
*/
onKeydownEnter(event) {
this.$emit('keydown', event)
},
/**
* Focus the first focusable element
*/

View file

@ -71,10 +71,10 @@
<section v-else>
<Submission
v-for="submission in submissions"
v-for="submission in form.submissions"
:key="submission.id"
:submission="submission"
:questions="questions"
:questions="form.questions"
@delete="deleteSubmission(submission.id)" />
</section>
</AppContent>
@ -117,14 +117,12 @@ export default {
data() {
return {
loadingResults: true,
submissions: [],
questions: [],
}
},
computed: {
noSubmissions() {
return this.submissions && this.submissions.length === 0
return this.form.submissions?.length === 0
},
/**
@ -161,13 +159,15 @@ export default {
})
},
copyShareLink() {
copyShareLink(event) {
const $formLink = window.location.protocol + '//' + window.location.host + generateUrl(`/apps/forms/${this.form.hash}`)
if (this.$clipboard($formLink)) {
showSuccess(t('forms', 'Form link copied'))
} else {
showError(t('forms', 'Cannot copy, please copy the link manually'))
}
// Set back focus as clipboard removes focus
event.target.focus()
},
async loadFormResults() {
@ -178,9 +178,8 @@ export default {
const response = await axios.get(generateUrl('/apps/forms/api/v1/submissions/{hash}', {
hash: this.form.hash,
}))
this.submissions = response.data.submissions
this.questions = response.data.questions
console.debug(this.submissions)
this.form.submissions = response.data.submissions
this.form.questions = response.data.questions
} catch (error) {
console.error(error)
showError(t('forms', 'There was an error while loading results'))
@ -194,8 +193,8 @@ export default {
try {
await axios.delete(generateUrl('/apps/forms/api/v1/submission/{id}', { id }))
const index = this.submissions.findIndex(search => search.id === id)
this.submissions.splice(index, 1)
const index = this.form.submissions.findIndex(search => search.id === id)
this.form.submissions.splice(index, 1)
} catch (error) {
console.error(error)
showError(t('forms', 'There was an error while removing this response'))
@ -212,7 +211,7 @@ export default {
this.loadingResults = true
try {
await axios.delete(generateUrl('/apps/forms/api/v1/submissions/{formId}', { formId: this.form.id }))
this.submissions = []
this.form.submissions = []
} catch (error) {
console.error(error)
showError(t('forms', 'There was an error while removing responses'))
@ -229,19 +228,20 @@ export default {
})
const formattedSubmissions = []
this.submissions.forEach(submission => {
this.form.submissions.forEach(submission => {
const formattedSubmission = {
userDisplayName: submission.userDisplayName,
timestamp: moment(submission.timestamp, 'X').format('L LT'),
}
submission.answers.forEach(answer => {
const questionText = this.questions.find(question => question.id === answer.questionId).text
if (questionText in formattedSubmission) {
formattedSubmission[questionText] = formattedSubmission[questionText].concat('; ').concat(answer.text)
} else {
formattedSubmission[questionText] = answer.text
this.form.questions.forEach(question => {
const questionText = question.text
const answers = submission.answers.filter(answer => answer.questionId === question.id)
if (!answers.length) {
return // no answers, go to next question
}
const squashedAnswers = answers.map(answer => answer.text).join('; ')
formattedSubmission[questionText] = squashedAnswers
})
formattedSubmissions.push(formattedSubmission)
})

View file

@ -323,12 +323,14 @@ export default {
return datetime < moment().add(1, 'hour').toDate()
},
copyShareLink() {
copyShareLink(event) {
if (this.$clipboard(this.shareLink)) {
showSuccess(t('forms', 'Form link copied'))
} else {
showError(t('forms', 'Cannot copy, please copy the link manually'))
}
// Set back focus as clipboard removes focus
event.target.focus()
},
},
}

View file

@ -38,7 +38,9 @@
</header>
<!-- Questions list -->
<form v-if="!loading && !success" @submit.prevent="onSubmit">
<form v-if="!loading && !success"
ref="form"
@submit.prevent="onSubmit">
<ul>
<Questions
:is="answerTypes[question.type].component"
@ -50,9 +52,12 @@
:index="index + 1"
:max-string-lengths="maxStringLengths"
v-bind="question"
:values.sync="answers[question.id]" />
:values.sync="answers[question.id]"
@keydown.enter="onKeydownEnter"
@keydown.ctrl.enter="onKeydownCtrlEnter" />
</ul>
<input class="primary"
<input ref="submitButton"
class="primary"
type="submit"
:value="t('forms', 'Submit')"
:disabled="loading"
@ -154,6 +159,27 @@ export default {
},
methods: {
/**
* On Enter, focus next form-element
* Last form element is the submit button, the form submits on enter then
* @param {Object} event The fired event.
*/
onKeydownEnter(event) {
const formInputs = Array.from(this.$refs.form)
const sourceInputIndex = formInputs.findIndex(input => input === event.originalTarget)
// Focus next form element
formInputs[sourceInputIndex + 1].focus()
},
/**
* Ctrl+Enter typically fires submit on forms.
* Some inputs do automatically, while some need explicit handling
*/
onKeydownCtrlEnter() {
// Using button-click event to not bypass validity-checks and use our specified behaviour
this.$refs.submitButton.click()
},
/**
* Submit the form after the browser validated it 🚀
*/