From cf0d1d46cc7f4e9596bdf3430f30a64c58faa173 Mon Sep 17 00:00:00 2001 From: Christian Hartmann Date: Fri, 8 May 2026 11:56:03 +0200 Subject: [PATCH 1/2] feat: migrate to TypeScript and update dependencies Signed-off-by: Christian Hartmann --- package-lock.json | 15 ++++---- package.json | 1 + src/main.ts | 23 ++++++++++++ src/mixins/QuestionMultipleMixin.ts | 53 ++++++++++++++++++++-------- src/router.ts | 54 +++++++++++++++++++++++++++++ src/settings.ts | 19 ++++++++++ src/submit.ts | 19 ++++++++++ src/types/vite-modulepreload.d.ts | 6 ++++ tsconfig.json | 5 +-- vite.config.js | 6 ++-- 10 files changed, 174 insertions(+), 27 deletions(-) create mode 100644 src/main.ts create mode 100644 src/router.ts create mode 100644 src/settings.ts create mode 100644 src/submit.ts create mode 100644 src/types/vite-modulepreload.d.ts diff --git a/package-lock.json b/package-lock.json index ccf0d03f6..ea569ced2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@nextcloud/stylelint-config": "^3.2.1", "@nextcloud/vite-config": "^2.5.1", "@playwright/test": "^1.59.1", + "@types/node": "^25.6.1", "@vue/tsconfig": "^0.9.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", @@ -3178,13 +3179,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", - "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "version": "25.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.1.tgz", + "integrity": "sha512-coJCN8O1q4AGyyqCAUSP06P+SrMTu18BkEj3NVAK07q6QUneD2wzj3CLv9+yP+BMeZQlMvneXqqvDe3w+xcq7g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/semver": { @@ -11721,9 +11722,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index aa983a32b..dc0882c1b 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@nextcloud/stylelint-config": "^3.2.1", "@nextcloud/vite-config": "^2.5.1", "@playwright/test": "^1.59.1", + "@types/node": "^25.6.1", "@vue/tsconfig": "^0.9.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 000000000..0da0e0705 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,23 @@ +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate, translatePlural } from '@nextcloud/l10n' +import { createApp } from 'vue' +import Forms from './Forms.vue' +import router from './router.ts' + +import 'vite/modulepreload-polyfill' +import '@nextcloud/dialogs/style.css' + +const app = createApp(Forms) + +app.config.globalProperties.t = translate +app.config.globalProperties.n = translatePlural + +app.use(router) + +app.mount('#content') + +export default app diff --git a/src/mixins/QuestionMultipleMixin.ts b/src/mixins/QuestionMultipleMixin.ts index ae1472497..fdd74fb42 100644 --- a/src/mixins/QuestionMultipleMixin.ts +++ b/src/mixins/QuestionMultipleMixin.ts @@ -15,39 +15,60 @@ import { defineComponent } from 'vue' import { INPUT_DEBOUNCE_MS, OptionType } from '../models/Constants.ts' import logger from '../utils/Logger.js' +// Temporary augmentation to allow using `this.` in mixin methods +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { + options: FormsOption[] + values: any[] + answerType: { validate: (ctx: any) => boolean } + readOnly: boolean + extraSettings?: any + formId: number + id: number + shuffleArray: (arr: T[]) => T[] + $refs: Record + } +} + export default defineComponent({ emits: ['update:options'], data() { return { - dirtyOptionsType: null, + dirtyOptionsType: null as string | null, } }, computed: { - areNoneChecked() { + areNoneChecked(): boolean { return this.values.length === 0 }, - contentValid() { + contentValid(): boolean { return this.answerType.validate(this) }, - isLastEmpty() { - const value = this.options[this.options.length - 1] + isLastEmpty(): boolean { + const value = this.options[this.options.length - 1] as + | FormsOption + | undefined return value?.text?.trim?.().length === 0 }, - expectedOptionTypes() { + expectedOptionTypes(): OptionType[] { return [OptionType.Choice, OptionType.Row, OptionType.Column] }, sortedOptionsPerType(): { [key: string]: FormsOption[] } { - const optionsPerType = Object.fromEntries( - this.expectedOptionTypes.map((optionType) => [optionType, []]), - ) + const optionsPerType: { [key: string]: FormsOption[] } = + Object.fromEntries( + this.expectedOptionTypes.map((optionType) => [ + optionType, + [] as FormsOption[], + ]), + ) - this.options.forEach((option) => { + this.options.forEach((option: FormsOption) => { optionsPerType[option.optionType].push(option) }) @@ -69,14 +90,15 @@ export default defineComponent({ }) if (!this.readOnly) { - // In edit mode append an empty option + // In edit mode append an empty option (cast as FormsOption temporarily) optionsPerType[optionType].push({ + id: 0, local: true, questionId: this.id, text: '', optionType, order: optionsPerType[optionType].length, - }) + } as FormsOption) } } } @@ -104,7 +126,7 @@ export default defineComponent({ */ focusIndex(index: number, optionType: string) { // refs are not guaranteed to be in correct order - we need to find the correct item - const item = this.$refs.input.find((instance) => { + const item = this.$refs.input.find((instance: any) => { return ( instance.$props.optionType === optionType && instance.$props.index === index @@ -143,12 +165,13 @@ export default defineComponent({ return [ ...options, { + id: 0, local: true, questionId: this.id, text: '', optionType, order: options.length, - }, + } as FormsOption, ] } return options @@ -386,7 +409,7 @@ export default defineComponent({ }, watch: { - dirtyOptionsType: debounce(function () { + dirtyOptionsType: debounce(function (this: any) { if (!this.dirtyOptionsType) { return } diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 000000000..6d37b34f8 --- /dev/null +++ b/src/router.ts @@ -0,0 +1,54 @@ +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { RouteRecordRaw } from 'vue-router' + +import { generateUrl } from '@nextcloud/router' +import { createRouter, createWebHistory } from 'vue-router' + +const Create = () => import('./views/Create.vue') +const Results = () => import('./views/Results.vue') +const Submit = () => import('./views/Submit.vue') +const EmptyContent = () => import('./FormsEmptyContent.vue') + +const routes = [ + { + path: '/', + name: 'root', + components: { default: EmptyContent }, + }, + { + path: '/:hash', + redirect: { name: 'submit' }, + name: 'formRoot', + props: true, + }, + { + path: '/:hash/edit', + components: { default: Create }, + name: 'edit', + props: { default: true }, + }, + { + path: '/:hash/results', + components: { default: Results }, + name: 'results', + props: { default: true }, + }, + { + path: '/:hash/submit/:submissionId?', + components: { default: Submit }, + name: 'submit', + props: { default: true }, + }, +] + +const router = createRouter({ + history: createWebHistory(generateUrl('/apps/forms')), + linkActiveClass: 'active', + routes: routes as unknown as RouteRecordRaw[], +}) + +export default router diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 000000000..b45809e5d --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,19 @@ +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate, translatePlural } from '@nextcloud/l10n' +import { createApp } from 'vue' +import FormsSettings from './FormsSettings.vue' + +import 'vite/modulepreload-polyfill' + +const app = createApp(FormsSettings) + +app.config.globalProperties.t = translate +app.config.globalProperties.n = translatePlural + +app.mount('#forms-settings') + +export default app diff --git a/src/submit.ts b/src/submit.ts new file mode 100644 index 000000000..7062a6ee3 --- /dev/null +++ b/src/submit.ts @@ -0,0 +1,19 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { translate, translatePlural } from '@nextcloud/l10n' +import { createApp } from 'vue' +import FormsSubmitRoot from './FormsSubmit.vue' + +import 'vite/modulepreload-polyfill' + +const app = createApp(FormsSubmitRoot) + +app.config.globalProperties.t = translate +app.config.globalProperties.n = translatePlural + +app.mount('#content') + +export default app diff --git a/src/types/vite-modulepreload.d.ts b/src/types/vite-modulepreload.d.ts new file mode 100644 index 000000000..a9f0f88af --- /dev/null +++ b/src/types/vite-modulepreload.d.ts @@ -0,0 +1,6 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare module 'vite/modulepreload-polyfill' diff --git a/tsconfig.json b/tsconfig.json index a2f6f9707..6794d7586 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,11 +5,12 @@ { "extends": "@vue/tsconfig", - "include": ["src/**.ts", "*.ts"], + "include": ["src/**/*", "*.ts"], "compilerOptions": { "allowJs": true, "target": "ESNext", "module": "ESNext", - "moduleResolution": "Bundler" + "moduleResolution": "Bundler", + "types": ["vite/client", "node"] } } diff --git a/vite.config.js b/vite.config.js index f454bf373..cd410faa2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,9 +9,9 @@ import { join, resolve } from 'path' export default createAppConfig( { emptyContent: resolve(join('src', 'emptyContent.js')), - main: resolve(join('src', 'main.js')), - submit: resolve(join('src', 'submit.js')), - settings: resolve(join('src', 'settings.js')), + main: resolve(join('src', 'main.ts')), + submit: resolve(join('src', 'submit.ts')), + settings: resolve(join('src', 'settings.ts')), }, { config: { From 8ef4c6f18a203318569a3f96f747dc021561dcc1 Mon Sep 17 00:00:00 2001 From: Christian Hartmann Date: Fri, 8 May 2026 22:39:28 +0200 Subject: [PATCH 2/2] refactor: migrate utility files and components to TypeScript Signed-off-by: Christian Hartmann --- src/Forms.vue | 4 +- src/FormsSettings.vue | 2 +- src/components/AppNavigationForm.vue | 2 +- src/components/QRDialog.vue | 2 +- src/components/Questions/AnswerInput.vue | 4 +- src/components/Questions/QuestionFile.vue | 4 +- src/components/Questions/QuestionShort.vue | 2 +- .../SidebarTabs/SharingSidebarTab.vue | 4 +- .../SidebarTabs/TransferOwnership.vue | 2 +- src/components/TopBar.vue | 2 +- src/{emptyContent.js => emptyContent.ts} | 2 + src/main.js | 23 --------- src/mixins/QuestionMixin.js | 4 +- src/mixins/QuestionMultipleMixin.ts | 14 +++--- src/mixins/ShareLinkMixin.js | 2 +- src/mixins/UserSearchMixin.js | 4 +- src/mixins/ViewsMixin.js | 6 +-- src/router.js | 48 ------------------- src/settings.js | 19 -------- src/submit.js | 19 -------- ...celableRequest.js => CancelableRequest.ts} | 16 +++---- src/utils/{Logger.js => Logger.ts} | 0 ...csResponse2Data.js => OcsResponse2Data.ts} | 8 ++-- ...ularExpression.js => RegularExpression.ts} | 16 +++---- src/utils/SetWindowTitle.js | 20 -------- src/utils/SetWindowTitle.ts | 20 ++++++++ src/views/Create.vue | 6 +-- src/views/Results.vue | 6 +-- src/views/Submit.vue | 6 +-- vite.config.js | 2 +- 30 files changed, 78 insertions(+), 191 deletions(-) rename src/{emptyContent.js => emptyContent.ts} (95%) delete mode 100644 src/main.js delete mode 100644 src/router.js delete mode 100644 src/settings.js delete mode 100644 src/submit.js rename src/utils/{CancelableRequest.js => CancelableRequest.ts} (58%) rename src/utils/{Logger.js => Logger.ts} (100%) rename src/utils/{OcsResponse2Data.js => OcsResponse2Data.ts} (58%) rename src/utils/{RegularExpression.js => RegularExpression.ts} (70%) delete mode 100644 src/utils/SetWindowTitle.js create mode 100644 src/utils/SetWindowTitle.ts diff --git a/src/Forms.vue b/src/Forms.vue index af83b7e7e..9a096239a 100644 --- a/src/Forms.vue +++ b/src/Forms.vue @@ -168,8 +168,8 @@ import FormsIcon from './components/Icons/FormsIcon.vue' import Sidebar from './views/Sidebar.vue' import PermissionTypes from './mixins/PermissionTypes.js' import { FormState } from './models/Constants.ts' -import logger from './utils/Logger.js' -import OcsResponse2Data from './utils/OcsResponse2Data.js' +import logger from './utils/Logger.ts' +import OcsResponse2Data from './utils/OcsResponse2Data.ts' const appName = 'forms' diff --git a/src/FormsSettings.vue b/src/FormsSettings.vue index a25158ffb..e9da16bf0 100644 --- a/src/FormsSettings.vue +++ b/src/FormsSettings.vue @@ -105,7 +105,7 @@ import NcInputField from '@nextcloud/vue/components/NcInputField' import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import NcSelect from '@nextcloud/vue/components/NcSelect' import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' -import logger from './utils/Logger.js' +import logger from './utils/Logger.ts' export default { name: 'FormsSettings', diff --git a/src/components/AppNavigationForm.vue b/src/components/AppNavigationForm.vue index 31c52352f..35ec1ceaf 100644 --- a/src/components/AppNavigationForm.vue +++ b/src/components/AppNavigationForm.vue @@ -115,7 +115,7 @@ import IconDelete from 'vue-material-design-icons/TrashCanOutline.vue' import FormsIcon from './Icons/FormsIcon.vue' import PermissionTypes from '../mixins/PermissionTypes.js' import { FormState } from '../models/Constants.ts' -import logger from '../utils/Logger.js' +import logger from '../utils/Logger.ts' export default { name: 'AppNavigationForm', diff --git a/src/components/QRDialog.vue b/src/components/QRDialog.vue index a5325f899..66e691d92 100644 --- a/src/components/QRDialog.vue +++ b/src/components/QRDialog.vue @@ -26,7 +26,7 @@