diff --git a/src/Forms.vue b/src/Forms.vue index 794543e85..10fd066d6 100644 --- a/src/Forms.vue +++ b/src/Forms.vue @@ -133,6 +133,7 @@ :form="selectedForm" :sidebarOpened="sidebarOpened" :active="sidebarActive" + @update:form="updateSelectedForm" @update:sidebarOpened="sidebarOpened = $event" @update:active="sidebarActive = $event" /> diff --git a/src/FormsSubmit.vue b/src/FormsSubmit.vue index b47beb5a7..db486c8e4 100644 --- a/src/FormsSubmit.vue +++ b/src/FormsSubmit.vue @@ -20,6 +20,7 @@ import type { FormsForm } from './models/Entities.d.ts' import { subscribe, unsubscribe } from '@nextcloud/event-bus' import { loadState } from '@nextcloud/initial-state' import { defineComponent } from 'vue' +import { nextTick, onMounted, onUnmounted } from 'vue' import NcContent from '@nextcloud/vue/components/NcContent' import Submit from './views/Submit.vue' @@ -33,45 +34,14 @@ export default defineComponent({ Submit, }, - data() { - return { - form: loadState(formsAppName, 'form') as FormsForm, - isLoggedIn: loadState(formsAppName, 'isLoggedIn') as boolean, - isEmbedded: loadState(formsAppName, 'isEmbedded', false) as boolean, - shareHash: loadState(formsAppName, 'shareHash') as string, - } - }, - - unmounted() { - unsubscribe('forms:last-updated:set', this.onSubmitMessageEvent) - }, - - mounted() { - if (this.isEmbedded) { - subscribe('forms:last-updated:set', this.onSubmitMessageEvent) - - // Communicate window size to parent window in iframes - const resizeObserver = new ResizeObserver((entries) => { - this.emitResizeMessage(entries[0].target as HTMLElement) - }) - this.$nextTick(() => { - const formEl = document.querySelector('.app-forms-embedded form') - if (formEl) { - resizeObserver.observe(formEl) - } - }) - } - }, + setup() { + const form = loadState(formsAppName, 'form') as FormsForm + const isLoggedIn = loadState(formsAppName, 'isLoggedIn') as boolean + const isEmbedded = loadState(formsAppName, 'isEmbedded', false) as boolean + const shareHash = loadState(formsAppName, 'shareHash') as string + let resizeObserver: ResizeObserver | undefined - methods: { - onSubmitMessageEvent(event: unknown): void { - const id = Number(event) - if (Number.isFinite(id)) { - this.emitSubmitMessage(id) - } - }, - - emitSubmitMessage(id: number): void { + const emitSubmitMessage = (id: number): void => { window.parent?.postMessage( { type: 'form-saved', @@ -81,12 +51,19 @@ export default defineComponent({ }, '*', ) - }, + } + + const onSubmitMessageEvent = (event: unknown): void => { + const id = Number(event) + if (Number.isFinite(id)) { + emitSubmitMessage(id) + } + } /** * @param target Target of which the size should be communicated */ - emitResizeMessage(target: HTMLElement): void { + const emitResizeMessage = (target: HTMLElement): void => { const rect = target.getBoundingClientRect() let height = rect.top + target.scrollHeight let width = target.scrollWidth @@ -117,7 +94,38 @@ export default defineComponent({ }, '*', ) - }, + } + + onMounted(() => { + if (!isEmbedded) { + return + } + + subscribe('forms:last-updated:set', onSubmitMessageEvent) + + // Communicate window size to parent window in iframes + resizeObserver = new ResizeObserver((entries) => { + emitResizeMessage(entries[0].target as HTMLElement) + }) + void nextTick(() => { + const formEl = document.querySelector('.app-forms-embedded form') + if (formEl) { + resizeObserver?.observe(formEl) + } + }) + }) + + onUnmounted(() => { + unsubscribe('forms:last-updated:set', onSubmitMessageEvent) + resizeObserver?.disconnect() + }) + + return { + form, + isLoggedIn, + isEmbedded, + shareHash, + } }, }) diff --git a/src/components/AddQuestionMenu.vue b/src/components/AddQuestionMenu.vue index 6c9859157..4333fc723 100644 --- a/src/components/AddQuestionMenu.vue +++ b/src/components/AddQuestionMenu.vue @@ -65,7 +65,7 @@ import IconPlus from '@material-symbols/svg-400/outlined/add.svg?raw' import IconChevronLeft from '@material-symbols/svg-400/outlined/chevron_left.svg?raw' import { t } from '@nextcloud/l10n' -import { defineComponent, ref, toRef, watch } from 'vue' +import { defineComponent, ref, watch } from 'vue' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActions from '@nextcloud/vue/components/NcActions' import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator' @@ -101,11 +101,13 @@ export default defineComponent({ setup(props, { emit }) { const activeQuestionType = ref(null) const openLocal = ref(props.open) - const open = toRef(props, 'open') - watch(open, (value: boolean) => { - openLocal.value = value - }) + watch( + () => props.open, + (value: boolean) => { + openLocal.value = value + }, + ) watch(openLocal, (value: boolean) => { emit('update:open', value) diff --git a/src/components/AppNavigationForm.vue b/src/components/AppNavigationForm.vue index 9e83edc02..00913a525 100644 --- a/src/components/AppNavigationForm.vue +++ b/src/components/AppNavigationForm.vue @@ -115,7 +115,8 @@ import { showConfirmation, showError } from '@nextcloud/dialogs' import { t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' import { generateOcsUrl } from '@nextcloud/router' -import { defineComponent } from 'vue' +import { computed, defineComponent, ref } from 'vue' +import { useRoute } from 'vue-router' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActionRouter from '@nextcloud/vue/components/NcActionRouter' import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator' @@ -161,98 +162,77 @@ export default defineComponent({ emits: ['mobileCloseNavigation', 'openSharing', 'clone', 'delete'], - setup() { - return { - t, - FormsIcon, - IconArchive, - IconArchiveOff, - IconCheck, - IconContentCopy, - IconDelete, - IconPencil, - IconPoll, - IconShareVariant, - } - }, - - data() { - return { - loading: false as boolean, - } - }, + setup(props, { emit }) { + const route = useRoute() + const loading = ref(false) - computed: { - canEdit(): boolean { - return this.form.permissions.includes(PERMISSION_TYPES.PERMISSION_EDIT) - }, - - canSeeResults(): boolean { - return ( - this.form.permissions.includes(PERMISSION_TYPES.PERMISSION_RESULTS) - || (this.form.submissionCount ?? 0) > 0 - ) - }, + const canEdit = computed(() => { + return props.form.permissions.includes(PERMISSION_TYPES.PERMISSION_EDIT) + }) + const canSeeResults = computed( + () => + props.form.permissions.includes(PERMISSION_TYPES.PERMISSION_RESULTS) + || (props.form.submissionCount ?? 0) > 0, + ) /** * Check if form is current form and set active */ - isActive(): boolean { - return this.form.hash === String(this.$route.params.hash) - }, + const isActive = computed( + () => props.form.hash === String(route.params.hash), + ) /** * Check if the form is archived */ - isArchived(): boolean { - return this.form.state === FormState.FormArchived - }, + const isArchived = computed( + () => props.form.state === FormState.FormArchived, + ) /** * Check if form is expired */ - isExpired(): boolean { - return Boolean(this.form.expires && moment().unix() > this.form.expires) - }, + const isExpired = computed(() => + Boolean(props.form.expires && moment().unix() > props.form.expires), + ) /** * Check if form is locked */ - isFormLocked(): boolean { + const isFormLocked = computed(() => { const currentUserUid = getCurrentUser()?.uid ?? '' - const lockedUntil = this.form.lockedUntil ?? -1 + const lockedUntil = props.form.lockedUntil ?? -1 return ( lockedUntil === 0 || (lockedUntil > moment().unix() - && this.form.lockedBy !== currentUserUid) + && props.form.lockedBy !== currentUserUid) ) - }, + }) /** * Return form title, or placeholder if not set * * @return */ - formTitle(): string { - if (this.form.title) { - return this.form.title + const formTitle = computed(() => { + if (props.form.title) { + return props.form.title } return t('forms', 'New form') - }, + }) /** * Return expiration details for subtitle */ - formSubtitle(): string { - if (this.form.state === FormState.FormClosed) { - // TRANSLATORS: The form was closed manually so it does not take new submissions + const formSubtitle = computed(() => { + if (props.form.state === FormState.FormClosed) { return t('forms', 'Form closed') } - if (this.form.expires) { - const relativeDate = moment(this.form.expires, 'X') + if (props.form.expires) { + const relativeDate = moment(props.form.expires, 'X') .locale(window.OC.getLanguage()) .fromNow() - if (this.isExpired) { + if (isExpired.value) { return t('forms', 'Expired {relativeDate}', { relativeDate, }) @@ -260,77 +240,97 @@ export default defineComponent({ return t('forms', 'Expires {relativeDate}', { relativeDate }) } return '' - }, + }) /** * Return, if form has Subtitle */ - hasSubtitle(): boolean { - return this.formSubtitle !== '' - }, + const hasSubtitle = computed(() => formSubtitle.value !== '') /** * Route to use, depending on readOnly * * @return Route to 'submit' or 'formRoot' */ - routerTarget(): NavigationTarget { - if (this.readOnly) { + const routerTarget = computed(() => { + if (props.readOnly) { return 'submit' } return 'formRoot' - }, - }, + }) - methods: { /** * Closes the App-Navigation on mobile-devices */ - mobileCloseNavigation(): void { - this.$emit('mobileCloseNavigation') - }, + const mobileCloseNavigation = (): void => { + emit('mobileCloseNavigation') + } - onShareForm(): void { - this.$emit('openSharing', this.form.hash) - }, + const onShareForm = (): void => { + emit('openSharing', props.form.hash) + } - onCloneForm(): void { - this.$emit('clone', this.form.id) - }, + const onCloneForm = (): void => { + emit('clone', props.form.id) + } + + const onDeleteForm = async (): Promise => { + loading.value = true + try { + await axios.delete( + generateOcsUrl('apps/forms/api/v3/forms/{id}', { + id: props.form.id, + }), + ) + emit('delete', props.form.id) + } catch (error) { + const response = (error as { response?: unknown }).response + logger.error(`Error while deleting ${formTitle.value}`, { + error: response, + }) + showError( + t('forms', 'Error while deleting {title}', { + title: formTitle.value, + }), + ) + } finally { + loading.value = false + } + } - async onConfirmDelete(): Promise { + const onConfirmDelete = async (): Promise => { const shouldDelete = await showConfirmation({ name: t('forms', 'Delete form'), text: t('forms', 'Are you sure you want to delete {title}?', { - title: this.formTitle, + title: formTitle.value, }), labelConfirm: t('forms', 'Delete form'), labelReject: t('forms', 'Cancel'), }) if (shouldDelete) { - await this.onDeleteForm() + await onDeleteForm() } - }, + } - async onToggleArchive(): Promise { + const onToggleArchive = async (): Promise => { try { // TODO: add loading status feedback ? await axios.patch( generateOcsUrl('apps/forms/api/v3/forms/{id}', { - id: this.form.id, + id: props.form.id, }), { keyValuePairs: { - state: this.isArchived + state: isArchived.value ? FormState.FormClosed : FormState.FormArchived, }, }, ) - ;(this.form as FormsForm).state = this.isArchived + ;(props.form as FormsForm).state = isArchived.value ? FormState.FormClosed : FormState.FormArchived } catch (error) { @@ -339,31 +339,37 @@ export default defineComponent({ }) showError(t('forms', 'Error changing archived state of form')) } - }, + } - async onDeleteForm(): Promise { - this.loading = true - try { - await axios.delete( - generateOcsUrl('apps/forms/api/v3/forms/{id}', { - id: this.form.id, - }), - ) - this.$emit('delete', this.form.id) - } catch (error) { - const response = (error as { response?: unknown }).response - logger.error(`Error while deleting ${this.formTitle}`, { - error: response, - }) - showError( - t('forms', 'Error while deleting {title}', { - title: this.formTitle, - }), - ) - } finally { - this.loading = false - } - }, + return { + t, + FormsIcon, + IconArchive, + IconArchiveOff, + IconCheck, + IconContentCopy, + IconDelete, + IconPencil, + IconPoll, + IconShareVariant, + loading, + canEdit, + canSeeResults, + isActive, + isArchived, + isExpired, + isFormLocked, + formTitle, + formSubtitle, + hasSubtitle, + routerTarget, + mobileCloseNavigation, + onShareForm, + onCloneForm, + onConfirmDelete, + onToggleArchive, + onDeleteForm, + } }, }) diff --git a/src/components/Questions/AnswerInput.vue b/src/components/Questions/AnswerInput.vue index f8ce9a6b2..93f6c2160 100644 --- a/src/components/Questions/AnswerInput.vue +++ b/src/components/Questions/AnswerInput.vue @@ -80,6 +80,7 @@ diff --git a/src/components/Questions/QuestionColor.vue b/src/components/Questions/QuestionColor.vue index 26540f948..5241f41a7 100644 --- a/src/components/Questions/QuestionColor.vue +++ b/src/components/Questions/QuestionColor.vue @@ -74,6 +74,9 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) + const values = computed(() => { + return props.values as Array + }) const colorPickerPlaceholder = computed(() => { return props.readOnly @@ -82,7 +85,7 @@ export default defineComponent({ }) const pickedColor = computed(() => { - return (props.values[0] as string | null | undefined) ?? '' + return values.value[0] ?? '' }) const validate = async (): Promise => { diff --git a/src/components/Questions/QuestionDate.vue b/src/components/Questions/QuestionDate.vue index 90c3ae782..f64711e3b 100644 --- a/src/components/Questions/QuestionDate.vue +++ b/src/components/Questions/QuestionDate.vue @@ -155,11 +155,17 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) + const values = computed(() => { + return props.values as Array + }) + const extraSettings = computed(() => { + return ( + (props.extraSettings as QuestionDateExtraSettings | undefined) ?? {} + ) + }) const isRangeQuestion = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.dateRange || extraSettings?.timeRange + return extraSettings.value.dateRange || extraSettings.value.timeRange ? true : false }) @@ -230,13 +236,13 @@ export default defineComponent({ const time = computed(() => { if (isRangeQuestion.value) { - const firstValue = props.values?.[0] as string | undefined - const secondValue = props.values?.[1] as string | undefined + const firstValue = values.value[0] + const secondValue = values.value[1] return firstValue && secondValue ? [parse(firstValue), parse(secondValue)] : null } - const value = props.values?.[0] as string | undefined + const value = values.value[0] return value ? parse(value) : null }) @@ -244,10 +250,8 @@ export default defineComponent({ * The maximum allowable date for the date input field */ const dateMax = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.dateMax - ? moment(extraSettings.dateMax, 'X').toDate() + return extraSettings.value.dateMax + ? moment(extraSettings.value.dateMax, 'X').toDate() : undefined }) @@ -255,28 +259,22 @@ export default defineComponent({ * The minimum allowable date for the date input field */ const dateMin = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.dateMin - ? moment(extraSettings.dateMin, 'X').toDate() + return extraSettings.value.dateMin + ? moment(extraSettings.value.dateMin, 'X').toDate() : undefined }) const dateRange = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.dateRange ?? false + return extraSettings.value.dateRange ?? false }) /** * The maximum allowable time for the time input field */ const timeMax = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.timeMax + return extraSettings.value.timeMax ? moment( - extraSettings.timeMax, + extraSettings.value.timeMax, props.answerType.storageFormat, ).toDate() : undefined @@ -286,20 +284,16 @@ export default defineComponent({ * The minimum allowable time for the time input field */ const timeMin = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.timeMin + return extraSettings.value.timeMin ? moment( - extraSettings.timeMin, + extraSettings.value.timeMin, props.answerType.storageFormat, ).toDate() : undefined }) const timeRange = computed(() => { - const extraSettings = props.extraSettings as - QuestionDateExtraSettings | undefined - return extraSettings?.timeRange ?? false + return extraSettings.value.timeRange ?? false }) const validate = async (): Promise => { diff --git a/src/components/Questions/QuestionDropdown.vue b/src/components/Questions/QuestionDropdown.vue index 7233a0d4f..7b91656fa 100644 --- a/src/components/Questions/QuestionDropdown.vue +++ b/src/components/Questions/QuestionDropdown.vue @@ -145,15 +145,22 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) - const questionMultiple = useQuestionMultiple(props, { emit }) + const values = computed>(() => { + return Array.isArray(props.values) ? props.values : [] + }) const input = ref< Array<{ focus?: () => void $props?: { optionType?: string; index?: number } } | null> >([]) - const isDragging = ref(false) const isLoading = ref(false) + const questionMultiple = useQuestionMultiple(props, { + emit, + input, + isLoading, + }) + const isDragging = ref(false) const isOptionDialogShown = ref(false) const selectOptionPlaceholder = computed(() => { @@ -177,12 +184,12 @@ export default defineComponent({ }) const selectedOption = computed(() => { - if (!props.values) { + if (values.value.length === 0) { return null } - const selected = props.values - .map((id: unknown) => + const selected = values.value + .map((id) => props.options.find( (option) => option.id === parseInt(String(id), 10), ), @@ -256,6 +263,7 @@ export default defineComponent({ shiftDragHandle, t, validate, + values, } }, }) diff --git a/src/components/Questions/QuestionFile.vue b/src/components/Questions/QuestionFile.vue index 377822a76..5d7f9a006 100644 --- a/src/components/Questions/QuestionFile.vue +++ b/src/components/Questions/QuestionFile.vue @@ -236,6 +236,14 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) + const values = computed(() => { + return props.values as UploadedFileValue[] + }) + const extraSettings = computed(() => { + return ( + (props.extraSettings as QuestionFileExtraSettings | undefined) ?? {} + ) + }) const fileInput = ref(null) const fileLoading = ref(false) const maxFileSizeUnit = ref( @@ -249,41 +257,35 @@ export default defineComponent({ }) const uploadedFiles = computed(() => { - return props.values as UploadedFileValue[] + return values.value }) const maxAllowedFilesCount = computed(() => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined - return extraSettings?.maxAllowedFilesCount ?? 1 + return extraSettings.value.maxAllowedFilesCount ?? 1 }) const allowedFileExtensions = computed(() => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined - return extraSettings?.allowedFileExtensions ?? [] + return extraSettings.value.allowedFileExtensions ?? [] }) const allowedFileTypes = computed(() => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined - return extraSettings?.allowedFileTypes ?? [] + return extraSettings.value.allowedFileTypes ?? [] }) const allowedFileTypesLabel = computed(() => { const allowedFileTypeLabels: string[] = [] - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined - if (extraSettings?.allowedFileTypes?.length) { + if (extraSettings.value.allowedFileTypes?.length) { allowedFileTypeLabels.push( - ...extraSettings.allowedFileTypes.map( + ...extraSettings.value.allowedFileTypes.map( (type: string) => fileTypes[type].label, ), ) } - if (extraSettings?.allowedFileExtensions?.length) { - allowedFileTypeLabels.push(...extraSettings.allowedFileExtensions) + if (extraSettings.value.allowedFileExtensions?.length) { + allowedFileTypeLabels.push( + ...extraSettings.value.allowedFileExtensions, + ) } if (allowedFileTypeLabels.length) { return t('forms', 'Allowed file types: {fileTypes}.', { @@ -295,10 +297,8 @@ export default defineComponent({ }) onMounted(() => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined - if (extraSettings?.maxFileSize) { - const maxFileSize = extraSettings.maxFileSize + if (extraSettings.value.maxFileSize) { + const maxFileSize = extraSettings.value.maxFileSize Object.keys(FILE_SIZE_UNITS).forEach((unit) => { const typedUnit = unit as FileSizeUnit if (maxFileSize > FILE_SIZE_UNITS[typedUnit]) { @@ -322,16 +322,14 @@ export default defineComponent({ const formData = new FormData() let fileInvalid = false - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined ;[...currentInput.files].forEach((file) => { formData.append('files[]', file) if ( - extraSettings?.maxFileSize - && extraSettings.maxFileSize > 0 - && file.size > extraSettings.maxFileSize + extraSettings.value.maxFileSize + && extraSettings.value.maxFileSize > 0 + && file.size > extraSettings.value.maxFileSize ) { showError( t( @@ -340,7 +338,7 @@ export default defineComponent({ { fileName: file.name, maxFileSize: formatFileSize( - extraSettings.maxFileSize, + extraSettings.value.maxFileSize, ), }, ), @@ -405,7 +403,7 @@ export default defineComponent({ } emit('update:values', [ - ...(props.values as UploadedFileValue[]), + ...values.value, ...(OcsResponse2Data(response) as UploadedFileValue[]), ]) } @@ -445,9 +443,7 @@ export default defineComponent({ fileType: string, allowed: boolean, ): void => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined - let allowedFileTypesList = extraSettings?.allowedFileTypes ?? [] + let allowedFileTypesList = extraSettings.value.allowedFileTypes ?? [] if (allowed) { allowedFileTypesList.push(fileType) @@ -463,10 +459,8 @@ export default defineComponent({ } const onAllowedFileExtensionsAdded = (fileExtension: string): void => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined const allowedFileExtensionsList = - extraSettings?.allowedFileExtensions ?? [] + extraSettings.value.allowedFileExtensions ?? [] allowedFileExtensionsList.push(fileExtension) question.onExtraSettingsChange({ allowedFileExtensions: allowedFileExtensionsList, @@ -474,10 +468,8 @@ export default defineComponent({ } const onAllowedFileExtensionsDeleted = (fileExtension: string): void => { - const extraSettings = props.extraSettings as - QuestionFileExtraSettings | undefined let allowedFileExtensionsList = - extraSettings?.allowedFileExtensions ?? [] + extraSettings.value.allowedFileExtensions ?? [] allowedFileExtensionsList = allowedFileExtensionsList.filter( (extension) => extension !== fileExtension, ) @@ -488,11 +480,11 @@ export default defineComponent({ } const onDeleteUploadedFile = (uploadedFileId: number | string): void => { - const values = (props.values as UploadedFileValue[]).filter( + const remainingValues = values.value.filter( (value) => value.uploadedFileId !== uploadedFileId, ) - emit('update:values', values) + emit('update:values', remainingValues) } const validate = async (): Promise => { @@ -518,6 +510,7 @@ export default defineComponent({ return { ...question, + values, IconChevronLeft, IconDelete, IconFile, diff --git a/src/components/Questions/QuestionGrid.vue b/src/components/Questions/QuestionGrid.vue index cff92a380..a5d8ebf48 100644 --- a/src/components/Questions/QuestionGrid.vue +++ b/src/components/Questions/QuestionGrid.vue @@ -228,15 +228,19 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) - const questionMultiple = useQuestionMultiple(props, { emit }) const input = ref< Array<{ focus?: () => void $props?: { optionType?: string; index?: number } } | null> >([]) - const isDragging = ref(false) const isLoading = ref(false) + const questionMultiple = useQuestionMultiple(props, { + emit, + input, + isLoading, + }) + const isDragging = ref(false) const questionTypes = [ { label: t('forms', 'Radio'), id: GridCellType.Radio }, { label: t('forms', 'Checkbox'), id: GridCellType.Checkbox }, @@ -248,6 +252,16 @@ export default defineComponent({ return props.answerType.unique === true }) + const values = computed(() => { + return (props.values as GridQuestionValues | undefined) ?? {} + }) + + const extraSettings = computed(() => { + return ( + (props.extraSettings as QuestionGridExtraSettings | undefined) ?? {} + ) + }) + const shiftDragHandle = computed(() => { return ( !props.readOnly @@ -257,9 +271,7 @@ export default defineComponent({ }) const questionType = computed(() => { - const extraSettings = props.extraSettings as - QuestionGridExtraSettings | undefined - return extraSettings?.questionType ?? GridCellType.Radio + return extraSettings.value.questionType ?? GridCellType.Radio }) const columns = computed({ @@ -289,25 +301,23 @@ export default defineComponent({ }) const plainValues = computed(() => { - const values: GridMatrixValues = {} - const questionValues = props.values as GridQuestionValues + const normalizedValues: GridMatrixValues = {} for (const row of rows.value) { for (const column of columns.value) { - values[row.id] = values[row.id] || {} - const rowValues = questionValues[row.id] as + normalizedValues[row.id] = normalizedValues[row.id] || {} + const rowValues = values.value[row.id] as Record | undefined - values[row.id][column.id] = rowValues?.[column.id] ?? '' + normalizedValues[row.id][column.id] = + rowValues?.[column.id] ?? '' } } - return values + return normalizedValues }) const validate = async (): Promise => { - const extraSettings = props.extraSettings as - QuestionGridExtraSettings | undefined - const values = props.values as unknown[] & { length?: number } - if (props.isRequired && (values.length === 0 || props.values === null)) { + const valueCount = Object.keys(values.value).length + if (props.isRequired && (props.values === null || valueCount === 0)) { question.errorMessage.value = t( 'forms', 'You must answer this question', @@ -317,9 +327,9 @@ export default defineComponent({ if (!isUnique.value) { // Validate limits - const max = extraSettings?.optionsLimitMax ?? 0 - const min = extraSettings?.optionsLimitMin ?? 0 - if (max && (values.length ?? 0) > max) { + const max = extraSettings.value.optionsLimitMax ?? 0 + const min = extraSettings.value.optionsLimitMin ?? 0 + if (max && valueCount > max) { question.errorMessage.value = n( 'forms', 'You must choose at most one option', @@ -328,7 +338,7 @@ export default defineComponent({ ) return false } - if (min && (values.length ?? 0) < min) { + if (min && valueCount < min) { question.errorMessage.value = n( 'forms', 'You must choose at least one option', @@ -354,10 +364,10 @@ export default defineComponent({ } const onChangeCheckboxRadio = (rowId: number, value: unknown): void => { - const values = { ...(props.values as GridQuestionValues) } - values[rowId] = value + const nextValues = { ...(values.value as GridQuestionValues) } + nextValues[rowId] = value - emit('update:values', values) + emit('update:values', nextValues) } const onChangeTextNumber = ( @@ -391,6 +401,8 @@ export default defineComponent({ shiftDragHandle, t, validate, + values, + extraSettings, } }, }) diff --git a/src/components/Questions/QuestionLinearScale.vue b/src/components/Questions/QuestionLinearScale.vue index 730bf3160..6e69e0c97 100644 --- a/src/components/Questions/QuestionLinearScale.vue +++ b/src/components/Questions/QuestionLinearScale.vue @@ -155,6 +155,14 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) + const values = computed(() => { + return props.values as string[] + }) + const extraSettings = computed(() => { + return ( + (props.extraSettings as LinearScaleExtraSettings | undefined) ?? {} + ) + }) const lowest = ref<{ $refs: { input: HTMLTextAreaElement } } | null>(null) const highest = ref<{ $refs: { input: HTMLTextAreaElement } } | null>(null) @@ -163,9 +171,7 @@ export default defineComponent({ const optionsLowest = computed({ get: () => { - const extraSettings = props.extraSettings as - LinearScaleExtraSettings | undefined - return extraSettings?.optionsLowest ?? 1 + return extraSettings.value.optionsLowest ?? 1 }, set: (value: number) => { question.onExtraSettingsChange({ @@ -176,9 +182,7 @@ export default defineComponent({ const optionsHighest = computed({ get: () => { - const extraSettings = props.extraSettings as - LinearScaleExtraSettings | undefined - return extraSettings?.optionsHighest ?? 5 + return extraSettings.value.optionsHighest ?? 5 }, set: (value: number) => { question.onExtraSettingsChange({ @@ -189,9 +193,7 @@ export default defineComponent({ const optionsLabelLowest = computed({ get: () => { - const extraSettings = props.extraSettings as - LinearScaleExtraSettings | undefined - return extraSettings?.optionsLabelLowest ?? defaultLowestLabel + return extraSettings.value.optionsLabelLowest ?? defaultLowestLabel }, set: (value: string) => { question.onExtraSettingsChange({ @@ -202,9 +204,7 @@ export default defineComponent({ const optionsLabelHighest = computed({ get: () => { - const extraSettings = props.extraSettings as - LinearScaleExtraSettings | undefined - return extraSettings?.optionsLabelHighest ?? defaultHighestLabel + return extraSettings.value.optionsLabelHighest ?? defaultHighestLabel }, set: (value: string) => { question.onExtraSettingsChange({ @@ -221,7 +221,7 @@ export default defineComponent({ ) }) - const questionValues = computed(() => props.values) + const questionValues = computed(() => values.value) /** * ID for the label for the lowest option @@ -258,7 +258,7 @@ export default defineComponent({ }) const validate = async (): Promise => { - if (props.isRequired && props.values.length === 0) { + if (props.isRequired && values.value.length === 0) { question.errorMessage.value = t( 'forms', 'You must answer this question', diff --git a/src/components/Questions/QuestionLong.vue b/src/components/Questions/QuestionLong.vue index a26a07a3a..507e4b51b 100644 --- a/src/components/Questions/QuestionLong.vue +++ b/src/components/Questions/QuestionLong.vue @@ -60,6 +60,11 @@ export default defineComponent({ setup(props, { emit }) { const textarea = ref(null) const question = useQuestion(props, { emit }) + const values = computed(() => { + return props.values as Array< + string | number | readonly string[] | null | undefined + > + }) const submissionInputPlaceholder = computed(() => { if (props.readOnly) { @@ -69,7 +74,7 @@ export default defineComponent({ }) const textareaValue = computed(() => { - return (props.values[0] ?? null) as + return (values.value[0] ?? null) as string | number | readonly string[] | null | undefined }) @@ -83,7 +88,7 @@ export default defineComponent({ } watch( - () => props.values, + () => values.value, () => { nextTick(() => { autoSizeText() @@ -95,7 +100,7 @@ export default defineComponent({ const validate = async (): Promise => { if ( props.isRequired - && (props.values.length === 0 || props.values[0] === '') + && (values.value.length === 0 || values.value[0] === '') ) { question.errorMessage.value = t( 'forms', diff --git a/src/components/Questions/QuestionMultiple.vue b/src/components/Questions/QuestionMultiple.vue index 2b28ea015..8d3ce11ec 100644 --- a/src/components/Questions/QuestionMultiple.vue +++ b/src/components/Questions/QuestionMultiple.vue @@ -5,6 +5,7 @@ @@ -251,14 +252,19 @@ export default defineComponent({ ], setup(props, { emit }) { - const question = useQuestion(props, { emit }) - const questionMultiple = useQuestionMultiple(props, { emit }) + const rootElement = ref<{ $el?: HTMLElement } | null>(null) const input = ref< Array<{ focus?: () => void $props?: { optionType?: string; index?: number } } | null> >([]) + const isLoading = ref(false) + const questionMultiple = useQuestionMultiple(props, { + emit, + input, + isLoading, + }) /** * This is used to cache the "other" answer, meaning if the user: * checks "other" types text, unchecks "other" and then re-check "other" the typed text is preserved @@ -266,9 +272,15 @@ export default defineComponent({ const cachedOtherAnswerText = ref('') const isDragging = ref(false) const isOptionDialogShown = ref(false) - const isLoading = ref(false) const isUnique = computed(() => props.answerType.unique === true) + const values = computed(() => props.values as string[]) + const extraSettings = computed(() => { + return ( + (props.extraSettings as QuestionMultipleExtraSettings | undefined) + ?? {} + ) + }) const shiftDragHandle = computed(() => { return ( @@ -290,27 +302,18 @@ export default defineComponent({ }) const questionValues = computed(() => { - const values = props.values as string[] - return isUnique.value ? values?.[0] : values - }) - - const multipleSettings = computed(() => { - return ( - (props.extraSettings as QuestionMultipleExtraSettings | undefined) - ?? {} - ) + return isUnique.value ? values.value?.[0] : values.value }) const allowOtherAnswer = computed(() => { - return multipleSettings.value.allowOtherAnswer ?? false + return extraSettings.value.allowOtherAnswer ?? false }) /** * The full "other" answer including prefix, undefined if no "other answer" */ const otherAnswer = computed(() => { - const values = props.values as string[] - return values.find((v) => + return values.value.find((v) => v.startsWith(QUESTION_EXTRASETTINGS_OTHER_PREFIX), ) }) @@ -336,8 +339,8 @@ export default defineComponent({ }) const infoMessage = computed(() => { - const min = multipleSettings.value.optionsLimitMin ?? 0 - const max = multipleSettings.value.optionsLimitMax ?? 0 + const min = extraSettings.value.optionsLimitMin ?? 0 + const max = extraSettings.value.optionsLimitMax ?? 0 if (!min && !max) { return null @@ -375,6 +378,7 @@ export default defineComponent({ max, ) }) + const question = useQuestion(props, { emit, infoMessage, rootElement }) /** * Is the provided answer required ? @@ -414,7 +418,6 @@ export default defineComponent({ } const validate = async (): Promise => { - const values = props.values as string[] if (props.isRequired && questionMultiple.areNoneChecked.value) { question.errorMessage.value = t( 'forms', @@ -427,7 +430,7 @@ export default defineComponent({ // Validate limits const max = multipleSettings.value.optionsLimitMax ?? 0 const min = multipleSettings.value.optionsLimitMin ?? 0 - if (max && values.length > max) { + if (max && values.value.length > max) { question.errorMessage.value = n( 'forms', 'You must choose at most one option', @@ -436,7 +439,7 @@ export default defineComponent({ ) return false } - if (min && values.length < min) { + if (min && values.value.length < min) { question.errorMessage.value = n( 'forms', 'You must choose at least one option', @@ -516,7 +519,7 @@ export default defineComponent({ return } - if ((multipleSettings.value.optionsLimitMin ?? 0) > parsedMax) { + if ((extraSettings.value.optionsLimitMin ?? 0) > parsedMax) { showError( t( 'forms', @@ -555,8 +558,8 @@ export default defineComponent({ } if ( - multipleSettings.value.optionsLimitMax - && parsedMin > multipleSettings.value.optionsLimitMax + extraSettings.value.optionsLimitMax + && parsedMin > extraSettings.value.optionsLimitMax ) { showError( t( @@ -607,7 +610,7 @@ export default defineComponent({ isUnique.value ? [prefixedValue] : [ - ...(props.values as string[]).filter( + ...values.value.filter( (v) => !v.startsWith( QUESTION_EXTRASETTINGS_OTHER_PREFIX, @@ -645,7 +648,6 @@ export default defineComponent({ IconCheckboxBlankOutline, IconContentPaste, IconRadioboxBlank, - multipleSettings, onAllowOtherAnswerChange, onChange, onChangeOther, @@ -661,6 +663,7 @@ export default defineComponent({ QUESTION_EXTRASETTINGS_OTHER_PREFIX, questionValues, resetOtherAnswerText, + rootElement, shiftDragHandle, t, validate, diff --git a/src/components/Questions/QuestionRanking.vue b/src/components/Questions/QuestionRanking.vue index 620aec4d2..9cb76604d 100644 --- a/src/components/Questions/QuestionRanking.vue +++ b/src/components/Questions/QuestionRanking.vue @@ -202,14 +202,7 @@ import IconDragIndicator from '@material-symbols/svg-400/outlined/drag_indicator import IconArrowDown from '@material-symbols/svg-400/outlined/keyboard_arrow_down.svg?raw' import IconArrowUp from '@material-symbols/svg-400/outlined/keyboard_arrow_up.svg?raw' import { t } from '@nextcloud/l10n' -import { - computed, - defineComponent, - getCurrentInstance, - nextTick, - ref, - watch, -} from 'vue' +import { computed, defineComponent, nextTick, ref, watch } from 'vue' import { VueDraggable as Draggable } from 'vue-draggable-plus' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox' @@ -252,17 +245,29 @@ export default defineComponent({ setup(props, { emit }) { const question = useQuestion(props, { emit }) - const questionMultiple = useQuestionMultiple(props, { emit }) - const instance = getCurrentInstance() + const values = computed>(() => { + return Array.isArray(props.values) ? props.values : [] + }) const input = ref< Array<{ focus?: () => void $props?: { optionType?: string; index?: number } } | null> >([]) + const isLoading = ref(false) + const questionMultiple = useQuestionMultiple(props, { + emit, + input, + isLoading, + }) + const buttonOptionUp = ref void } } | null>>( + [], + ) + const buttonOptionDown = ref void } } | null>>( + [], + ) const isDragging = ref(false) const isRanking = ref(false) - const isLoading = ref(false) const isOptionDialogShown = ref(false) const rankedOptions = ref([]) const unrankedOptions = ref([]) @@ -309,12 +314,12 @@ export default defineComponent({ OptionType.Choice, ) - if (props.values && props.values.length > 0) { + if (values.value.length > 0) { // Restore order from saved values (array of option IDs) const byId = Object.fromEntries( sorted.map((option) => [option.id, option]), ) as Record - rankedOptions.value = props.values + rankedOptions.value = values.value .map((id) => byId[parseInt(String(id), 10)]) .filter((option): option is FormsOption => Boolean(option)) unrankedOptions.value = sorted.filter( @@ -395,10 +400,15 @@ export default defineComponent({ * @param refName The ref name ('buttonOptionUp' or 'buttonOptionDown') * @param index The index of the item in the v-for */ - const focusButton = (refName: string, index: number): void => { + const focusButton = ( + refName: 'buttonOptionUp' | 'buttonOptionDown', + index: number, + ): void => { nextTick(() => { - const refs = instance?.proxy?.$refs?.[refName] as - Array<{ $el?: { focus?: () => void } }> | undefined + const refs = + refName === 'buttonOptionUp' + ? buttonOptionUp.value + : buttonOptionDown.value if (Array.isArray(refs) && refs[index]) { refs[index].$el?.focus?.() } @@ -476,6 +486,8 @@ export default defineComponent({ return { ...question, ...questionMultiple, + buttonOptionDown, + buttonOptionUp, choices, input, initRankedOptions, @@ -502,6 +514,7 @@ export default defineComponent({ unrankedOptions, unrankOption, validate, + values, } }, }) diff --git a/src/components/Questions/QuestionShort.vue b/src/components/Questions/QuestionShort.vue index d7c79dd83..a1c5a5462 100644 --- a/src/components/Questions/QuestionShort.vue +++ b/src/components/Questions/QuestionShort.vue @@ -97,6 +97,11 @@ import { INPUT_DEBOUNCE_MS } from '../../models/Constants.ts' import validationTypes from '../../models/ValidationTypes.ts' import { splitRegex, validateExpression } from '../../utils/RegularExpression.ts' +type QuestionShortExtraSettings = { + validationRegex?: string + validationType?: string +} + export default defineComponent({ name: 'QuestionShort', @@ -120,15 +125,22 @@ export default defineComponent({ } } | null>(null) const isValidationTypeMenuOpen = ref(false) + const values = computed(() => { + return props.values as Array< + string | number | readonly string[] | null | undefined + > + }) + const extraSettings = computed(() => { + return ( + (props.extraSettings as QuestionShortExtraSettings | undefined) ?? {} + ) + }) /** * Name of the current validation type, fallsback to 'text' */ const validationType = computed(() => { - return ( - (props.extraSettings as { validationType?: string } | undefined) - ?.validationType || 'text' - ) + return extraSettings.value.validationType || 'text' }) /** @@ -149,10 +161,7 @@ export default defineComponent({ * The regular expression */ const validationRegex = computed(() => { - return ( - (props.extraSettings as { validationRegex?: string } | undefined) - ?.validationRegex || '' - ) + return extraSettings.value.validationRegex || '' }) const submissionInputPlaceholder = computed(() => { @@ -169,7 +178,7 @@ export default defineComponent({ }) const inputValue = computed(() => { - return (props.values[0] ?? null) as + return (values.value[0] ?? null) as string | number | readonly string[] | null | undefined }) diff --git a/src/components/SidebarTabs/SettingsSidebarTab.vue b/src/components/SidebarTabs/SettingsSidebarTab.vue index b16e2f85d..2faad8007 100644 --- a/src/components/SidebarTabs/SettingsSidebarTab.vue +++ b/src/components/SidebarTabs/SettingsSidebarTab.vue @@ -281,7 +281,7 @@ import { loadState } from '@nextcloud/initial-state' import { t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' import { vOnClickOutside as ClickOutside } from '@vueuse/components' -import { defineComponent } from 'vue' +import { computed, defineComponent, inject, ref, watch } from 'vue' import NcButton from '@nextcloud/vue/components/NcButton' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePicker' @@ -329,8 +329,6 @@ export default defineComponent({ ClickOutside, }, - inject: ['$markdownit'], - props: { form: { type: Object, @@ -350,210 +348,162 @@ export default defineComponent({ emits: ['update:formProp'], - setup() { + setup(props, { emit }) { const { SHARE_TYPES } = useShareTypes() - - return { - t, - SHARE_TYPES, - } - }, - - data(): { - formatter: { - stringify: (datetime: Date | [Date, Date] | null) => string - parse: (value: number) => Date - } - appConfig: SettingsAppConfig - maxStringLengths: Record - editMessage: boolean - svgLockOpen: string - confirmationEmailSubject: string - confirmationEmailBody: string - } { - return { - formatter: { - stringify: (datetime: Date | [Date, Date] | null) => { - if (datetime instanceof Date) { - return this.stringifyDate(datetime) - } - return this.stringifyDate(new Date()) - }, - - parse: this.parseTimestampToDate, - }, - - appConfig: loadState(formsAppName, 'appConfig') as SettingsAppConfig, - maxStringLengths: loadState(formsAppName, 'maxStringLengths'), - /** If custom submission message is shown as input or rendered markdown */ - editMessage: false, - svgLockOpen, - confirmationEmailSubject: '', - confirmationEmailBody: '', - } - }, - - computed: { - isCurrentUserOwner(): boolean { - return getCurrentUser()?.uid === this.form.ownerId - }, - - isFormLockedPermanently(): boolean { - return this.locked && this.form.lockedUntil === 0 - }, + const appConfig = ref( + loadState(formsAppName, 'appConfig') as SettingsAppConfig, + ) + const maxStringLengths = ref>( + loadState(formsAppName, 'maxStringLengths'), + ) + const editMessage = ref(false) + const confirmationEmailSubject = ref('') + const confirmationEmailBody = ref('') + const isCurrentUserOwner = computed( + () => getCurrentUser()?.uid === props.form.ownerId, + ) + const isFormLockedPermanently = computed( + () => props.locked && props.form.lockedUntil === 0, + ) /** * If the form has a custom submission message or the user wants to add one (settings switch) */ - hasCustomSubmissionMessage(): boolean { - return ( - this.form?.submissionMessage !== undefined - && this.form?.submissionMessage !== null - ) - }, + const hasCustomSubmissionMessage = computed( + () => + props.form?.submissionMessage !== undefined + && props.form?.submissionMessage !== null, + ) + const hasPublicLink = computed( + () => + props.form.shares.filter( + (share) => share.shareType === SHARE_TYPES.SHARE_TYPE_LINK, + ).length !== 0, + ) /** * Submit Multiple is disabled, if it cannot be controlled. */ - disableSubmitMultiple(): boolean { - return this.hasPublicLink || this.form.isAnonymous - }, - - disableSubmitMultipleExplanation(): string { - if (this.disableSubmitMultiple) { + const disableSubmitMultiple = computed( + () => hasPublicLink.value || props.form.isAnonymous, + ) + const disableSubmitMultipleExplanation = computed(() => { + if (disableSubmitMultiple.value) { return t( 'forms', 'This can not be controlled, if the form has a public link or stores responses anonymously.', ) } return '' - }, - - hasPublicLink(): boolean { - return ( - this.form.shares.filter( - (share) => share.shareType === this.SHARE_TYPES.SHARE_TYPE_LINK, - ).length !== 0 - ) - }, + }) // If disabled, submitMultiple will be casted to true - submitMultiple(): boolean { - return this.disableSubmitMultiple || this.form.submitMultiple - }, - - formExpires(): boolean { - return this.form.expires !== 0 - }, - - formArchived(): boolean { - return this.form.state === FormState.FormArchived - }, - - formClosed(): boolean { - return this.form.state !== FormState.FormActive - }, - - hasMaxSubmissions(): boolean { + const submitMultiple = computed( + () => disableSubmitMultiple.value || props.form.submitMultiple, + ) + const formExpires = computed(() => props.form.expires !== 0) + const formArchived = computed( + () => props.form.state === FormState.FormArchived, + ) + const formClosed = computed(() => props.form.state !== FormState.FormActive) + const hasMaxSubmissions = computed( + () => + props.form.maxSubmissions !== null + && props.form.maxSubmissions !== undefined, + ) + const maxSubmissionsValue = computed(() => props.form.maxSubmissions ?? 1) + const isExpired = computed( + () => props.form.expires && moment().unix() > props.form.expires, + ) + const expirationDate = computed(() => + moment(props.form.expires, 'X').toDate(), + ) + const injectMarkdownit = (): MarkdownRenderer => { return ( - this.form.maxSubmissions !== null - && this.form.maxSubmissions !== undefined + (inject('$markdownit', { + render: (input: string) => input, + }) as MarkdownRenderer) ?? { + render: (input: string) => input, + } ) - }, - - maxSubmissionsValue(): number { - return this.form.maxSubmissions ?? 1 - }, - - isExpired(): boolean { - return this.form.expires && moment().unix() > this.form.expires - }, - - expirationDate(): Date { - return moment(this.form.expires, 'X').toDate() - }, + } /** * The submission message rendered as HTML */ - submissionMessageHTML(): string { - return (this.$markdownit as MarkdownRenderer).render( - this.form.submissionMessage || '', - ) - }, - - emailBodyPlaceholder(): string { - return t( + const submissionMessageHTML = computed(() => { + const markdownit = injectMarkdownit() + return markdownit.render(props.form.submissionMessage || '') + }) + const emailBodyPlaceholder = computed(() => + t( 'forms', 'Hello,\n\nThank you for submitting the form "{formTitle}".\n\nBest regards', - ) - }, - - emailQuestionCount(): number { - return this.confirmationEmailQuestions.length - }, - - confirmationEmailQuestions(): FormsQuestion[] { - const questions = this.form?.questions || [] + ), + ) + const confirmationEmailQuestions = computed(() => { + const questions = props.form?.questions || [] return questions.filter( (question: FormsQuestion) => question.type === 'short' && question.extraSettings?.validationType === 'email', ) - }, - - selectedConfirmationEmailQuestion(): FormsQuestion | null { - const selectedQuestion = this.confirmationEmailQuestions.find( + }) + const emailQuestionCount = computed( + () => confirmationEmailQuestions.value.length, + ) + const selectedConfirmationEmailQuestion = computed(() => { + const selectedQuestion = confirmationEmailQuestions.value.find( (question: FormsQuestion) => - question.id === this.form.confirmationEmailQuestionId, + question.id === props.form.confirmationEmailQuestionId, ) if (selectedQuestion) { return selectedQuestion } if ( - this.form.confirmationEmailQuestionId === null - && this.emailQuestionCount === 1 + props.form.confirmationEmailQuestionId === null + && emailQuestionCount.value === 1 ) { - return this.confirmationEmailQuestions[0] + return confirmationEmailQuestions.value[0] } - return null - }, - - selectedConfirmationEmailQuestionId(): number | string { - return ( - this.form.confirmationEmailQuestionId - ?? this.selectedConfirmationEmailQuestion?.id - ?? '' - ) - }, - - confirmationEmailQuestionOptions(): ConfirmationEmailQuestionOption[] { - return this.confirmationEmailQuestions.map((question) => ({ + }) + const selectedConfirmationEmailQuestionId = computed( + () => + props.form.confirmationEmailQuestionId + ?? selectedConfirmationEmailQuestion.value?.id + ?? '', + ) + const confirmationEmailQuestionLabel = (question: FormsQuestion): string => + question.text || t('forms', 'Untitled question') + const confirmationEmailQuestionOptions = computed(() => + confirmationEmailQuestions.value.map((question) => ({ id: question.id, - label: this.confirmationEmailQuestionLabel(question), - })) - }, - - selectedConfirmationEmailQuestionOption(): ConfirmationEmailQuestionOption | null { - return ( - this.confirmationEmailQuestionOptions.find( + label: confirmationEmailQuestionLabel(question), + })), + ) + const selectedConfirmationEmailQuestionOption = computed( + () => + confirmationEmailQuestionOptions.value.find( (question) => - question.id === this.selectedConfirmationEmailQuestionId, - ) || null - ) - }, - - confirmationEmailErrorText(): string { - if (this.emailQuestionCount === 0) { + question.id === selectedConfirmationEmailQuestionId.value, + ) || null, + ) + const requiresConfirmationEmailQuestionIdSelection = computed( + () => + emailQuestionCount.value > 1 + && !selectedConfirmationEmailQuestion.value, + ) + const confirmationEmailErrorText = computed(() => { + if (emailQuestionCount.value === 0) { return t( 'forms', 'Add at least one email field before confirmation emails can be used.', ) } - if (this.requiresConfirmationEmailQuestionIdSelection) { + if (requiresConfirmationEmailQuestionIdSelection.value) { return t( 'forms', 'Select which email field should receive confirmation emails before finishing this setup.', @@ -561,289 +511,307 @@ export default defineComponent({ } return '' - }, - - confirmationEmailNoteCardType(): 'warning' | 'info' { - if (this.requiresConfirmationEmailQuestionIdSelection) { + }) + const confirmationEmailNoteCardType = computed<'warning' | 'info'>(() => { + if (requiresConfirmationEmailQuestionIdSelection.value) { return 'warning' } return 'info' - }, + }) + const isConfirmationEmailConfigurationBlocked = computed( + () => + props.form.confirmationEmailEnabled + && (emailQuestionCount.value === 0 + || requiresConfirmationEmailQuestionIdSelection.value), + ) - requiresConfirmationEmailQuestionIdSelection(): boolean { - return ( - this.emailQuestionCount > 1 - && !this.selectedConfirmationEmailQuestion - ) - }, + /** + * Datepicker timestamp to string + * + * @param datetime the datepicker Date + * @return + */ + const stringifyDate = (datetime: Date): string => { + const date = moment(datetime).format('LLL') + if (isExpired.value) { + return t('forms', 'Expired on {date}', { date }) + } + return t('forms', 'Expires on {date}', { date }) + } - isConfirmationEmailConfigurationBlocked(): boolean { - return ( - this.form.confirmationEmailEnabled - && (this.emailQuestionCount === 0 - || this.requiresConfirmationEmailQuestionIdSelection) - ) - }, - }, + /** + * Form expires timestamp to Date of the datepicker + * + * @param value the expires timestamp + * @return + */ + const parseTimestampToDate = (value: number): Date => + moment(value, 'X').toDate() - watch: { - 'form.confirmationEmailSubject': { - handler(val: string | null | undefined) { - this.confirmationEmailSubject = val || '' - }, + /** + * Prevent selecting a day before today + * + * @param datetime the datepicker Date + * @return + */ + const notBeforeToday = (datetime: Date): boolean => + datetime < moment().add(-1, 'day').toDate() - immediate: true, - }, + /** + * Prevent selecting a time before the current one + * + * @param datetime the datepicker Date + * @return + */ + const notBeforeNow = (datetime: Date): boolean => + datetime < moment().toDate() - 'form.confirmationEmailBody': { - handler(val: string | null | undefined) { - this.confirmationEmailBody = val || '' + const saveConfirmationEmailQuestionId = ( + selectedQuestionId: number | null, + ): void => { + if (props.form.confirmationEmailQuestionId === selectedQuestionId) { + return + } + emit( + 'update:formProp', + 'confirmationEmailQuestionId', + selectedQuestionId, + ) + } + watch( + () => props.form.confirmationEmailSubject, + (val) => { + confirmationEmailSubject.value = val || '' }, - - immediate: true, - }, - - confirmationEmailQuestions: { - handler() { - const selectedRecipientId = this.form.confirmationEmailQuestionId + { immediate: true }, + ) + watch( + () => props.form.confirmationEmailBody, + (val) => { + confirmationEmailBody.value = val || '' + }, + { immediate: true }, + ) + watch( + confirmationEmailQuestions, + () => { + const selectedRecipientId = props.form.confirmationEmailQuestionId const hasValidSelectedRecipient = selectedRecipientId !== null - && this.confirmationEmailQuestions.some( + && confirmationEmailQuestions.value.some( (question) => question.id === selectedRecipientId, ) if (selectedRecipientId !== null && !hasValidSelectedRecipient) { - if (this.emailQuestionCount === 1) { - this.saveConfirmationEmailQuestionId( - this.confirmationEmailQuestions[0].id, + if (emailQuestionCount.value === 1) { + saveConfirmationEmailQuestionId( + confirmationEmailQuestions.value[0].id, ) } else { - this.saveConfirmationEmailQuestionId(null) + saveConfirmationEmailQuestionId(null) } return } if ( - this.form.confirmationEmailEnabled - && this.emailQuestionCount === 1 - && this.form.confirmationEmailQuestionId === null + props.form.confirmationEmailEnabled + && emailQuestionCount.value === 1 + && props.form.confirmationEmailQuestionId === null ) { - this.saveConfirmationEmailQuestionId( - this.confirmationEmailQuestions[0].id, + saveConfirmationEmailQuestionId( + confirmationEmailQuestions.value[0].id, ) } }, - - deep: true, - }, - }, - - methods: { - confirmationEmailQuestionLabel(question: FormsQuestion): string { - return question.text || t('forms', 'Untitled question') - }, + { deep: true }, + ) /** * Save Form-Properties * * @param checked New Checkbox/Switch Value to use */ - onAnonChange(checked: boolean): void { - this.$emit('update:formProp', 'isAnonymous', checked) - }, - - onSubmitMultipleChange(checked: boolean): void { - this.$emit('update:formProp', 'submitMultiple', checked) - }, - - onAllowEditSubmissionsChange(checked: boolean): void { - this.$emit('update:formProp', 'allowEditSubmissions', checked) - }, - - onAllowCommentsChange(checked: boolean): void { - this.$emit('update:formProp', 'allowComments', checked) - }, - - onFormExpiresChange(checked: boolean): void { + const onAnonChange = (checked: boolean): void => { + emit('update:formProp', 'isAnonymous', checked) + } + const onSubmitMultipleChange = (checked: boolean): void => { + emit('update:formProp', 'submitMultiple', checked) + } + const onAllowEditSubmissionsChange = (checked: boolean): void => { + emit('update:formProp', 'allowEditSubmissions', checked) + } + const onAllowCommentsChange = (checked: boolean): void => { + emit('update:formProp', 'allowComments', checked) + } + const onFormExpiresChange = (checked: boolean): void => { if (checked) { - this.$emit( - 'update:formProp', - 'expires', - moment().add(1, 'hour').unix(), - ) // Expires in one hour. + emit('update:formProp', 'expires', moment().add(1, 'hour').unix()) } else { - this.$emit('update:formProp', 'expires', 0) + emit('update:formProp', 'expires', 0) } - }, - - onShowExpirationChange(checked: boolean): void { - this.$emit('update:formProp', 'showExpiration', checked) - }, + } + const onShowExpirationChange = (checked: boolean): void => { + emit('update:formProp', 'showExpiration', checked) + } /** * On date picker change * * @param datetime the expiration Date */ - onExpirationDateChange(datetime: Date | [Date, Date] | null): void { + const onExpirationDateChange = ( + datetime: Date | [Date, Date] | null, + ): void => { if (!(datetime instanceof Date)) { return } - this.$emit( + emit( 'update:formProp', 'expires', parseInt(moment(datetime).format('X')), ) - }, - - onMaxSubmissionsChange(checked: boolean): void { - this.$emit('update:formProp', 'maxSubmissions', checked ? 1 : null) - }, - - onMaxSubmissionsValueChange(value: string | number): void { + } + const onMaxSubmissionsChange = (checked: boolean): void => { + emit('update:formProp', 'maxSubmissions', checked ? 1 : null) + } + const onMaxSubmissionsValueChange = (value: string | number): void => { const parsedValue = Number(value) if (parsedValue > 0) { - this.$emit('update:formProp', 'maxSubmissions', parsedValue) + emit('update:formProp', 'maxSubmissions', parsedValue) } - }, - - onFormClosedChange(isClosed: boolean): void { - this.$emit( + } + const onFormClosedChange = (isClosed: boolean): void => { + emit( 'update:formProp', 'state', isClosed ? FormState.FormClosed : FormState.FormActive, ) - }, - - onFormLockChange(locked: boolean): void { - this.$emit('update:formProp', 'lockedUntil', locked ? 0 : null) - }, - - onFormArchivedChange(isArchived: boolean): void { - this.$emit( + } + const onFormLockChange = (locked: boolean): void => { + emit('update:formProp', 'lockedUntil', locked ? 0 : null) + } + const onFormArchivedChange = (isArchived: boolean): void => { + emit( 'update:formProp', 'state', isArchived ? FormState.FormArchived : FormState.FormClosed, ) - }, - - onSubmissionMessageChange(event: Event): void { - this.$emit( + } + const onSubmissionMessageChange = (event: Event): void => { + emit( 'update:formProp', 'submissionMessage', (event.target as HTMLTextAreaElement).value, ) - }, + } /** * Enable or disable the whole custom submission message * Disabled means the value is set to null. */ - onUpdateHasCustomSubmissionMessage(): void { - if (this.hasCustomSubmissionMessage) { - this.$emit('update:formProp', 'submissionMessage', null) + const onUpdateHasCustomSubmissionMessage = (): void => { + if (hasCustomSubmissionMessage.value) { + emit('update:formProp', 'submissionMessage', null) } else { - this.$emit('update:formProp', 'submissionMessage', '') + emit('update:formProp', 'submissionMessage', '') } - }, - - onConfirmationEmailEnabledChange(checked: boolean): void { + } + const onConfirmationEmailEnabledChange = (checked: boolean): void => { if ( checked - && this.form.confirmationEmailQuestionId === null - && this.emailQuestionCount === 1 + && props.form.confirmationEmailQuestionId === null + && emailQuestionCount.value === 1 ) { - this.saveConfirmationEmailQuestionId( - this.confirmationEmailQuestions[0].id, + saveConfirmationEmailQuestionId( + confirmationEmailQuestions.value[0].id, ) } - - this.$emit('update:formProp', 'confirmationEmailEnabled', checked) - }, - - onConfirmationEmailSubjectChange(): void { - this.$emit( + emit('update:formProp', 'confirmationEmailEnabled', checked) + } + const onConfirmationEmailSubjectChange = (): void => { + emit( 'update:formProp', 'confirmationEmailSubject', - this.confirmationEmailSubject, + confirmationEmailSubject.value, ) - }, - - onConfirmationEmailBodyChange(): void { - this.$emit( + } + const onConfirmationEmailBodyChange = (): void => { + emit( 'update:formProp', 'confirmationEmailBody', - this.confirmationEmailBody, + confirmationEmailBody.value, ) - }, - - onConfirmationEmailQuestionIdSelectionChange( + } + const onConfirmationEmailQuestionIdSelectionChange = ( option: ConfirmationEmailQuestionOption | null, - ): void { + ): void => { const questionId = option?.id ?? null if (questionId === null) { return } + saveConfirmationEmailQuestionId(questionId) + } - this.saveConfirmationEmailQuestionId(questionId) - }, - - saveConfirmationEmailQuestionId(selectedQuestionId: number | null): void { - if (this.form.confirmationEmailQuestionId === selectedQuestionId) { - return - } - - this.$emit( - 'update:formProp', - 'confirmationEmailQuestionId', - selectedQuestionId, - ) - }, - - /** - * Datepicker timestamp to string - * - * @param datetime the datepicker Date - * @return - */ - stringifyDate(datetime: Date): string { - const date = moment(datetime).format('LLL') - - if (this.isExpired) { - return t('forms', 'Expired on {date}', { date }) - } - return t('forms', 'Expires on {date}', { date }) - }, - - /** - * Form expires timestamp to Date of the datepicker - * - * @param value the expires timestamp - * @return - */ - parseTimestampToDate(value: number): Date { - return moment(value, 'X').toDate() - }, - - /** - * Prevent selecting a day before today - * - * @param datetime the datepicker Date - * @return - */ - notBeforeToday(datetime: Date): boolean { - return datetime < moment().add(-1, 'day').toDate() - }, - - /** - * Prevent selecting a time before the current one - * - * @param datetime the datepicker Date - * @return - */ - notBeforeNow(datetime: Date): boolean { - return datetime < moment().toDate() - }, + return { + t, + SHARE_TYPES, + appConfig, + maxStringLengths, + editMessage, + svgLockOpen, + confirmationEmailSubject, + confirmationEmailBody, + isCurrentUserOwner, + isFormLockedPermanently, + hasCustomSubmissionMessage, + disableSubmitMultiple, + disableSubmitMultipleExplanation, + hasPublicLink, + submitMultiple, + formExpires, + formArchived, + formClosed, + hasMaxSubmissions, + maxSubmissionsValue, + isExpired, + expirationDate, + submissionMessageHTML, + emailBodyPlaceholder, + emailQuestionCount, + confirmationEmailQuestions, + selectedConfirmationEmailQuestion, + selectedConfirmationEmailQuestionId, + confirmationEmailQuestionOptions, + selectedConfirmationEmailQuestionOption, + confirmationEmailErrorText, + confirmationEmailNoteCardType, + requiresConfirmationEmailQuestionIdSelection, + isConfirmationEmailConfigurationBlocked, + confirmationEmailQuestionLabel, + stringifyDate, + parseTimestampToDate, + notBeforeToday, + notBeforeNow, + onAnonChange, + onSubmitMultipleChange, + onAllowEditSubmissionsChange, + onAllowCommentsChange, + onFormExpiresChange, + onShowExpirationChange, + onExpirationDateChange, + onMaxSubmissionsChange, + onMaxSubmissionsValueChange, + onFormClosedChange, + onFormLockChange, + onFormArchivedChange, + onSubmissionMessageChange, + onUpdateHasCustomSubmissionMessage, + onConfirmationEmailEnabledChange, + onConfirmationEmailSubjectChange, + onConfirmationEmailBodyChange, + onConfirmationEmailQuestionIdSelectionChange, + saveConfirmationEmailQuestionId, + } }, }) diff --git a/src/components/SidebarTabs/SharingSearchDiv.vue b/src/components/SidebarTabs/SharingSearchDiv.vue index 3a6389df1..ed187fb20 100644 --- a/src/components/SidebarTabs/SharingSearchDiv.vue +++ b/src/components/SidebarTabs/SharingSearchDiv.vue @@ -23,7 +23,7 @@ diff --git a/src/components/SidebarTabs/SharingShareDiv.vue b/src/components/SidebarTabs/SharingShareDiv.vue index 6b3c4dea4..95cc690b5 100644 --- a/src/components/SidebarTabs/SharingShareDiv.vue +++ b/src/components/SidebarTabs/SharingShareDiv.vue @@ -48,7 +48,7 @@ diff --git a/src/components/SidebarTabs/SharingSidebarTab.vue b/src/components/SidebarTabs/SharingSidebarTab.vue index d4e4eed6c..140dfe706 100644 --- a/src/components/SidebarTabs/SharingSidebarTab.vue +++ b/src/components/SidebarTabs/SharingSidebarTab.vue @@ -46,18 +46,17 @@