Skip to content
Open
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
72 changes: 72 additions & 0 deletions src/models/AnswerTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
import type { Component } from 'vue'
import type { FormsOption, FormsQuestion } from '../types/Entities.d.ts'

import IconNumeric from '@material-symbols/svg-400/outlined/123.svg?raw'
import IconNumeric from '@material-symbols/svg-400/outlined/123.svg?raw'
import IconArrowDownDropCircleOutline from '@material-symbols/svg-400/outlined/arrow_drop_down_circle.svg?raw'
import IconCalendar from '@material-symbols/svg-400/outlined/calendar_today.svg?raw'
import IconPhone from '@material-symbols/svg-400/outlined/call.svg?raw'
import IconCheckboxOutline from '@material-symbols/svg-400/outlined/check_box.svg?raw'
import IconFile from '@material-symbols/svg-400/outlined/draft.svg?raw'
import IconGrid from '@material-symbols/svg-400/outlined/grid_view.svg?raw'
import IconLinearScale from '@material-symbols/svg-400/outlined/linear_scale.svg?raw'
import IconLink from '@material-symbols/svg-400/outlined/link.svg?raw'
import IconEMail from '@material-symbols/svg-400/outlined/mail.svg?raw'
import IconPalette from '@material-symbols/svg-400/outlined/palette.svg?raw'
import IconRadioboxMarked from '@material-symbols/svg-400/outlined/radio_button_checked.svg?raw'
import IconClockOutline from '@material-symbols/svg-400/outlined/schedule.svg?raw'
Expand Down Expand Up @@ -55,6 +59,15 @@ export interface AnswerTypeConfig {
warningInvalid: string
unique?: boolean
subtypes?: Record<string, AnswerTypeSubtype>
/**
* Creates an existing question type with its settings already filled in, so an input
* type can be picked directly when adding a question.
*/
preset?: {
type: string
subtype?: string
extraSettings?: Record<string, unknown>
}
pickerType?: string
storageFormat?: string
momentFormat?: string
Expand Down Expand Up @@ -267,6 +280,65 @@ const answerTypes: Record<string, AnswerTypeConfig> = {
warningInvalid: t('forms', 'This question needs a title!'),
},

/**
* The input types below are presets over short text. The validation they use already
* exists, but could previously only be reached from the input-type menu inside an
* existing short-text question, so it could not be picked when adding a question.
*
* Email is deliberately a preset rather than its own type: Question::checkEmailType()
* recognises an email question only as short text carrying email validation, and the
* confirmation-email recipient picker relies on that.
*/
number: {
component: markRaw(QuestionShort),
icon: IconNumeric,
label: t('forms', 'Number'),
predefined: false,
preset: { type: 'short', extraSettings: { validationType: 'number' } },

titlePlaceholder: t('forms', 'Number question title'),
warningInvalid: t('forms', 'This question needs a title!'),
},

phone: {
component: markRaw(QuestionShort),
icon: IconPhone,
label: t('forms', 'Phone number'),
predefined: false,
preset: { type: 'short', extraSettings: { validationType: 'phone' } },

titlePlaceholder: t('forms', 'Phone number question title'),
warningInvalid: t('forms', 'This question needs a title!'),
},

email: {
component: markRaw(QuestionShort),
icon: IconEMail,
label: t('forms', 'Email address'),
predefined: false,
preset: { type: 'short', extraSettings: { validationType: 'email' } },

titlePlaceholder: t('forms', 'Email question title'),
warningInvalid: t('forms', 'This question needs a title!'),
},

link: {
component: markRaw(QuestionShort),
icon: IconLink,
label: t('forms', 'Web address'),
predefined: false,
preset: {
type: 'short',
extraSettings: {
validationType: 'regex',
validationRegex: '/^https?:\\/\\/\\S+$/i',
},
},

titlePlaceholder: t('forms', 'Web address question title'),
warningInvalid: t('forms', 'This question needs a title!'),
},

color: {
component: markRaw(QuestionColor),
icon: IconPalette,
Expand Down
44 changes: 42 additions & 2 deletions src/views/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -641,12 +641,21 @@ export default defineComponent({
isLoadingQuestions.value = true

try {
// A preset is an ordinary question type created with its settings already
// filled in, so that an input type such as a phone number can be picked
// directly instead of being found inside an existing short-text question.
// Applied client-side, so presets need no new backend type.
const preset = answerTypes[type]?.preset
const body: {
type: string
text: string
subtype: string | null
position?: number
} = { type, text, subtype }
} = {
type: preset?.type ?? type,
text,
subtype: preset?.subtype ?? subtype,
}
if (position !== null) {
// position: current question position + 2 (0-based index: +1, next position: +1)
body.position = position + 2
Expand All @@ -660,8 +669,39 @@ export default defineComponent({
)
const question = OcsResponse2Data<FormsQuestion>(response)

// Apply the preset's settings, then carry them into the local copy so the
// question appears configured straight away rather than after a reload.
if (preset?.extraSettings) {
try {
await axios.patch(
generateOcsUrl(
'apps/forms/api/v3/forms/{id}/questions/{questionId}',
{ id: props.form.id, questionId: question.id },
),
{
keyValuePairs: {
extraSettings: preset.extraSettings,
},
},
)
question.extraSettings = {
...(question.extraSettings ?? {}),
...preset.extraSettings,
}
} catch (error) {
// The question exists and is usable; only its preconfiguration failed.
logger.error('Error while applying question preset', {
error,
})
}
}

// Delegate insertion & focus handling to helper
insertQuestion(question, { text, type, answers: [] }, position)
insertQuestion(
question,
{ text, type: body.type, answers: [] },
position,
)
} catch (error) {
logger.error('Error while adding new question', { error })
showError(
Expand Down