Compare commits

..
Author SHA1 Message Date
Dominik Pschenitschni fb6c96f6c6 wip2 2025-01-17 15:00:30 +01:00
Dominik Pschenitschni b4875a839a wip: try out to fix beforeAuth issue 2025-01-17 15:00:30 +01:00
Dominik Pschenitschni 4fa3389232 wip 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 96586ad193 feat: unify project.view & project.index 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 6741b1b6eb fix: 404 route 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni a057523314 fix: settings 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 37e8cba630 feat: re-add route names, meta and props via definePage 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 6828b6a10e fix: merge project.create with project.createFromParent 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 2501c07bfb feat: move migrators to helpers 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni ecbb16c63e feat: move useGanttFilters and useGanttTaskList to composables 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni cb0c1d90eb feat: ShowTasks 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni f29eb76318 feat: register and share routes 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 05aeba483e feat: move to file-based routes 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni c0651b9927 feat: eslint 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 0efa70c5ee feat: router hot update 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni beb2e1f9f9 feat: install unplugin-vue-router 2025-01-17 15:00:29 +01:00
Dominik Pschenitschni 356e83a6c9 feat: inline dynamic component definitions in routes 2025-01-17 14:59:59 +01:00
Dominik Pschenitschni 47538ca810 fix: method name 2025-01-17 13:59:12 +01:00
kolaente abae6f05e0 fix(auth): move read all notifications to notification group
Resolves
https://kolaente.dev/vikunja/vikunja/issues/2977
2025-01-17 12:52:42 +01:00
kolaente 45ec1a4c47 fix(auth): make sure routes from the "other" group work as intended
Resolves
https://kolaente.dev/vikunja/vikunja/issues/2977
2025-01-17 11:06:00 +01:00
kolaente 77d1616fea docs: adjust frontend readme
Resolves https://kolaente.dev/vikunja/vikunja/issues/3012
2025-01-17 10:41:45 +01:00
Dominik Pschenitschniandkonrad b5cb98498a fix: global component types
See canonical source https://github.com/vuejs/language-tools/wiki/Global-Component-Types
This was kind of hard to find, imho should be in vue docs itself…
2025-01-17 08:09:44 +00:00
Dominik Pschenitschniandkonrad 70e027a84e feat: withDefaults for RelatedTasks 2025-01-17 07:56:57 +00:00
Dominik Pschenitschniandkonrad 289bb73e9e feat: withDefaults for Flatpickr 2025-01-17 07:55:44 +00:00
Dominik Pschenitschniandkonrad 8bada3e967 fix: git ignore all dist folders 2025-01-17 07:53:54 +00:00
renovate 1863b06d0c chore(deps): update dev-dependencies 2025-01-17 00:07:27 +00:00
renovate d630586f63 chore(deps): update dependency go to v1.23.5 2025-01-16 21:07:59 +00:00
renovateandkonrad 13002d77f5 fix(deps): update module github.com/threedotslabs/watermill to v1.4.4 2025-01-16 15:38:43 +00:00
Dominik Pschenitschni a61e2d064d fix: lowlight imports for v3 2025-01-16 13:14:09 +01:00
renovateandDominik Pschenitschni e1f78462e5 fix(deps): update dependency lowlight to v3 2025-01-16 13:05:24 +01:00
128 changed files with 818 additions and 1673 deletions
+6 -1
View File
@@ -30,5 +30,10 @@
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.keystyle": "nested"
"i18n-ally.keystyle": "nested",
// unplugin-vue-router
// https://uvr.esm.is/introduction.html#setup
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": ["vue-router/auto$"]
}
+2
View File
@@ -11,6 +11,8 @@ stats.html
node_modules
.DS_Store
dist
dist-dev
dist-test
coverage
*.zip
.vite/
+2 -18
View File
@@ -2,30 +2,14 @@
> The todo app to organize your life.
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/vikunja/status.svg)](https://drone.kolaente.de/vikunja/vikunja)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.22.1-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js.
Take a look at [our roadmap](https://my.vikunja.cloud/share/UrdhKPqumxDXUbYpEGJLSIyNTwAnbBzVlwdDpRbv/auth) (hosted on Vikunja!) for a list of things we're currently working on!
## Security Reports
If you find any security-related issues you don't want to disclose publicly, please use [the contact information on our website](https://vikunja.io/contact/#security).
## Docker
There is a [docker image available](https://hub.docker.com/r/vikunja/vikunja) with support for http/2 and aggressive caching enabled.
In order to build it from sources run the command below. (Docker >= v19.03)
```shell
export DOCKER_BUILDKIT=1
docker build -t vikunja/frontend .
```
Refer to [multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for different platforms.
For general information about the project, refer to the top-level readme of this repo.
## Project setup
@@ -36,7 +20,7 @@ pnpm install
### Compiles and hot-reloads for development
```shell
pnpm run serve
pnpm run dev
```
### Compiles and minifies for production
+1
View File
@@ -2,6 +2,7 @@
/// <reference types="vite-svg-loader" />
/// <reference types="cypress" />
/// <reference types="@histoire/plugin-vue/components" />
/// <reference types="unplugin-vue-router/client" />
interface ImportMetaEnv {
readonly VIKUNJA_API_URL?: string
+10 -1
View File
@@ -84,5 +84,14 @@ export default [
// 'cypress/*',
// ],
},
{
// unplugin-vue-router
// https://uvr.esm.is/guide/eslint.html#eslint
settings: {
'import/core-modules': ['vue-router/auto-routes'],
},
globals: {
'definePage': 'readonly',
},
},
]
+5 -7
View File
@@ -93,14 +93,14 @@
"floating-vue": "5.2.2",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lowlight": "2.9.0",
"lowlight": "3.3.0",
"pinia": "2.3.0",
"register-service-worker": "1.7.2",
"sortablejs": "1.15.6",
"tailwindcss": "3.4.17",
"tippy.js": "6.3.7",
"ufo": "1.5.4",
"validator": "13.12.0",
"unplugin-vue-router": "0.10.9",
"vue": "3.5.13",
"vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5",
@@ -108,8 +108,7 @@
"vue-router": "4.5.0",
"vuemoji-picker": "0.3.2",
"workbox-precaching": "7.3.0",
"zhyswan-vuedraggable": "4.1.3",
"zod": "3.24.1"
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.5",
@@ -124,7 +123,6 @@
"@types/is-touch-device": "1.0.3",
"@types/node": "22.10.7",
"@types/sortablejs": "1.15.8",
"@types/validator": "13.12.2",
"@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.20.0",
"@vitejs/plugin-vue": "5.2.1",
@@ -135,7 +133,7 @@
"browserslist": "4.24.4",
"caniuse-lite": "1.0.30001692",
"csstype": "3.1.3",
"cypress": "13.17.0",
"cypress": "14.0.0",
"esbuild": "0.24.2",
"eslint": "9.18.0",
"eslint-plugin-vue": "9.32.0",
@@ -155,7 +153,7 @@
"vite-plugin-sentry": "1.4.0",
"vite-plugin-vue-devtools": "7.7.0",
"vite-svg-loader": "5.1.0",
"vitest": "2.1.8",
"vitest": "3.0.1",
"vue-tsc": "2.2.0",
"wait-on": "8.0.2",
"workbox-cli": "7.3.0"
+343 -197
View File
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

+1 -1
View File
@@ -37,7 +37,7 @@
v-slot="{ Component }"
:route="routeWithModal"
>
<keep-alive :include="['project.view']">
<keep-alive :include="['project']">
<component :is="Component" />
</keep-alive>
</RouterView>
@@ -15,7 +15,7 @@
/>
</BaseButton>
<BaseButton
:to="{ name: 'project.index', params: { projectId: project.id} }"
:to="{ name: 'project', params: { projectId: project.id} }"
class="list-menu-link"
:class="{'router-link-exact-active': currentProject?.id === project.id}"
>
@@ -167,7 +167,7 @@ import {Node} from '@tiptap/pm/model'
import Commands from './commands'
import suggestionSetup from './suggestion'
import {lowlight} from 'lowlight'
import {common, createLowlight} from 'lowlight'
import type {BottomAction, UploadCallback} from './types'
import type {ITask} from '@/modelTypes/ITask'
@@ -343,7 +343,7 @@ const extensions : Extensions = [
}),
CodeBlockLowlight.configure({
lowlight,
lowlight: createLowlight(common),
}),
HardBreak.extend({
addKeyboardShortcuts() {
@@ -62,29 +62,22 @@ export default {inheritAttrs: false}
</script>
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue'
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect} from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number, Date, Array] as PropType<DateOption | DateOption[] | null>,
default: null,
},
// https://flatpickr.js.org/options/
config: {
type: Object as PropType<Options>,
default: () => ({
defaultDate: null,
wrap: false,
}),
},
events: {
type: Array as PropType<HookKey[]>,
default: () => includedEvents,
},
disabled: {
type: Boolean,
default: false,
},
const props = withDefaults(defineProps<{
modelValue: DateOption | DateOption[] | null,
/** https://flatpickr.js.org/options/ */
config: Options,
events: HookKey[],
disabled: boolean,
}>(), {
modelValue: null,
config: () => ({
defaultDate: undefined,
wrap: false,
}),
events: () => includedEvents,
disabled: false,
})
const emit = defineEmits([
@@ -62,7 +62,7 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
},
{
title: 'project.kanban.title',
available: (route) => route.name === 'project.view',
available: (route) => route.name === 'project',
shortcuts: [
{
title: 'keyboardShortcuts.task.done',
@@ -103,7 +103,7 @@
{{ $t('project.webhooks.title') }}
</DropdownItem>
<DropdownItem
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
:to="{ name: 'project.create', params: { parentProjectId: project.id } }"
icon="layer-group"
>
{{ $t('menu.createProject') }}
@@ -20,7 +20,7 @@
:key="view.id"
class="switch-view-button"
:class="{'is-active': view.id === viewId}"
:to="{ name: 'project.view', params: { projectId, viewId: view.id } }"
:to="{ name: 'project', params: { projectId, viewId: view.id } }"
>
{{ getViewTitle(view) }}
</BaseButton>
@@ -42,7 +42,7 @@
</template>
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {computed, ref, shallowReactive, watch, watchEffect} from 'vue'
import {useRoute} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
@@ -72,7 +72,7 @@ const {t} = useI18n()
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const projectService = ref(new ProjectService())
const projectService = shallowReactive(new ProjectService())
const loadedProjectId = ref(0)
const currentProject = computed<IProject>(() => {
@@ -96,8 +96,7 @@ watch(
() => props.projectId,
// loadProject
async (projectIdToLoad: number) => {
const projectData = {id: projectIdToLoad}
saveProjectToHistory(projectData)
saveProjectToHistory({id: projectIdToLoad})
// Don't load the project if we either already loaded it or aren't dealing with a project at all currently and
// the currently loaded project has the right set.
@@ -117,15 +116,15 @@ watch(
// Set the current project to the one we're about to load so that the title is already shown at the top
loadedProjectId.value = 0
const projectFromStore = projectStore.projects[projectData.id]
const projectFromStore = projectStore.projects[projectIdToLoad]
if (projectFromStore) {
baseStore.handleSetCurrentProject({project: projectFromStore, currentProjectViewId: props.viewId})
}
// We create an extra project object instead of creating it in project.value because that would trigger a ui update which would result in bad ux.
const project = new ProjectModel(projectData)
const project = new ProjectModel({id: projectIdToLoad})
try {
const loadedProject = await projectService.value.get(project)
const loadedProject = await projectService.get(project)
baseStore.handleSetCurrentProject({project: loadedProject, currentProjectViewId: props.viewId})
} finally {
loadedProjectId.value = projectIdToLoad
@@ -134,13 +133,7 @@ watch(
{immediate: true},
)
watch(
() => props.viewId,
() => {
baseStore.setCurrentProjectViewId(props.viewId)
},
{immediate: true},
)
watchEffect(() => baseStore.setCurrentProjectViewId(props.viewId))
function getViewTitle(view: IProjectView) {
switch (view.title) {
@@ -37,7 +37,7 @@
:aria-label="project.title"
:title="textOnlyDescription"
:to="{
name: 'project.index',
name: 'project',
params: { projectId: project.id}
}"
/>
@@ -85,7 +85,7 @@ import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import TaskForm from '@/components/tasks/TaskForm.vue'
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
import {useGanttFilters} from '../../../views/project/helpers/useGanttFilters'
import {useGanttFilters} from '@/composables/useGanttFilters'
import {RIGHTS} from '@/constants/rights'
import type {DateISO} from '@/types/DateISO'
@@ -449,7 +449,7 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
case ACTION_TYPE.PROJECT:
closeQuickActions()
await router.push({
name: 'project.index',
name: 'project',
params: {projectId: (item as DoAction<IProject>).id},
})
break
+1 -1
View File
@@ -54,7 +54,7 @@ import {parseKebabDate} from '@/helpers/time/parseKebabDate'
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
import type {DateISO} from '@/types/DateISO'
import type {GanttFilters} from '@/views/project/helpers/useGanttFilters'
import type {GanttFilters} from '@/composables/useGanttFilters'
import {
extendDayjs,
@@ -183,7 +183,7 @@
</template>
<script setup lang="ts">
import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue'
import {ref, reactive, shallowReactive, watch, computed} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRoute} from 'vue-router'
@@ -206,27 +206,17 @@ import {useTaskStore} from '@/stores/tasks'
import {useProjectStore} from '@/stores/projects'
import {playPopSound} from '@/helpers/playPop'
const props = defineProps({
taskId: {
type: Number,
required: true,
},
initialRelatedTasks: {
type: Object as PropType<ITask['relatedTasks']>,
default: () => ({}),
},
showNoRelationsNotice: {
type: Boolean,
default: false,
},
projectId: {
type: Number,
default: 0,
},
editEnabled: {
type: Boolean,
default: true,
},
const props = withDefaults(defineProps<{
taskId: number,
initialRelatedTasks?: ITask['relatedTasks'],
showNoRelationsNotice?: boolean,
projectId: number,
editEnabled: boolean,
}>(), {
initialRelatedTasks: () => ({}),
showNoRelationsNotice: false,
projectId: 0,
editEnabled: true, // this seems like a mistake
})
const taskStore = useTaskStore()
@@ -28,7 +28,7 @@
<RouterLink
v-if="showProject && typeof project !== 'undefined'"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
:to="{ name: 'project', params: { projectId: task.projectId } }"
class="task-project mr-1"
:class="{'mr-2': task.hexColor !== ''}"
@click.stop
@@ -139,7 +139,7 @@
<RouterLink
v-if="showProjectSeparately"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
:to="{ name: 'project', params: { projectId: task.projectId } }"
class="task-project"
@click.stop
>
@@ -5,7 +5,7 @@ import {isoToKebabDate} from '@/helpers/time/isoToKebabDate'
import {parseDateProp} from '@/helpers/time/parseDateProp'
import {parseBooleanProp} from '@/helpers/time/parseBooleanProp'
import {useRouteFilters} from '@/composables/useRouteFilters'
import {useGanttTaskList} from './useGanttTaskList'
import {useGanttTaskList} from '@/composables/useGanttTaskList'
import type {IProject} from '@/modelTypes/IProject'
import type {TaskFilterParams} from '@/services/taskCollection'
@@ -72,7 +72,7 @@ function ganttFiltersToRoute(filters: GanttFilters): RouteLocationRaw {
}
return {
name: 'project.view',
name: 'project',
params: {
projectId: filters.projectId,
viewId: filters.viewId,
@@ -104,7 +104,7 @@ export function useGanttFilters(route: Ref<RouteLocationNormalized>, viewId: Ref
ganttGetDefaultFilters,
ganttRouteToFilters,
ganttFiltersToRoute,
['project.view'],
['project'],
)
const {
@@ -72,7 +72,7 @@ export function useRouteWithModal() {
}
const newRoute = {
name: 'project.view',
name: 'project',
params: {
projectId: baseStore.currentProject?.id,
viewId,
@@ -1,9 +1,9 @@
import wunderlistIcon from './icons/wunderlist.jpg'
import todoistIcon from './icons/todoist.svg?url'
import trelloIcon from './icons/trello.svg?url'
import microsoftTodoIcon from './icons/microsoft-todo.svg?url'
import vikunjaFileIcon from './icons/vikunja-file.png?url'
import tickTickIcon from './icons/ticktick.svg?url'
import wunderlistIcon from '@/assets/migrators/wunderlist.jpg'
import todoistIcon from '@/assets/migrators/todoist.svg?url'
import trelloIcon from '@/assets/migrators/trello.svg?url'
import microsoftTodoIcon from '@/assets/migrators/microsoft-todo.svg?url'
import vikunjaFileIcon from '@/assets/migrators/vikunja-file.png?url'
import tickTickIcon from '@/assets/migrators/ticktick.svg?url'
export interface Migrator {
id: string
+17 -17
View File
@@ -73,26 +73,26 @@ setLanguage(browserLanguage).then(() => {
error(err)
}
if (import.meta.env.DEV) {
app.config.warnHandler = (msg) => {
error(msg)
throw msg
}
// if (import.meta.env.DEV) {
// app.config.warnHandler = (msg) => {
// error(msg)
// throw msg
// }
// https://stackoverflow.com/a/52076738/15522256
window.addEventListener('error', (err) => {
error(err)
throw err
})
// // https://stackoverflow.com/a/52076738/15522256
// window.addEventListener('error', (err) => {
// error(err)
// throw err
// })
window.addEventListener('unhandledrejection', (err) => {
// event.promise contains the promise object
// event.reason contains the reason for the rejection
error(err)
throw err
})
}
// window.addEventListener('unhandledrejection', (err) => {
// // event.promise contains the promise object
// // event.reason contains the reason for the rejection
// error(err)
// throw err
// })
// }
app.config.globalProperties.$message = {
error,
-13
View File
@@ -1,13 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
export const LabelTaskSchema = AbstractSchema.extend({
id: IdSchema.nullable(),
taskId: IdSchema.nullable(),
labelId: IdSchema.nullable(),
})
export type LabelTask = TypeOf<typeof LabelTaskSchema>
-10
View File
@@ -1,10 +0,0 @@
import type {TypeOf} from 'zod'
import {object, nativeEnum} from 'zod'
import {RIGHTS} from '@/constants/rights'
export const AbstractSchema = object({
maxRight: nativeEnum(RIGHTS).nullable(),
})
export type IAbstract = TypeOf<typeof AbstractSchema>
-19
View File
@@ -1,19 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {FileSchema} from './file'
export const AttachmentSchema = AbstractSchema.extend({
id: IdSchema.default(0),
taskId: IdSchema.default(0), // iTaskSchema.shape.id
createdBy: UserSchema,
file: FileSchema,
created: DateSchema.nullable(),
})
export type IAttachment = TypeOf<typeof AttachmentSchema>
-18
View File
@@ -1,18 +0,0 @@
import type {TypeOf} from 'zod'
import {z, string, object} from 'zod'
export const AVATAR_PROVIDER = [
'default',
'initials',
'gravatar',
'marble',
'upload',
] as const
export const AvatarProviderSchema = z.enum(AVATAR_PROVIDER)
export type IAvatarProvider = TypeOf<typeof AvatarProviderSchema>
export const AvatarSchema = object({
// FIXME: shouldn't the default be 'default'?
avatarProvider: string().or(AvatarProviderSchema).default(''),
})
export type IAvatar = TypeOf<typeof AvatarSchema>
@@ -1,19 +0,0 @@
import type {TypeOf} from 'zod'
import {object, record, string, unknown} from 'zod'
import {IdSchema} from './common/id'
export const BackgroundImageSchema = object({
id: IdSchema.default(0),
url: string().url().default(''),
thumb: string().default(''),
// FIXME: not sure if this needs to defined, since it seems provider specific
// {
// author: string(),
// authorName: string(),
// }
info: record(unknown()).default({}),
blurHash: string().default(''),
})
export type BackgroundImage = TypeOf<typeof BackgroundImageSchema>
-26
View File
@@ -1,26 +0,0 @@
import type {TypeOf} from 'zod'
import {number, array, boolean} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {TextFieldSchema} from './common/textField'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {TaskSchema} from './task'
export const BucketSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: TextFieldSchema,
listId: IdSchema.default(0),
limit: number().default(0),
tasks: array(TaskSchema).default([]),
isDoneBucket: boolean().default(false),
position: number().default(0),
createdBy: UserSchema.nullable(),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type IBucket = TypeOf<typeof BucketSchema>
-14
View File
@@ -1,14 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
export const CaldavTokenSchema = AbstractSchema.extend({
id: IdSchema,
created: DateSchema,
})
export type CaldavToken = TypeOf<typeof CaldavTokenSchema>
@@ -1,22 +0,0 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
export const RELATION_KIND = {
'SUBTASK': 'subtask',
'PARENTTASK': 'parenttask',
'RELATED': 'related',
'DUPLICATES': 'duplicates',
'BLOCKING': 'blocking',
'BLOCKED': 'blocked',
'PROCEDES': 'precedes',
'FOLLOWS': 'follows',
'COPIEDFROM': 'copiedfrom',
'COPIEDTO': 'copiedto',
} as const
export const RELATION_KINDS = [...Object.values(RELATION_KIND)] as const
export const RelationKindSchema = nativeEnum(RELATION_KIND)
export type IRelationKind = TypeOf<typeof RelationKindSchema>
-11
View File
@@ -1,11 +0,0 @@
import {preprocess, date} from 'zod'
export const DateSchema = preprocess((arg) => {
if (
// FIXME: Add comment why we check for `0001`
typeof arg == 'string' && !arg.startsWith('0001') ||
arg instanceof Date
) {
return new Date(arg)
}
}, date())
-80
View File
@@ -1,80 +0,0 @@
import type {TypeOf} from 'zod'
import {z, nativeEnum, array, boolean, object, number} from 'zod'
export enum SORT_BY {
ID = 'id',
DONE = 'done',
TITLE = 'title',
PRIORITY = 'priority',
DONE_AT = 'done_at',
DUE_DATE = 'due_date',
START_DATE = 'start_date',
END_DATE = 'end_date',
PERCENT_DONE = 'percent_done',
CREATED = 'created',
UPDATED = 'updated',
POSITION = 'position',
KANBAN_POSITION = 'kanban_position',
}
export enum ORDER_BY {
ASC = 'asc',
DESC = 'desc',
NONE = 'none',
}
export enum FILTER_BY {
DONE = 'done',
DUE_DATE = 'due_date',
START_DATE = 'start_date',
END_DATE = 'end_date',
NAMESPACE = 'namespace',
ASSIGNEES = 'assignees',
LIST_ID = 'list_id',
BUCKET_ID = 'bucket_id',
PRIORITY = 'priority',
PERCENT_DONE = 'percent_done',
LABELS = 'labels',
UNDEFINED = 'undefined', // FIXME: Why do we have a value that is undefined as string?
}
export enum FILTER_COMPARATOR {
EQUALS = 'equals',
LESS = 'less',
GREATER = 'greater',
GREATER_EQUALS = 'greater_equals',
LESS_EQUALS = 'less_equals',
IN = 'in',
}
export enum FILTER_CONCAT {
AND = 'and',
OR = 'or',
IN = 'in',
}
const TASKS_PER_BUCKET = 25
export const FilterSchema = object({
sortBy: array(nativeEnum(SORT_BY)).default([SORT_BY.DONE, SORT_BY.ID]), // FIXME: create from taskSchema,
// fixme default order seem so also be `desc`
// see line from ListTable:
// if (typeof order === 'undefined' || order === 'none') {
orderBy: array(nativeEnum(ORDER_BY)).default([ORDER_BY.ASC, ORDER_BY.DESC]),
// FIXME: create from taskSchema
filterBy: array(nativeEnum(FILTER_BY)).default([FILTER_BY.DONE]),
// FIXME: create from taskSchema
// FIXME: might need to preprocess values, e.g. date.
// see line from 'filters.vue':
// params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
filterValue: array(z.enum(['false'])).default(['false']),
// FIXME: is `in` value correct?
// found in `quick-actions.vue`:
// params.filter_comparator.push('in')
filterComparator: array(nativeEnum(FILTER_COMPARATOR)).default([FILTER_COMPARATOR.EQUALS]),
filterConcat: z.nativeEnum(FILTER_CONCAT).default(FILTER_CONCAT.AND),
filterIncludeNulls: boolean().default(true),
perPage: number().default(TASKS_PER_BUCKET), // FIXME: is perPage is just available for the bucket endpoint?
})
export type IFilter = TypeOf<typeof FilterSchema>
@@ -1,10 +0,0 @@
import {string} from 'zod'
import isHexColor from 'validator/lib/isHexColor'
export const HexColorSchema = string().transform(
(value) => {
if (!value || value.startsWith('#')) {
return value
}
return '#' + value
}).refine(value => isHexColor(value))
-6
View File
@@ -1,6 +0,0 @@
import {number, preprocess} from 'zod'
export const IdSchema = preprocess(
(value: unknown) => Number(value),
number().positive().int(),
)
+14 -18
View File
@@ -1,26 +1,24 @@
import type {TypeOf} from 'zod'
import {nativeEnum, number, object, preprocess} from 'zod'
import {SECONDS_A_HOUR} from '@/constants/date'
import {REPEAT_TYPES, type IRepeatAfter} from '@/types/IRepeatAfter'
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
import { nativeEnum, number, object, preprocess } from 'zod'
/**
* Parses `repeatAfterSeconds` into a usable js object.
*/
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
// if its dividable by SECONDS_A_DAY, its something with days, otherwise hours
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
repeatAfter = {type: REPEAT_TYPES.HOURS, amount: repeatAfterSeconds / SECONDS_A_HOUR}
} else if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: REPEAT_TYPES.WEEKS, amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type: REPEAT_TYPES.MONTHS, amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: REPEAT_TYPES.YEARS, amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: REPEAT_TYPES.DAYS, amount: repeatAfterSeconds / SECONDS_A_DAY}
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
}
return repeatAfter
}
@@ -39,6 +37,4 @@ export const RepeatsSchema = preprocess(
type: nativeEnum(REPEAT_TYPES),
amount: number().int(),
}),
)
export type RepeatAfter = TypeOf<typeof RepeatsSchema>
)
@@ -1,3 +0,0 @@
import {string} from 'zod'
export const TextFieldSchema = string().transform((value) => value.trim()).default('')
-11
View File
@@ -1,11 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {AbstractSchema} from './abstract'
export const EmailUpdateSchema = AbstractSchema.extend({
newEmail: string().email().default(''),
password: string().default(''),
})
export type EmailUpdate = TypeOf<typeof EmailUpdateSchema>
-15
View File
@@ -1,15 +0,0 @@
import type {TypeOf} from 'zod'
import {object, number, string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
export const FileSchema = object({
id: IdSchema.default(0),
mime: string().default(''),
name: string().default(''),
size: number().default(0),
created: DateSchema.nullable(),
})
export type File = TypeOf<typeof FileSchema>
-33
View File
@@ -1,33 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {HexColorSchema} from './common/hexColor'
import {UserSchema} from './user'
import {AbstractSchema} from './abstract'
import {colorIsDark} from '@/helpers/color/colorIsDark'
const DEFAULT_LABEL_BACKGROUND_COLOR = 'e8e8e8'
export const LabelSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: string().default(''),
hexColor: HexColorSchema.default(DEFAULT_LABEL_BACKGROUND_COLOR),
textColor: string(), // implicit
description: string().default(''),
createdBy: UserSchema, // FIXME: default: current user?
listId: IdSchema.default(0),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
}).transform((obj) => {
// FIXME: remove textColor location => should be defined in UI
obj.textColor = colorIsDark(obj.hexColor) ? '#4a4a4a' : '#ffffff'
return obj
},
)
export type ILabel = TypeOf<typeof LabelSchema>
-26
View File
@@ -1,26 +0,0 @@
import type {TypeOf} from 'zod'
import {number, string, nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {RIGHTS} from '@/constants/rights'
export const LinkShareSchema = AbstractSchema.extend({
id: IdSchema.default(0),
hash: string().default(''),
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
sharedBy: UserSchema,
sharingType: number().default(0), // FIXME: use correct numbers
listId: IdSchema.default(0),
name: string().default(''),
password: string().default(''),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type LinkShare = TypeOf<typeof LinkShareSchema>
-32
View File
@@ -1,32 +0,0 @@
import type {TypeOf} from 'zod'
import {boolean, number, string, array, any} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {SubscriptionSchema} from './subscription'
import {TaskSchema} from './task'
import {UserSchema} from './user'
export const ListSchema = AbstractSchema.extend({
id: IdSchema.default(0),
hash: string().default(''),
description: string().default(''),
owner: UserSchema,
tasks: array(TaskSchema),
namespaceId: IdSchema.default(0), // INamespace['id'],
isArchived: boolean().default(false),
hexColor: string().default(''),
identifier: string().default(''),
backgroundInformation: any().nullable().default(null), // FIXME: what is this for?
isFavorite: boolean().default(false),
subscription: SubscriptionSchema.nullable(),
position: number().default(0),
backgroundBlurHash: string().default(''),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type List = TypeOf<typeof ListSchema>
@@ -1,14 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
import {ListSchema} from './list'
export const ListDuplicationSchema = AbstractSchema.extend({
listId: IdSchema.default(0),
namespaceId: IdSchema.default(0), // INamespace['id'],
list: ListSchema,
})
export type ListDuplication = TypeOf<typeof ListDuplicationSchema>
-26
View File
@@ -1,26 +0,0 @@
import type {TypeOf} from 'zod'
import {boolean, string, array} from 'zod'
import {IdSchema} from './common/id'
import {HexColorSchema} from './common/hexColor'
import {AbstractSchema} from './abstract'
import {ListSchema} from './list'
import {UserSchema} from './user'
import {SubscriptionSchema} from './subscription'
export const NamespaceSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: string().default(''),
description: string().default(''),
owner: UserSchema,
lists: array(ListSchema),
isArchived: boolean().default(false),
hexColor: HexColorSchema.default(''),
subscription: SubscriptionSchema.nullable(),
created: IdSchema.nullable(),
updated: IdSchema.nullable(),
})
export type Namespace = TypeOf<typeof NamespaceSchema>
-54
View File
@@ -1,54 +0,0 @@
import type {TypeOf} from 'zod'
import {union, boolean, object, string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {TaskSchema} from './task'
import {TaskCommentSchema} from './taskComment'
import {TeamSchema} from './team'
import {UserSchema} from './user'
const NotificationTypeSchema = object({
doer: UserSchema,
})
const NotificationTypeTask = NotificationTypeSchema.extend({
task: TaskSchema,
comment: TaskCommentSchema,
})
const NotificationTypeAssigned = NotificationTypeSchema.extend({
task: TaskSchema,
assignee: UserSchema,
})
const NotificationTypeDeleted = NotificationTypeSchema.extend({
task: TaskSchema,
})
const NotificationTypeCreated = NotificationTypeSchema.extend({
task: TaskSchema,
})
const NotificationTypeMemberAdded = NotificationTypeSchema.extend({
member: UserSchema,
team: TeamSchema,
})
export const NotificationSchema = AbstractSchema.extend({
id: IdSchema.default(0),
name: string().default(''),
notification: union([
NotificationTypeTask,
NotificationTypeAssigned,
NotificationTypeDeleted,
NotificationTypeCreated,
NotificationTypeMemberAdded,
]),
read: boolean().default(false),
readAt: DateSchema.nullable(),
})
export type Notification = TypeOf<typeof NotificationSchema>
-13
View File
@@ -1,13 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {AbstractSchema} from './abstract'
// FIXME: is it correct that this extends the Abstract Schema?
export const PasswordResetSchema = AbstractSchema.extend({
token: string().default(''),
newPassword: string().default(''),
email: string().email().default(''),
})
export type PasswordReset = TypeOf<typeof PasswordResetSchema>
@@ -1,15 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {AbstractSchema} from './abstract'
// FIXME: is it correct that this extends the Abstract Schema?
export const PasswordUpdateSchema = AbstractSchema.extend({
newPassword: string().default(''),
oldPassword: string().default(''),
}).refine((data) => data.newPassword === data.oldPassword, {
message: 'Passwords don\'t match',
path: ['confirm'], // path of error
})
export type PasswordUpdate = TypeOf<typeof PasswordUpdateSchema>
-23
View File
@@ -1,23 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {FilterSchema} from './common/filter'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
// FIXME: is it correct that this extends the Abstract Schema?
export const SavedFilterSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: string().default(''),
description: string().default(''),
filters: FilterSchema,
owner: UserSchema,
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type SavedFilter = TypeOf<typeof SavedFilterSchema>
-19
View File
@@ -1,19 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {DateSchema} from './common/date'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
export const SubscriptionSchema = AbstractSchema.extend({
id: IdSchema.default(0),
entity: string().default(''), // FIXME: correct type?
entityId: IdSchema.default(0), // FIXME: correct type?
user: UserSchema,
created: DateSchema.nullable(),
})
export type Subscription = TypeOf<typeof SubscriptionSchema>
-119
View File
@@ -1,119 +0,0 @@
import type {ZodType, TypeOf} from 'zod'
import {nativeEnum, boolean, number, string, array, record, unknown, lazy} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {HexColorSchema} from './common/hexColor'
import {TextFieldSchema} from './common/textField'
import {RelationKindSchema} from './common/RelationKind'
import {RepeatsSchema} from './common/repeats'
import {AbstractSchema} from './abstract'
import {AttachmentSchema} from './attachment'
import {LabelSchema} from './label'
import {SubscriptionSchema} from './subscription'
import {UserSchema} from './user'
import {PRIORITIES} from '@/constants/priorities'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
const LabelsSchema = array(LabelSchema)
.transform((labels) => labels.sort((f, s) => f.title > s.title ? 1 : -1)) // FIXME: use
.default([])
export type ILabels = TypeOf<typeof LabelsSchema>
const RelatedTasksSchema = record(RelationKindSchema, record(string(), unknown()))
export type IRelatedTasksSchema = TypeOf<typeof RelatedTasksSchema>
const RelatedTasksLazySchema : ZodType<Task['relatedTasks']> = lazy(() =>
record(RelationKindSchema, TaskSchema),
)
export type IRelatedTasksLazySchema = TypeOf<typeof RelatedTasksLazySchema>
// export interface ITask extends IAbstract {
// id: number
// title: string
// description: string
// done: boolean
// doneAt: Date | null
// priority: Priority
// labels: ILabel[]
// assignees: IUser[]
// dueDate: Date | null
// startDate: Date | null
// endDate: Date | null
// repeatAfter: number | IRepeatAfter
// repeatFromCurrentDate: boolean
// repeatMode: IRepeatMode
// reminderDates: Date[]
// parentTaskId: ITask['id']
// hexColor: string
// percentDone: number
// relatedTasks: Partial<Record<IRelationKind, ITask>>,
// attachments: IAttachment[]
// identifier: string
// index: number
// isFavorite: boolean
// subscription: ISubscription
// position: number
// kanbanPosition: number
// createdBy: IUser
// created: Date
// updated: Date
// listId: IList['id'] // Meta, only used when creating a new task
// bucketId: IBucket['id']
// }
export const TaskSchema = AbstractSchema.extend({
id: IdSchema.default(0),
title: TextFieldSchema,
description: TextFieldSchema,
done: boolean().default(false),
doneAt: DateSchema.nullable().default(null),
priority: nativeEnum(PRIORITIES).default(PRIORITIES.UNSET),
labels: LabelsSchema,
assignees: array(UserSchema).default([]),
dueDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
startDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
endDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
repeatAfter: RepeatsSchema, // FIXME: default value is `0`. Shouldn't this be `null`?
repeatFromCurrentDate: boolean().default(false),
repeatMode: nativeEnum(TASK_REPEAT_MODES).default(TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT),
// TODO: schedule notifications
// FIXME: triggered notificaitons not supported anymore / remove feature?
reminderDates: array(DateSchema).default([]),
parentTaskId: IdSchema.default(0), // shouldn't this have `null` as default?
hexColor: HexColorSchema.default(''),
percentDone: number().default(0),
relatedTasks: RelatedTasksSchema.default({}),
attachments: array(AttachmentSchema).default([]),
identifier: string().default(''),
index: number().default(0),
isFavorite: boolean().default(false),
subscription: SubscriptionSchema.nullable().default(null),
position: number().default(0),
kanbanPosition: number().default(0),
createdBy: UserSchema,
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
listId: IdSchema.default(0), //IList['id'], // Meta, only used when creating a new task
bucketId: IdSchema.default(0), // IBucket['id'],
}).transform((obj) => {
if (obj.identifier === `-${obj.index}`) {
obj.identifier = ''
}
return obj
})
export type Task = TypeOf<typeof TaskSchema>
-14
View File
@@ -1,14 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
export const TaskAssigneeSchema = AbstractSchema.extend({
created: DateSchema.nullable(),
userId: IdSchema.default(0), // IUser['id']
taskId: IdSchema.default(0), // ITask['id']
})
export type TaskAssignee = TypeOf<typeof TaskAssigneeSchema>
-20
View File
@@ -1,20 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
export const TaskCommentSchema = AbstractSchema.extend({
id: IdSchema.default(0),
taskId: IdSchema.default(0),
comment: string().default(''),
author: UserSchema,
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type TaskComment = TypeOf<typeof TaskCommentSchema>
-24
View File
@@ -1,24 +0,0 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {RELATION_KIND} from '@/types/IRelationKind'
export const TaskRelationSchema = AbstractSchema.extend({
id: IdSchema.default(0),
otherTaskId: IdSchema.default(0),
taskId: IdSchema.default(0),
relationKind: nativeEnum(RELATION_KIND).nullable().default(null), // FIXME: default value was empty string?
createdBy: UserSchema,
// FIXME: shouldn't the empty value of dates be `new Date()`
// Because e.g. : `new Date(null)` => Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
created: DateSchema.nullable().default(null),
})
export type ITaskRelation = TypeOf<typeof TaskRelationSchema>
-25
View File
@@ -1,25 +0,0 @@
import type {TypeOf} from 'zod'
import {array, nativeEnum, string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSchema} from './user'
import {TeamMemberSchema} from './teamMember'
import {RIGHTS} from '@/constants/rights'
export const TeamSchema = AbstractSchema.extend({
id: IdSchema.default(0),
name: string().default(''),
description: string().default(''),
members: array(TeamMemberSchema),
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
createdBy: UserSchema, // FIXME: default was {},
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type Team = TypeOf<typeof TeamSchema>
-11
View File
@@ -1,11 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {TeamShareBaseSchema} from './teamShareBase'
export const TeamListSchema = TeamShareBaseSchema.extend({
listId: IdSchema.default(0), // IList['id']
})
export type TeamList = TypeOf<typeof TeamListSchema>
-13
View File
@@ -1,13 +0,0 @@
import type {TypeOf} from 'zod'
import {boolean} from 'zod'
import {IdSchema} from './common/id'
import {UserSchema} from './user'
export const TeamMemberSchema = UserSchema.extend({
admin: boolean().default(false),
teamId: IdSchema.default(0), // IList['id']
})
export type TeamMember = TypeOf<typeof TeamMemberSchema>
-11
View File
@@ -1,11 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {TeamShareBaseSchema} from './teamShareBase'
export const TeamNamespaceSchema = TeamShareBaseSchema.extend({
namespaceId: IdSchema.default(0), // INamespace['id']
})
export type ITeamNamespace = TypeOf<typeof TeamNamespaceSchema>
-19
View File
@@ -1,19 +0,0 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {RIGHTS} from '@/constants/rights'
export const TeamShareBaseSchema = AbstractSchema.extend({
teamId: IdSchema.default(0), // ITeam['id']
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type ITeamShareBase = TypeOf<typeof TeamShareBaseSchema>
-9
View File
@@ -1,9 +0,0 @@
import type {TypeOf} from 'zod'
import {object, string} from 'zod'
export const TokenSchema = object({
token: string(),
})
export type IToken = TypeOf<typeof TokenSchema>
-12
View File
@@ -1,12 +0,0 @@
import type {TypeOf} from 'zod'
import {string, boolean} from 'zod'
import {AbstractSchema} from './abstract'
export const TotpSchema = AbstractSchema.extend({
secret: string().default(''),
enabled: boolean().default(false),
url: string().url().default(''),
})
export type Totp = TypeOf<typeof TotpSchema>
-21
View File
@@ -1,21 +0,0 @@
import type {TypeOf} from 'zod'
import {string} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {UserSettingsSchema} from './userSettings'
export const UserSchema = AbstractSchema.extend({
id: IdSchema.default(0),
email: string().email().default(''),
username: string().default(''),
name: string().default(''),
settings: UserSettingsSchema.nullable(),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type User = TypeOf<typeof UserSchema>
-11
View File
@@ -1,11 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {UserShareBaseSchema} from './userShareBase'
export const UserListSchema = UserShareBaseSchema.extend({
listId: IdSchema.default(0), // IList['id']
})
export type IUserList = TypeOf<typeof UserListSchema>
-11
View File
@@ -1,11 +0,0 @@
import type {TypeOf} from 'zod'
import {IdSchema} from './common/id'
import {UserShareBaseSchema} from './userShareBase'
export const UserNamespaceSchema = UserShareBaseSchema.extend({
namespaceId: IdSchema.default(0), // INamespace['id']
})
export type IUserNamespace = TypeOf<typeof UserNamespaceSchema>
-29
View File
@@ -1,29 +0,0 @@
import type {TypeOf} from 'zod'
import {boolean, string, undefined, nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {AbstractSchema} from './abstract'
const WEEKDAYS = {
MONDAY: 0,
TUESDAY: 1,
WEDNESDAY: 2,
THURSDAY: 3,
FRIDAY: 4,
SATURDAY: 5,
SUNDAY: 6,
} as const
export const UserSettingsSchema = AbstractSchema.extend({
name: string().default(''),
emailRemindersEnabled: boolean().default(true),
discoverableByName: boolean().default(false),
discoverableByEmail: boolean().default(false),
overdueTasksRemindersEnabled: boolean().default(true),
defaultListId: IdSchema.or(undefined()), // iListSchema['id'] // FIXME: shouldn't this be `null`?
weekStart: nativeEnum(WEEKDAYS).default(WEEKDAYS.MONDAY),
timezone: string().default(''),
})
export type IUserSettings = TypeOf<typeof UserSettingsSchema>
-19
View File
@@ -1,19 +0,0 @@
import type {TypeOf} from 'zod'
import {nativeEnum} from 'zod'
import {IdSchema} from './common/id'
import {DateSchema} from './common/date'
import {AbstractSchema} from './abstract'
import {RIGHTS} from '@/constants/rights'
export const UserShareBaseSchema = AbstractSchema.extend({
userId: IdSchema, // FIXME: default of model is `''`
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
created: DateSchema.nullable(),
updated: DateSchema.nullable(),
})
export type TeamMember = TypeOf<typeof UserShareBaseSchema>
+1 -8
View File
@@ -1,13 +1,6 @@
import type {IAbstract} from './IAbstract'
export const AVATAR_PROVIDER = [
'default',
'initials',
'gravatar',
'marble',
'upload',
] as const
export type AvatarProvider = typeof AVATAR_PROVIDER[number]
export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload'
export interface IAvatar extends IAbstract {
avatarProvider: AvatarProvider
@@ -8,5 +8,13 @@
<script setup lang="ts">
import {useTitle} from '@/composables/useTitle'
// regarding the name of this component, see
// https://github.com/posva/unplugin-vue-router/issues/412#issuecomment-2150643118
definePage({
name: 'not-found',
})
defineOptions({ name: 'NotFound' })
useTitle(() => '404')
</script>
@@ -71,6 +71,11 @@ import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset'
import Message from '@/components/misc/Message.vue'
definePage({
name: 'user.password-reset.request',
meta: { title: 'user.auth.resetPassword' },
})
const passwordResetService = shallowReactive(new PasswordResetService())
const passwordReset = ref(new PasswordResetModel())
const errorMsg = ref('')
@@ -161,6 +161,11 @@ import {useConfigStore} from '@/stores/config'
import {useTitle} from '@/composables/useTitle'
definePage({
name: 'user.login',
meta: { title: 'user.auth.login' },
})
const {t} = useI18n({useScope: 'global'})
useTitle(() => t('user.auth.login'))
@@ -58,6 +58,11 @@ import PasswordResetService from '@/services/passwordReset'
import Message from '@/components/misc/Message.vue'
import Password from '@/components/input/Password.vue'
definePage({
name: 'user.password-reset.reset',
meta: { title: 'user.auth.resetPassword' },
})
const credentials = reactive({
password: '',
})
@@ -32,6 +32,8 @@ import {computed} from 'vue'
import {useConfigStore} from '@/stores/config'
definePage({ name: 'about' })
const configStore = useConfigStore()
const apiVersion = computed(() => configStore.version)
</script>
@@ -34,6 +34,8 @@ import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
import {useAuthStore} from '@/stores/auth'
definePage({ name: 'openid.auth' })
const {t} = useI18n({useScope: 'global'})
const route = useRoute()
@@ -88,6 +88,11 @@ import Filters from '@/components/project/partials/Filters.vue'
import {useSavedFilter} from '@/services/savedFilter'
definePage({
name: 'filters.create',
meta: { showAsModal: true }
})
const {
filter,
filters,
@@ -58,7 +58,7 @@
import {ref, computed} from 'vue'
import Message from '@/components/misc/Message.vue'
import ShowTasks from '@/views/tasks/ShowTasks.vue'
import ShowTasks from '@/components/tasks/ShowTasks.vue'
import ProjectCardGrid from '@/components/project/partials/ProjectCardGrid.vue'
import AddTask from '@/components/tasks/AddTask.vue'
@@ -73,6 +73,8 @@ import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
definePage({ name: 'home' })
const salutation = useDaytimeSalutation()
const baseStore = useBaseStore()
@@ -149,6 +149,8 @@ import {useLabelStore} from '@/stores/labels'
import { useTitle } from '@/composables/useTitle'
definePage({ name: 'labels.index' })
const {t} = useI18n({useScope: 'global'})
const labelEditLabel = ref<ILabel>(new LabelModel())
@@ -54,6 +54,8 @@ import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {getRandomColorHex} from '@/helpers/color/randomColor'
definePage({ name: 'labels.create' })
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
@@ -110,16 +110,6 @@
</div>
</template>
<script lang="ts">
export default {
beforeRouteEnter(to) {
if (MIGRATORS[to.params.service as string] === undefined) {
return {name: 'not-found'}
}
},
}
</script>
<script setup lang="ts">
import {computed, ref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
@@ -133,11 +123,24 @@ import AbstractMigrationFileService from '@/services/migrator/abstractMigrationF
import {formatDateLong} from '@/helpers/time/formatDate'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {MIGRATORS, type Migrator} from './migrators'
import {MIGRATORS, type Migrator} from '@/helpers/migrators'
import {useTitle} from '@/composables/useTitle'
import {useProjectStore} from '@/stores/projects'
import {getErrorText} from '@/message'
definePage({
name: 'migrate.service',
props: route => ({
service: route.params.service as string,
code: route.query.code as string,
}),
beforeEnter(to) {
if (MIGRATORS[to.params.service as string] === undefined) {
return {name: 'not-found'}
}
}
})
const props = defineProps<{
service: string,
code?: string,
@@ -65,6 +65,14 @@ import {useProjectStore} from '@/stores/projects'
import ProjectSearch from '@/components/tasks/partials/ProjectSearch.vue'
import type {IProject} from '@/modelTypes/IProject'
definePage({
name: 'project.create',
props: route => ({ parentProjectId: Number(route.params.parentProjectId as string) }),
meta: {
showAsModal: true,
},
})
const props = defineProps<{
parentProjectId?: number,
}>()
@@ -1,8 +1,8 @@
<script setup lang="ts">
import {computed, watch} from 'vue'
import {computed, watch, watchEffect} from 'vue'
import {useProjectStore} from '@/stores/projects'
import {useRoute, useRouter} from 'vue-router'
import {saveProjectView} from '@/helpers/projectView'
import {getProjectViewId, saveProjectView} from '@/helpers/projectView'
import ProjectList from '@/components/project/views/ProjectList.vue'
import ProjectGantt from '@/components/project/views/ProjectGantt.vue'
@@ -11,6 +11,35 @@ import ProjectKanban from '@/components/project/views/ProjectKanban.vue'
import {useAuthStore} from '@/stores/auth'
import {DEFAULT_PROJECT_VIEW_SETTINGS} from '@/modelTypes/IProjectView'
definePage({
name: 'project',
// beforeEnter(to) {
// if (to.name !== 'project') {
// throw new Error()
// }
// const projectId = Number(to.params?.projectId)
// const viewIdFromRoute = Number(to.params?.viewId)
// const newViewid = getProjectViewId(projectId) ?? 0
// if (!viewIdFromRoute || viewIdFromRoute !== newViewid) {
// return {
// name: 'project',
// replace: true,
// params: {
// projectId,
// viewId: newViewid,
// },
// }
// }
// },
props: route => ({
projectId: parseInt(route.params.projectId as string),
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
}),
})
const props = defineProps<{
projectId: number,
viewId: number,
@@ -27,23 +56,24 @@ const currentView = computed(() => {
})
function redirectToDefaultViewIfNecessary() {
if (props.viewId === 0 || !projectStore.projects[props.projectId]?.views.find(v => v.id === props.viewId)) {
if (props.viewId === 0 || !currentView.value) {
// Ideally, we would do that in the router redirect, but the projects (and therefore, the views)
// are not always loaded then.
let view
if (authStore.settings.frontendSettings.defaultView !== DEFAULT_PROJECT_VIEW_SETTINGS.FIRST) {
view = projectStore.projects[props.projectId]?.views.find(v => v.viewKind === authStore.settings.frontendSettings.defaultView)
const defaultView = authStore.settings.frontendSettings.defaultView
if (defaultView !== DEFAULT_PROJECT_VIEW_SETTINGS.FIRST) {
view = currentProject.value?.views.find(v => v.viewKind === defaultView)
}
// Use the first view as fallback if the default view is not available
if (view === undefined && projectStore.projects[props.projectId]?.views?.length > 0) {
view = projectStore.projects[props.projectId]?.views[0]
if (view === undefined && currentProject.value?.views?.length > 0) {
view = currentProject.value?.views[0]
}
if (view) {
router.replace({
name: 'project.view',
name: 'project',
params: {
projectId: props.projectId,
viewId: view.id,
@@ -65,11 +95,7 @@ watch(
)
// using a watcher instead of beforeEnter because beforeEnter is not called when only the viewId changes
watch(
() => [props.projectId, props.viewId],
() => saveProjectView(props.projectId, props.viewId),
{immediate: true},
)
watchEffect(() => saveProjectView(props.projectId, props.viewId))
const route = useRoute()
</script>
@@ -25,6 +25,12 @@ import {computed} from 'vue'
import DOMPurify from 'dompurify'
import {useProjectStore} from '@/stores/projects'
definePage({
name: 'project.info',
meta: { showAsModal: true },
props: route => ({ projectId: Number(route.params.projectId as string) }),
})
const props = defineProps<{
projectId: number
}>()
@@ -28,6 +28,11 @@ import {useTitle} from '@/composables/useTitle'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
definePage({
name: 'project.settings.archive',
meta: { showAsModal: true }
})
const {t} = useI18n({useScope: 'global'})
const projectStore = useProjectStore()
const router = useRouter()
@@ -133,6 +133,11 @@ import {useTitle} from '@/composables/useTitle'
import CreateEdit from '@/components/misc/CreateEdit.vue'
import {success} from '@/message'
definePage({
name: 'project.settings.background',
meta: { showAsModal: true }
})
const SEARCH_DEBOUNCE = 300
const {t} = useI18n({useScope: 'global'})
@@ -41,6 +41,11 @@ import Loading from '@/components/misc/Loading.vue'
import {useProjectStore} from '@/stores/projects'
import TaskService from '@/services/task'
definePage({
name: 'project.settings.delete',
meta: { showAsModal: true }
})
const {t} = useI18n({useScope: 'global'})
const projectStore = useProjectStore()
const route = useRoute()
@@ -24,6 +24,11 @@ import {useTitle} from '@/composables/useTitle'
import {useProject, useProjectStore} from '@/stores/projects'
import type {IProject} from '@/modelTypes/IProject'
definePage({
name: 'project.settings.duplicate',
meta: { showAsModal: true }
})
const {t} = useI18n({useScope: 'global'})
useTitle(() => t('project.duplicate.title'))
@@ -101,6 +101,14 @@ import {useProject} from '@/stores/projects'
import {useTitle} from '@/composables/useTitle'
import {RIGHTS} from '@/constants/rights'
definePage({
name: 'project.settings.edit',
props: route => ({ projectId: Number(route.params.projectId as string) }),
meta: {
showAsModal: true,
},
})
const props = defineProps<{
projectId: IProject['id'],
}>()
@@ -48,6 +48,11 @@ import userTeam from '@/components/sharing/UserTeam.vue'
import {useBaseStore} from '@/stores/base'
import {useConfigStore} from '@/stores/config'
definePage({
name: 'project.settings.share',
meta: { showAsModal: true }
})
const {t} = useI18n({useScope: 'global'})
const project = ref<IProject>()
@@ -16,6 +16,12 @@ import Message from '@/components/misc/Message.vue'
import draggable from 'zhyswan-vuedraggable'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
definePage({
name: 'project.settings.views',
meta: { showAsModal: true },
props: route => ({ projectId: Number(route.params.projectId as string) }),
})
const props = defineProps<{
projectId: number
}>()
@@ -25,6 +25,11 @@ import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import {success} from '@/message'
import {isValidHttpUrl} from '@/helpers/isValidHttpUrl'
definePage({
name: 'project.settings.webhooks',
meta: { showAsModal: true }
})
const {t} = useI18n({useScope: 'global'})
const project = ref<IProject>()
@@ -48,6 +48,8 @@ import {useStorage} from '@vueuse/core'
import {useProjectStore} from '@/stores/projects'
definePage({ name: 'projects.index' })
const {t} = useI18n()
const projectStore = useProjectStore()
@@ -35,7 +35,7 @@
</a>
<RouterLink
v-else
:to="{ name: 'project.index', params: { projectId: p.id } }"
:to="{ name: 'project', params: { projectId: p.id } }"
>
{{ getProjectTitle(p) }}
</RouterLink>
@@ -644,6 +644,11 @@ import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import type {Action as MessageAction} from '@/message'
definePage({
name: 'task.detail',
props: route => ({ taskId: Number(route.params.id as string) })
})
const props = defineProps<{
taskId: ITask['id'],
backdropView?: RouteLocation['fullPath'],
@@ -876,7 +881,7 @@ const showDeleteModal = ref(false)
async function deleteTask() {
await taskStore.delete(task.value)
success({message: t('task.detail.deleteSuccess')})
router.push({name: 'project.index', params: {projectId: task.value.projectId}})
router.push({name: 'project', params: {projectId: task.value.projectId}})
}
async function toggleTaskDone() {
+27
View File
@@ -0,0 +1,27 @@
<template>
<ShowTasks v-bind="props" />
</template>
<script setup lang="ts">
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import ShowTasks from '@/components/tasks/ShowTasks.vue';
definePage({
name: 'tasks.range',
props: route => ({
dateFrom: parseDateOrString(route.query.from as string, new Date()),
dateTo: parseDateOrString(route.query.to as string, getNextWeekDate()),
showNulls: route.query.showNulls === 'true',
showOverdue: route.query.showOverdue === 'true',
})
})
const props = defineProps<{
dateFrom?: Date | string,
dateTo?: Date | string,
showNulls?: boolean,
showOverdue?: boolean,
}>()
</script>

Some files were not shown because too many files have changed in this diff Show More