diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 85% rename from .eslintrc.js rename to .eslintrc.cjs index 01274e10..7ac57269 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -1,3 +1,6 @@ +/* eslint-env node */ +require("@rushstack/eslint-patch/modern-module-resolution") + module.exports = { 'root': true, 'env': { @@ -9,7 +12,7 @@ module.exports = { 'extends': [ 'eslint:recommended', 'plugin:vue/vue3-essential', - '@vue/typescript', + '@vue/eslint-config-typescript/recommended', ], 'rules': { 'vue/html-quotes': [ @@ -28,7 +31,6 @@ module.exports = { 'error', 'never', ], - 'vue/script-setup-uses-vars': 'error', // see https://segmentfault.com/q/1010000040813116/a-1020000041134455 (original in chinese) 'no-unused-vars': 'off', @@ -40,6 +42,7 @@ module.exports = { 'parserOptions': { 'parser': '@typescript-eslint/parser', 'ecmaVersion': 2022, + 'sourceType': 'module', }, 'ignorePatterns': [ '*.test.*', diff --git a/package.json b/package.json index 54edf829..40cc0f95 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@cypress/vite-dev-server": "3.2.0", "@cypress/vue": "4.2.0", "@faker-js/faker": "7.5.0", + "@rushstack/eslint-patch": "^1.2.0", "@types/dompurify": "2.3.4", "@types/flexsearch": "0.7.3", "@types/marked": "4.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eac2e769..fa10bd51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ specifiers: '@fortawesome/vue-fontawesome': 3.0.1 '@github/hotkey': 2.0.1 '@kyvg/vue3-notification': 2.4.1 + '@rushstack/eslint-patch': ^1.2.0 '@sentry/tracing': 7.14.1 '@sentry/vue': 7.14.1 '@types/dompurify': 2.3.4 @@ -133,6 +134,7 @@ devDependencies: '@cypress/vite-dev-server': 3.2.0 '@cypress/vue': 4.2.0_cypress@10.9.0+vue@3.2.40 '@faker-js/faker': 7.5.0 + '@rushstack/eslint-patch': 1.2.0 '@types/dompurify': 2.3.4 '@types/flexsearch': 0.7.3 '@types/marked': 4.0.7 @@ -2667,6 +2669,10 @@ packages: rollup: 2.79.1 dev: true + /@rushstack/eslint-patch/1.2.0: + resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} + dev: true + /@samverschueren/stream-to-observable/0.3.1_rxjs@6.6.7: resolution: {integrity: sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==} engines: {node: '>=6'} diff --git a/src/components/list/partials/filters.vue b/src/components/list/partials/filters.vue index f02d5a07..0c886b98 100644 --- a/src/components/list/partials/filters.vue +++ b/src/components/list/partials/filters.vue @@ -578,7 +578,7 @@ export default defineComponent({ return } - let ids = [] + const ids = [] this[kind].forEach(u => { ids.push(kind === 'users' ? u.username : u.id) }) @@ -613,7 +613,7 @@ export default defineComponent({ return } - let labelIDs = [] + const labelIDs = [] this.labels.forEach(u => { labelIDs.push(u.id) }) diff --git a/src/components/misc/ready.vue b/src/components/misc/ready.vue index 2d869b94..d9493c04 100644 --- a/src/components/misc/ready.vue +++ b/src/components/misc/ready.vue @@ -75,8 +75,8 @@ async function load() { await router.push(redirectTo) } ready.value = true - } catch (e: any) { - error.value = e + } catch (e: unknown) { + error.value = String(e) } } diff --git a/src/components/tasks/add-task.vue b/src/components/tasks/add-task.vue index cb8f515e..84d1fcc1 100644 --- a/src/components/tasks/add-task.vue +++ b/src/components/tasks/add-task.vue @@ -214,7 +214,7 @@ async function addTask() { return rel }) await Promise.all(relations) - } catch (e: any) { + } catch (e: { message?: string }) { newTaskTitle.value = taskTitleBackup if (e?.message === 'NO_LIST') { errorMessage.value = t('list.create.addListRequired') diff --git a/src/components/tasks/gantt-component.vue b/src/components/tasks/gantt-component.vue index 4955da44..596dae83 100644 --- a/src/components/tasks/gantt-component.vue +++ b/src/components/tasks/gantt-component.vue @@ -278,13 +278,13 @@ export default defineComponent({ prepareGanttDays() { console.debug('prepareGanttDays; start date: ', this.startDate, 'end date:', this.endDate) // Layout: years => [months => [days]] - let years = {} + const years = {} for ( let d = this.startDate; d <= this.endDate; d.setDate(d.getDate() + 1) ) { - let date = new Date(d) + const date = new Date(d) if (years[date.getFullYear() + ''] === undefined) { years[date.getFullYear() + ''] = {} } @@ -353,7 +353,7 @@ export default defineComponent({ const didntHaveDates = newTask.startDate === null ? true : false - let startDate = new Date(this.startDate) + const startDate = new Date(this.startDate) startDate.setDate( startDate.getDate() + newRect.left / this.dayWidth, ) @@ -362,7 +362,7 @@ export default defineComponent({ startDate.setUTCSeconds(0) startDate.setUTCMilliseconds(0) newTask.startDate = startDate - let endDate = new Date(startDate) + const endDate = new Date(startDate) endDate.setDate( startDate.getDate() + newRect.width / this.dayWidth, ) @@ -430,7 +430,7 @@ export default defineComponent({ if (!this.newTaskFieldActive) { return } - let task = new TaskModel({ + const task = new TaskModel({ title: this.newTaskTitle, listId: this.listId, }) diff --git a/src/components/tasks/partials/relatedTasks.vue b/src/components/tasks/partials/relatedTasks.vue index 3d76ebfd..83e9505d 100644 --- a/src/components/tasks/partials/relatedTasks.vue +++ b/src/components/tasks/partials/relatedTasks.vue @@ -348,7 +348,7 @@ async function toggleTaskDone(task: ITask) { // Find the task in the list and update it so that it is correctly strike through Object.entries(relatedTasks.value).some(([kind, tasks]) => { - return tasks.some((t, key) => { + return (tasks as ITask[]).some((t, key) => { const found = t.id === task.id if (found) { relatedTasks.value[kind as IRelationKind]![key] = task diff --git a/src/directives/focus.ts b/src/directives/focus.ts index 10a820dd..52e6d843 100644 --- a/src/directives/focus.ts +++ b/src/directives/focus.ts @@ -1,6 +1,8 @@ -export default { +import type {Directive} from 'vue' + +const focus = >{ // When the bound element is inserted into the DOM... - mounted: (el, {modifiers}) => { + mounted(el, {modifiers}) { // Focus the element only if the viewport is big enough // auto focusing elements on mobile can be annoying since in these cases the // keyboard always pops up and takes half of the available space on the screen. @@ -10,3 +12,5 @@ export default { } }, } + +export default focus \ No newline at end of file diff --git a/src/helpers/attachments.ts b/src/helpers/attachments.ts index ce51c203..c8d86f51 100644 --- a/src/helpers/attachments.ts +++ b/src/helpers/attachments.ts @@ -4,7 +4,7 @@ import type {IAttachment} from '@/modelTypes/IAttachment' import AttachmentService from '@/services/attachment' import {useTaskStore} from '@/stores/tasks' -export function uploadFile(taskId: number, file: File, onSuccess: (url: string) => void) { +export function uploadFile(taskId: number, file: File, onSuccess?: (url: string) => void) { const attachmentService = new AttachmentService() const files = [file] @@ -15,7 +15,7 @@ export async function uploadFiles( attachmentService: AttachmentService, taskId: number, files: File[] | FileList, - onSuccess: Function = () => {}, + onSuccess?: (attachmentUrl: string) => void, ) { const attachmentModel = new AttachmentModel({taskId}) const response = await attachmentService.create(attachmentModel, files) @@ -26,7 +26,7 @@ export async function uploadFiles( taskId, attachment, }) - onSuccess(generateAttachmentUrl(taskId, attachment.id)) + onSuccess?.(generateAttachmentUrl(taskId, attachment.id)) }) if (response.errors !== null) { diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts index 54e1cdb4..878ec443 100644 --- a/src/helpers/auth.ts +++ b/src/helpers/auth.ts @@ -45,7 +45,6 @@ export async function refreshToken(persist: boolean): Promise { return response } catch(e) { - // @ts-ignore throw new Error('Error renewing token: ', { cause: e }) } } diff --git a/src/helpers/calculateItemPosition.ts b/src/helpers/calculateItemPosition.ts index a181adca..9cea6e51 100644 --- a/src/helpers/calculateItemPosition.ts +++ b/src/helpers/calculateItemPosition.ts @@ -1,19 +1,18 @@ export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => { - if (positionBefore === null && positionAfter === null) { - return 0 - } - - // If there is no task before, our task is the first task in which case we let it have half of the position of the task after it - if (positionBefore === null && positionAfter !== null) { + if (positionBefore === null) { + if (positionAfter === null) { + return 0 + } + + // If there is no task after it, we just add 2^16 to the last position to have enough room in the future return positionAfter / 2 } // If there is no task after it, we just add 2^16 to the last position to have enough room in the future - if (positionBefore !== null && positionAfter === null) { + if (positionAfter === null) { return positionBefore + Math.pow(2, 16) } // If we have both a task before and after it, we acually calculate the position - // @ts-ignore - can never be null but TS does not seem to understand that return positionBefore + (positionAfter - positionBefore) / 2 } \ No newline at end of file diff --git a/src/helpers/objectIsEmpty.ts b/src/helpers/objectIsEmpty.ts index e51e2c4f..30cebc36 100644 --- a/src/helpers/objectIsEmpty.ts +++ b/src/helpers/objectIsEmpty.ts @@ -1,5 +1,5 @@ // https://stackoverflow.com/a/32108184/10924593 -export function objectIsEmpty(obj: any): boolean { +export function objectIsEmpty(obj: Record): boolean { return obj && Object.keys(obj).length === 0 && Object.getPrototypeOf(obj) === Object.prototype diff --git a/src/helpers/redirectToProvider.ts b/src/helpers/redirectToProvider.ts index fec8a1eb..d5ca9f35 100644 --- a/src/helpers/redirectToProvider.ts +++ b/src/helpers/redirectToProvider.ts @@ -3,7 +3,7 @@ import {parseURL} from 'ufo' import {createRandomID} from '@/helpers/randomId' import type {IProvider} from '@/types/IProvider' -export const redirectToProvider = (provider: IProvider, redirectUrl: string = '') => { +export const redirectToProvider = (provider: IProvider, redirectUrl = '') => { // We're not using the redirect url provided by the server to allow redirects when using the electron app. // The implications are not quite clear yet hence the logic to pass in another redirect url still exists. diff --git a/src/helpers/time/parseDate.ts b/src/helpers/time/parseDate.ts index cc8e2f76..ffab0aa6 100644 --- a/src/helpers/time/parseDate.ts +++ b/src/helpers/time/parseDate.ts @@ -125,16 +125,16 @@ const addTimeToDate = (text: string, date: Date, previousMatch: string | null): } export const getDateFromText = (text: string, now: Date = new Date()) => { - const fullDateRegex: RegExp = / ([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig + const fullDateRegex = / ([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig // 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021 let results: string[] | null = fullDateRegex.exec(text) let result: string | null = results === null ? null : results[0] let foundText: string | null = result - let containsYear: boolean = true + let containsYear = true if (result === null) { // 2. Try parsing the date as something like "jan 21" or "21 jan" - const monthRegex: RegExp = new RegExp(` (${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig') + const monthRegex = new RegExp(` (${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig') results = monthRegex.exec(text) result = results === null ? null : `${results[0]} ${now.getFullYear()}`.trim() foundText = results === null ? '' : results[0].trim() @@ -142,7 +142,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => { if (result === null) { // 3. Try parsing the date as "27/01" or "01/27" - const monthNumericRegex: RegExp = / ([0-9][0-9]?\/[0-9][0-9]?)/ig + const monthNumericRegex = / ([0-9][0-9]?\/[0-9][0-9]?)/ig results = monthNumericRegex.exec(text) // Put the year before or after the date, depending on what works @@ -229,7 +229,7 @@ export const getDateFromTextIn = (text: string, now: Date = new Date()) => { } const getDateFromWeekday = (text: string): dateFoundResult => { - const matcher: RegExp = / (next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g + const matcher = / (next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work. if (results === null) { return { @@ -240,7 +240,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => { const date: Date = new Date() const currentDay: number = date.getDay() - let day: number = 0 + let day = 0 switch (results[2]) { case 'mon': diff --git a/src/helpers/time/parseDateOrString.ts b/src/helpers/time/parseDateOrString.ts index 1aeeb115..13945570 100644 --- a/src/helpers/time/parseDateOrString.ts +++ b/src/helpers/time/parseDateOrString.ts @@ -1,12 +1,11 @@ -export function parseDateOrString(rawValue: string | undefined, fallback: any): string | Date { +export function parseDateOrString(rawValue: string | undefined, fallback: unknown) { if (typeof rawValue === 'undefined') { return fallback } const d = new Date(rawValue) - // @ts-ignore if rawValue is an invalid date, isNan will return false. - return !isNaN(d) + return !isNaN(+d) ? d : rawValue } diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index e03c7f94..e9000d4b 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -15,7 +15,7 @@ export function isNil(value: unknown) { return value == null } -export function omitBy(obj: {}, check: (value: unknown) => boolean) { +export function omitBy(obj: Record, check: (value: unknown) => boolean) { if (isNil(obj)) { return {} } diff --git a/src/indexes/index.ts b/src/indexes/index.ts index 14709cad..3de83dea 100644 --- a/src/indexes/index.ts +++ b/src/indexes/index.ts @@ -31,15 +31,14 @@ export const createNewIndexer = (name: string, fieldsToIndex: string[]) => { return index.update(item.id, item) } - function search(query: string | null): number[] | null { + function search(query: string | null) { if (query === '' || query === null) { return null } - // @ts-ignore return index.search(query) ?.flatMap(r => r.result) - .filter((value, index, self) => self.indexOf(value) === index) + .filter((value, index, self) => self.indexOf(value) === index) as number[] || null } diff --git a/src/modelTypes/IList.ts b/src/modelTypes/IList.ts index c51633b5..9e0c84e9 100644 --- a/src/modelTypes/IList.ts +++ b/src/modelTypes/IList.ts @@ -15,7 +15,7 @@ export interface IList extends IAbstract { isArchived: boolean hexColor: string identifier: string - backgroundInformation: any // FIXME: improve type + backgroundInformation: unknown | null // FIXME: improve type isFavorite: boolean subscription: ISubscription position: number diff --git a/src/models/list.ts b/src/models/list.ts index a9b834d5..57f233cb 100644 --- a/src/models/list.ts +++ b/src/models/list.ts @@ -19,7 +19,7 @@ export default class ListModel extends AbstractModel implements IList { isArchived = false hexColor = '' identifier = '' - backgroundInformation: any = null + backgroundInformation: unknown | null = null isFavorite = false subscription: ISubscription = null position = 0 diff --git a/src/services/abstractService.ts b/src/services/abstractService.ts index 11aede34..a2e7563f 100644 --- a/src/services/abstractService.ts +++ b/src/services/abstractService.ts @@ -149,7 +149,7 @@ export default abstract class AbstractService) : string { const replacements = this.getRouteReplacements(path, pathparams) return Object.entries(replacements).reduce( (result, [parameter, value]) => result.replace(parameter, value as string), diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue index beb9f454..345fa654 100644 --- a/src/views/tasks/TaskDetailView.vue +++ b/src/views/tasks/TaskDetailView.vue @@ -562,8 +562,8 @@ const hasAttachments = computed(() => attachmentStore.attachments.length > 0) // HACK: const shouldShowClosePopup = computed(() => (route.name as string).includes('kanban')) -function attachmentUpload(...args: any[]) { - return uploadFile(taskId.value, ...args) +function attachmentUpload(file: File, onSuccess?: (url: string) => void) { + return uploadFile(taskId.value, file, onSuccess) } const heading = ref(null)