Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ad9c69fca | ||
|
|
6fddac75f9 |
@@ -30,10 +30,5 @@
|
||||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.keepFulfilled": true,
|
||||
"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$"]
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
@@ -11,8 +11,6 @@ stats.html
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-dev
|
||||
dist-test
|
||||
coverage
|
||||
*.zip
|
||||
.vite/
|
||||
|
||||
@@ -2,14 +2,30 @@
|
||||
|
||||
> The todo app to organize your life.
|
||||
|
||||
[](https://drone.kolaente.de/vikunja/vikunja)
|
||||
[](LICENSE)
|
||||
[](https://dl.vikunja.io)
|
||||
[](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!
|
||||
|
||||
For general information about the project, refer to the top-level readme of this repo.
|
||||
## 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.
|
||||
|
||||
## Project setup
|
||||
|
||||
@@ -20,7 +36,7 @@ pnpm install
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```shell
|
||||
pnpm run dev
|
||||
pnpm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/// <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
|
||||
|
||||
@@ -84,14 +84,5 @@ 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',
|
||||
},
|
||||
},
|
||||
|
||||
]
|
||||
@@ -93,14 +93,14 @@
|
||||
"floating-vue": "5.2.2",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lowlight": "3.3.0",
|
||||
"lowlight": "2.9.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",
|
||||
"unplugin-vue-router": "0.10.9",
|
||||
"validator": "13.12.0",
|
||||
"vue": "3.5.13",
|
||||
"vue-advanced-cropper": "2.8.9",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
@@ -108,7 +108,8 @@
|
||||
"vue-router": "4.5.0",
|
||||
"vuemoji-picker": "0.3.2",
|
||||
"workbox-precaching": "7.3.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
"zhyswan-vuedraggable": "4.1.3",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
@@ -123,6 +124,7 @@
|
||||
"@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",
|
||||
@@ -133,7 +135,7 @@
|
||||
"browserslist": "4.24.4",
|
||||
"caniuse-lite": "1.0.30001692",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "14.0.0",
|
||||
"cypress": "13.17.0",
|
||||
"esbuild": "0.24.2",
|
||||
"eslint": "9.18.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
@@ -153,7 +155,7 @@
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-plugin-vue-devtools": "7.7.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "3.0.1",
|
||||
"vitest": "2.1.8",
|
||||
"vue-tsc": "2.2.0",
|
||||
"wait-on": "8.0.2",
|
||||
"workbox-cli": "7.3.0"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
v-slot="{ Component }"
|
||||
:route="routeWithModal"
|
||||
>
|
||||
<keep-alive :include="['project']">
|
||||
<keep-alive :include="['project.view']">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</RouterView>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:to="{ name: 'project', params: { projectId: project.id} }"
|
||||
:to="{ name: 'project.index', 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 {common, createLowlight} from 'lowlight'
|
||||
import {lowlight} from 'lowlight'
|
||||
|
||||
import type {BottomAction, UploadCallback} from './types'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
@@ -343,7 +343,7 @@ const extensions : Extensions = [
|
||||
}),
|
||||
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight: createLowlight(common),
|
||||
lowlight,
|
||||
}),
|
||||
HardBreak.extend({
|
||||
addKeyboardShortcuts() {
|
||||
|
||||
@@ -62,22 +62,29 @@ export default {inheritAttrs: false}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect} from 'vue'
|
||||
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue'
|
||||
|
||||
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 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 emit = defineEmits([
|
||||
|
||||
@@ -62,7 +62,7 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||
},
|
||||
{
|
||||
title: 'project.kanban.title',
|
||||
available: (route) => route.name === 'project',
|
||||
available: (route) => route.name === 'project.view',
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.task.done',
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
{{ $t('project.webhooks.title') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.create', params: { parentProjectId: project.id } }"
|
||||
:to="{ name: 'project.createFromParent', 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', params: { projectId, viewId: view.id } }"
|
||||
:to="{ name: 'project.view', params: { projectId, viewId: view.id } }"
|
||||
>
|
||||
{{ getViewTitle(view) }}
|
||||
</BaseButton>
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, shallowReactive, watch, watchEffect} from 'vue'
|
||||
import {computed, ref, watch} 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 = shallowReactive(new ProjectService())
|
||||
const projectService = ref(new ProjectService())
|
||||
const loadedProjectId = ref(0)
|
||||
|
||||
const currentProject = computed<IProject>(() => {
|
||||
@@ -96,7 +96,8 @@ watch(
|
||||
() => props.projectId,
|
||||
// loadProject
|
||||
async (projectIdToLoad: number) => {
|
||||
saveProjectToHistory({id: projectIdToLoad})
|
||||
const projectData = {id: projectIdToLoad}
|
||||
saveProjectToHistory(projectData)
|
||||
|
||||
// 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.
|
||||
@@ -116,15 +117,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[projectIdToLoad]
|
||||
const projectFromStore = projectStore.projects[projectData.id]
|
||||
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({id: projectIdToLoad})
|
||||
const project = new ProjectModel(projectData)
|
||||
try {
|
||||
const loadedProject = await projectService.get(project)
|
||||
const loadedProject = await projectService.value.get(project)
|
||||
baseStore.handleSetCurrentProject({project: loadedProject, currentProjectViewId: props.viewId})
|
||||
} finally {
|
||||
loadedProjectId.value = projectIdToLoad
|
||||
@@ -133,7 +134,13 @@ watch(
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
watchEffect(() => baseStore.setCurrentProjectViewId(props.viewId))
|
||||
watch(
|
||||
() => props.viewId,
|
||||
() => {
|
||||
baseStore.setCurrentProjectViewId(props.viewId)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function getViewTitle(view: IProjectView) {
|
||||
switch (view.title) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
:aria-label="project.title"
|
||||
:title="textOnlyDescription"
|
||||
:to="{
|
||||
name: 'project',
|
||||
name: 'project.index',
|
||||
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 '@/composables/useGanttFilters'
|
||||
import {useGanttFilters} from '../../../views/project/helpers/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',
|
||||
name: 'project.index',
|
||||
params: {projectId: (item as DoAction<IProject>).id},
|
||||
})
|
||||
break
|
||||
|
||||
@@ -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 '@/composables/useGanttFilters'
|
||||
import type {GanttFilters} from '@/views/project/helpers/useGanttFilters'
|
||||
|
||||
import {
|
||||
extendDayjs,
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, shallowReactive, watch, computed} from 'vue'
|
||||
import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute} from 'vue-router'
|
||||
|
||||
@@ -206,17 +206,27 @@ import {useTaskStore} from '@/stores/tasks'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
|
||||
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 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 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', params: { projectId: task.projectId } }"
|
||||
:to="{ name: 'project.index', 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', params: { projectId: task.projectId } }"
|
||||
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
|
||||
class="task-project"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function useRouteWithModal() {
|
||||
}
|
||||
|
||||
const newRoute = {
|
||||
name: 'project',
|
||||
name: 'project.view',
|
||||
params: {
|
||||
projectId: baseStore.currentProject?.id,
|
||||
viewId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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>
|
||||
@@ -0,0 +1,10 @@
|
||||
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>
|
||||
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
@@ -0,0 +1,18 @@
|
||||
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>
|
||||
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
@@ -0,0 +1,26 @@
|
||||
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>
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,22 @@
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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())
|
||||
@@ -0,0 +1,80 @@
|
||||
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>
|
||||
@@ -0,0 +1,10 @@
|
||||
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))
|
||||
@@ -0,0 +1,6 @@
|
||||
import {number, preprocess} from 'zod'
|
||||
|
||||
export const IdSchema = preprocess(
|
||||
(value: unknown) => Number(value),
|
||||
number().positive().int(),
|
||||
)
|
||||
@@ -1,24 +1,26 @@
|
||||
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 { nativeEnum, number, object, preprocess } from 'zod'
|
||||
import {REPEAT_TYPES, type IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
|
||||
/**
|
||||
* Parses `repeatAfterSeconds` into a usable js object.
|
||||
*/
|
||||
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
|
||||
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
|
||||
let repeatAfter: IRepeatAfter
|
||||
|
||||
// if its dividable by 24, its something with days, otherwise hours
|
||||
// if its dividable by SECONDS_A_DAY, its something with days, otherwise hours
|
||||
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
|
||||
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}
|
||||
}
|
||||
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}
|
||||
}
|
||||
return repeatAfter
|
||||
}
|
||||
@@ -37,4 +39,6 @@ export const RepeatsSchema = preprocess(
|
||||
type: nativeEnum(REPEAT_TYPES),
|
||||
amount: number().int(),
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
export type RepeatAfter = TypeOf<typeof RepeatsSchema>
|
||||
@@ -0,0 +1,3 @@
|
||||
import {string} from 'zod'
|
||||
|
||||
export const TextFieldSchema = string().transform((value) => value.trim()).default('')
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
@@ -0,0 +1,15 @@
|
||||
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>
|
||||
@@ -0,0 +1,33 @@
|
||||
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>
|
||||
@@ -0,0 +1,26 @@
|
||||
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>
|
||||
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
@@ -0,0 +1,14 @@
|
||||
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>
|
||||
@@ -0,0 +1,26 @@
|
||||
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>
|
||||
@@ -0,0 +1,54 @@
|
||||
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>
|
||||
@@ -0,0 +1,13 @@
|
||||
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>
|
||||
@@ -0,0 +1,15 @@
|
||||
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>
|
||||
@@ -0,0 +1,23 @@
|
||||
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>
|
||||
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
@@ -0,0 +1,119 @@
|
||||
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>
|
||||
@@ -0,0 +1,14 @@
|
||||
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>
|
||||
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
@@ -0,0 +1,25 @@
|
||||
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>
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
@@ -0,0 +1,13 @@
|
||||
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>
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
@@ -0,0 +1,9 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {object, string} from 'zod'
|
||||
|
||||
export const TokenSchema = object({
|
||||
token: string(),
|
||||
})
|
||||
|
||||
export type IToken = TypeOf<typeof TokenSchema>
|
||||
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
@@ -0,0 +1,29 @@
|
||||
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>
|
||||
@@ -0,0 +1,19 @@
|
||||
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,6 +1,13 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
|
||||
export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload'
|
||||
export const AVATAR_PROVIDER = [
|
||||
'default',
|
||||
'initials',
|
||||
'gravatar',
|
||||
'marble',
|
||||
'upload',
|
||||
] as const
|
||||
export type AvatarProvider = typeof AVATAR_PROVIDER[number]
|
||||
|
||||
export interface IAvatar extends IAbstract {
|
||||
avatarProvider: AvatarProvider
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<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>
|
||||
@@ -1,25 +1,74 @@
|
||||
import { createRouter, createWebHistory, type RouteLocation, type RouteRecordInfo, type ParamValue } from 'vue-router'
|
||||
import { routes, handleHotUpdate } from 'vue-router/auto-routes'
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteLocation } from 'vue-router'
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
|
||||
import {getProjectViewId} from '@/helpers/projectView'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
|
||||
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import Register from '@/views/user/Register.vue'
|
||||
import LinkSharingAuth from '@/views/sharing/LinkSharingAuth.vue'
|
||||
import HomeComponent from '@/views/Home.vue'
|
||||
import NotFoundComponent from '@/views/404.vue'
|
||||
const About = () => import('@/views/About.vue')
|
||||
// User Handling
|
||||
import LoginComponent from '@/views/user/Login.vue'
|
||||
import RegisterComponent from '@/views/user/Register.vue'
|
||||
import OpenIdAuth from '@/views/user/OpenIdAuth.vue'
|
||||
const DataExportDownload = () => import('@/views/user/DataExportDownload.vue')
|
||||
// Tasks
|
||||
import UpcomingTasksComponent from '@/views/tasks/ShowTasks.vue'
|
||||
import LinkShareAuthComponent from '@/views/sharing/LinkSharingAuth.vue'
|
||||
const TaskDetailView = () => import('@/views/tasks/TaskDetailView.vue')
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
export interface RouteNamedMap {
|
||||
'/register': RouteRecordInfo<'/register', '/register', {}, {}>,
|
||||
'/share/[share]/auth': RouteRecordInfo<'/share/[share]/auth', '/share/:share/auth', { share: ParamValue<true> }, { share: ParamValue<false> }>,
|
||||
// Team Handling
|
||||
const ListTeamsComponent = () => import('@/views/teams/ListTeams.vue')
|
||||
// Label Handling
|
||||
const ListLabelsComponent = () => import('@/views/labels/ListLabels.vue')
|
||||
const NewLabelComponent = () => import('@/views/labels/NewLabel.vue')
|
||||
// Migration
|
||||
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
|
||||
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
|
||||
// Project View
|
||||
import ProjectView from '@/views/project/ProjectView.vue'
|
||||
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
|
||||
|
||||
}
|
||||
}
|
||||
// Project Settings
|
||||
const ListProjects = () => import('@/views/project/ListProjects.vue')
|
||||
const ProjectSettingEdit = () => import('@/views/project/settings/ProjectSettingsEdit.vue')
|
||||
const ProjectSettingBackground = () => import('@/views/project/settings/ProjectSettingsBackground.vue')
|
||||
const ProjectSettingDuplicate = () => import('@/views/project/settings/ProjectSettingsDuplicate.vue')
|
||||
const ProjectSettingShare = () => import('@/views/project/settings/ProjectSettingsShare.vue')
|
||||
const ProjectSettingWebhooks = () => import('@/views/project/settings/ProjectSettingsWebhooks.vue')
|
||||
const ProjectSettingDelete = () => import('@/views/project/settings/ProjectSettingsDelete.vue')
|
||||
const ProjectSettingArchive = () => import('@/views/project/settings/ProjectSettingsArchive.vue')
|
||||
const ProjectSettingViews = () => import('@/views/project/settings/ProjectSettingsViews.vue')
|
||||
|
||||
console.log(routes)
|
||||
// Saved Filters
|
||||
const FilterNew = () => import('@/views/filters/FilterNew.vue')
|
||||
const FilterEdit = () => import('@/views/filters/FilterEdit.vue')
|
||||
const FilterDelete = () => import('@/views/filters/FilterDelete.vue')
|
||||
|
||||
const PasswordResetComponent = () => import('@/views/user/PasswordReset.vue')
|
||||
const GetPasswordResetComponent = () => import('@/views/user/RequestPasswordReset.vue')
|
||||
const UserSettingsComponent = () => import('@/views/user/Settings.vue')
|
||||
const UserSettingsAvatarComponent = () => import('@/views/user/settings/Avatar.vue')
|
||||
const UserSettingsCaldavComponent = () => import('@/views/user/settings/Caldav.vue')
|
||||
const UserSettingsDataExportComponent = () => import('@/views/user/settings/DataExport.vue')
|
||||
const UserSettingsDeletionComponent = () => import('@/views/user/settings/Deletion.vue')
|
||||
const UserSettingsEmailUpdateComponent = () => import('@/views/user/settings/EmailUpdate.vue')
|
||||
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
|
||||
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
|
||||
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
|
||||
const UserSettingsApiTokensComponent = () => import('@/views/user/settings/ApiTokens.vue')
|
||||
|
||||
// Project Handling
|
||||
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
|
||||
|
||||
const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
|
||||
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -38,23 +87,147 @@ const router = createRouter({
|
||||
return {left: 0, top: 0}
|
||||
},
|
||||
routes: [
|
||||
...routes,
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeComponent,
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
component: NotFoundComponent,
|
||||
},
|
||||
// if you omit the last `*`, the `/` character in params will be encoded when resolving or pushing
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: 'bad-not-found',
|
||||
component: NotFoundComponent,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'user.login',
|
||||
component: LoginComponent,
|
||||
meta: {
|
||||
title: 'user.auth.login',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/get-password-reset',
|
||||
name: 'user.password-reset.request',
|
||||
component: GetPasswordResetComponent,
|
||||
meta: {
|
||||
title: 'user.auth.resetPassword',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/password-reset',
|
||||
name: 'user.password-reset.reset',
|
||||
component: PasswordResetComponent,
|
||||
meta: {
|
||||
title: 'user.auth.resetPassword',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'user.register',
|
||||
// FIXME: use dynamic imports
|
||||
// component: () => import('@/views/user/Register.vue'),
|
||||
component: Register,
|
||||
component: RegisterComponent,
|
||||
meta: {
|
||||
title: 'user.auth.createAccount',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user/settings',
|
||||
name: 'user.settings',
|
||||
component: UserSettingsComponent,
|
||||
redirect: {name: 'user.settings.general'},
|
||||
children: [
|
||||
{
|
||||
path: '/user/settings/avatar',
|
||||
name: 'user.settings.avatar',
|
||||
component: UserSettingsAvatarComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/caldav',
|
||||
name: 'user.settings.caldav',
|
||||
component: UserSettingsCaldavComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/data-export',
|
||||
name: 'user.settings.data-export',
|
||||
component: UserSettingsDataExportComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/deletion',
|
||||
name: 'user.settings.deletion',
|
||||
component: UserSettingsDeletionComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/email-update',
|
||||
name: 'user.settings.email-update',
|
||||
component: UserSettingsEmailUpdateComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/general',
|
||||
name: 'user.settings.general',
|
||||
component: UserSettingsGeneralComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/password-update',
|
||||
name: 'user.settings.password-update',
|
||||
component: UserSettingsPasswordUpdateComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/totp',
|
||||
name: 'user.settings.totp',
|
||||
component: UserSettingsTOTPComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/api-tokens',
|
||||
name: 'user.settings.apiTokens',
|
||||
component: UserSettingsApiTokensComponent,
|
||||
},
|
||||
{
|
||||
path: '/user/settings/migrate',
|
||||
name: 'migrate.start',
|
||||
component: MigrationComponent,
|
||||
},
|
||||
{
|
||||
path: '/migrate/:service',
|
||||
name: 'migrate.service',
|
||||
component: MigrationHandlerComponent,
|
||||
props: route => ({
|
||||
service: route.params.service as string,
|
||||
code: route.query.code as string,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/user/export/download',
|
||||
name: 'user.export.download',
|
||||
component: DataExportDownload,
|
||||
},
|
||||
{
|
||||
path: '/share/:share/auth',
|
||||
name: 'link-share.auth',
|
||||
// FIXME: use dynamic imports
|
||||
// component: () => import('@/views/sharing/LinkSharingAuth.vue'),
|
||||
component: LinkSharingAuth,
|
||||
component: LinkShareAuthComponent,
|
||||
},
|
||||
{
|
||||
path: '/tasks/:id',
|
||||
name: 'task.detail',
|
||||
component: TaskDetailView,
|
||||
props: route => ({ taskId: Number(route.params.id as string) }),
|
||||
},
|
||||
{
|
||||
path: '/tasks/by/upcoming',
|
||||
name: 'tasks.range',
|
||||
component: UpcomingTasksComponent,
|
||||
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',
|
||||
}),
|
||||
},
|
||||
{
|
||||
// Redirect old list routes to the respective project routes
|
||||
@@ -69,10 +242,98 @@ const router = createRouter({
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects',
|
||||
name: 'projects.index',
|
||||
component: ListProjects,
|
||||
},
|
||||
{
|
||||
path: '/projects/new',
|
||||
name: 'project.create',
|
||||
component: NewProjectComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:parentProjectId/new',
|
||||
name: 'project.createFromParent',
|
||||
component: NewProjectComponent,
|
||||
props: route => ({ parentProjectId: Number(route.params.parentProjectId as string) }),
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/edit',
|
||||
name: 'project.settings.edit',
|
||||
component: ProjectSettingEdit,
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/background',
|
||||
name: 'project.settings.background',
|
||||
component: ProjectSettingBackground,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/duplicate',
|
||||
name: 'project.settings.duplicate',
|
||||
component: ProjectSettingDuplicate,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/share',
|
||||
name: 'project.settings.share',
|
||||
component: ProjectSettingShare,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/webhooks',
|
||||
name: 'project.settings.webhooks',
|
||||
component: ProjectSettingWebhooks,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/delete',
|
||||
name: 'project.settings.delete',
|
||||
component: ProjectSettingDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/archive',
|
||||
name: 'project.settings.archive',
|
||||
component: ProjectSettingArchive,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/views',
|
||||
name: 'project.settings.views',
|
||||
component: ProjectSettingViews,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/settings/edit',
|
||||
name: 'filter.settings.edit',
|
||||
component: () => import('@/views/filters/FilterEdit.vue'),
|
||||
component: FilterEdit,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
@@ -81,20 +342,101 @@ const router = createRouter({
|
||||
{
|
||||
path: '/projects/:projectId/settings/delete',
|
||||
name: 'filter.settings.delete',
|
||||
component: () => import('@/views/filters/FilterDelete.vue'),
|
||||
component: FilterDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/info',
|
||||
name: 'project.info',
|
||||
component: ProjectInfo,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId',
|
||||
name: 'project.index',
|
||||
redirect(to) {
|
||||
const viewId = getProjectViewId(Number(to.params.projectId as string))
|
||||
|
||||
if (viewId) {
|
||||
console.debug('Replaced list view with', viewId)
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'project.view',
|
||||
params: {
|
||||
projectId: parseInt(to.params.projectId as string),
|
||||
viewId: viewId ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/:viewId',
|
||||
name: 'project.view',
|
||||
component: ProjectView,
|
||||
props: route => ({
|
||||
projectId: parseInt(route.params.projectId as string),
|
||||
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/teams',
|
||||
name: 'teams.index',
|
||||
component: ListTeamsComponent,
|
||||
},
|
||||
{
|
||||
path: '/teams/new',
|
||||
name: 'teams.create',
|
||||
component: NewTeamComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/teams/:id/edit',
|
||||
name: 'teams.edit',
|
||||
component: EditTeamComponent,
|
||||
},
|
||||
{
|
||||
path: '/labels',
|
||||
name: 'labels.index',
|
||||
component: ListLabelsComponent,
|
||||
},
|
||||
{
|
||||
path: '/labels/new',
|
||||
name: 'labels.create',
|
||||
component: NewLabelComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/filters/new',
|
||||
name: 'filters.create',
|
||||
component: FilterNew,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/auth/openid/:provider',
|
||||
name: 'openid.auth',
|
||||
component: OpenIdAuth,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: About,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Update routes at runtime without reloading the page
|
||||
if (import.meta.hot) {
|
||||
handleHotUpdate(router)
|
||||
}
|
||||
|
||||
export async function getAuthForRoute(to: RouteLocation, authStore) {
|
||||
if (authStore.authUser || authStore.authLinkShare) {
|
||||
return
|
||||
|
||||
@@ -87,7 +87,7 @@ export function useSavedFilter(projectId?: MaybeRefOrGetter<IProject['id']>) {
|
||||
async function createFilter() {
|
||||
filter.value = await filterService.create(filter.value)
|
||||
await projectStore.loadAllProjects()
|
||||
router.push({name: 'project', params: {projectId: getProjectId(filter.value)}})
|
||||
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
|
||||
}
|
||||
|
||||
async function saveFilter() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {HTTPFactory} from '@/helpers/fetcher'
|
||||
import {objectToCamelCase} from '@/helpers/case'
|
||||
|
||||
import type {IProvider} from '@/types/IProvider'
|
||||
import type {MIGRATORS} from '@/helpers/migrators'
|
||||
import type {MIGRATORS} from '@/views/migrate/migrators'
|
||||
import {InvalidApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
export interface ConfigState {
|
||||
|
||||
@@ -150,7 +150,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||
const createdProject = await projectService.create(project)
|
||||
setProject(createdProject)
|
||||
router.push({
|
||||
name: 'project',
|
||||
name: 'project.index',
|
||||
params: { projectId: createdProject.id },
|
||||
})
|
||||
return createdProject
|
||||
@@ -315,7 +315,7 @@ export function useProject(projectId: MaybeRefOrGetter<IProject['id']>) {
|
||||
|
||||
projectStore.setProject(duplicate.duplicatedProject)
|
||||
success({message: t('project.duplicate.success')})
|
||||
router.push({name: 'project', params: {projectId: duplicate.duplicatedProject.id}})
|
||||
router.push({name: 'project.index', params: {projectId: duplicate.duplicatedProject.id}})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,8 +7,11 @@ import type Modal from '@/components/misc/Modal.vue'
|
||||
import type Card from '@/components/misc/Card.vue'
|
||||
|
||||
// Here we define globally imported components
|
||||
// See: https://github.com/vuejs/language-tools/wiki/Global-Component-Types
|
||||
declare module 'vue' {
|
||||
// See:
|
||||
// https://github.com/johnsoncodehk/volar/blob/2ca8fd3434423c7bea1c8e08132df3b9ce84eea7/extensions/vscode-vue-language-features/README.md#usage
|
||||
// Under the hidden collapsible "Define Global Components"
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
Icon: FontAwesomeIconFixedTypes
|
||||
Notifications: FunctionalComponent<Notifications>
|
||||
|
||||
@@ -8,13 +8,5 @@
|
||||
<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>
|
||||
@@ -32,8 +32,6 @@ import {computed} from 'vue'
|
||||
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
definePage({ name: 'about' })
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const apiVersion = computed(() => configStore.version)
|
||||
</script>
|
||||
@@ -58,7 +58,7 @@
|
||||
import {ref, computed} from 'vue'
|
||||
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
import ShowTasks from '@/components/tasks/ShowTasks.vue'
|
||||
import ShowTasks from '@/views/tasks/ShowTasks.vue'
|
||||
import ProjectCardGrid from '@/components/project/partials/ProjectCardGrid.vue'
|
||||
import AddTask from '@/components/tasks/AddTask.vue'
|
||||
|
||||
@@ -73,8 +73,6 @@ import {useConfigStore} from '@/stores/config'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
definePage({ name: 'home' })
|
||||
|
||||
const salutation = useDaytimeSalutation()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
@@ -88,11 +88,6 @@ import Filters from '@/components/project/partials/Filters.vue'
|
||||
|
||||
import {useSavedFilter} from '@/services/savedFilter'
|
||||
|
||||
definePage({
|
||||
name: 'filters.create',
|
||||
meta: { showAsModal: true }
|
||||
})
|
||||
|
||||
const {
|
||||
filter,
|
||||
filters,
|
||||
@@ -149,8 +149,6 @@ 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,8 +54,6 @@ 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'})
|
||||
@@ -24,12 +24,10 @@
|
||||
import {computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {MIGRATORS} from '@/helpers/migrators'
|
||||
import {MIGRATORS} from './migrators'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
definePage({ name: 'migrate.start' })
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
useTitle(() => t('migrate.title'))
|
||||
@@ -110,6 +110,16 @@
|
||||
</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'
|
||||
@@ -123,24 +133,11 @@ import AbstractMigrationFileService from '@/services/migrator/abstractMigrationF
|
||||
import {formatDateLong} from '@/helpers/time/formatDate'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
|
||||
import {MIGRATORS, type Migrator} from '@/helpers/migrators'
|
||||
import {MIGRATORS, type Migrator} from './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,
|
||||
|
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,9 +1,9 @@
|
||||
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'
|
||||
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'
|
||||
|
||||
export interface Migrator {
|
||||
id: string
|
||||
@@ -48,8 +48,6 @@ import {useStorage} from '@vueuse/core'
|
||||
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
definePage({ name: 'projects.index' })
|
||||
|
||||
const {t} = useI18n()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
@@ -65,14 +65,6 @@ 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,
|
||||
}>()
|
||||
@@ -25,12 +25,6 @@ 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
|
||||
}>()
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, watch, watchEffect} from 'vue'
|
||||
import {computed, watch} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {getProjectViewId, saveProjectView} from '@/helpers/projectView'
|
||||
import {saveProjectView} from '@/helpers/projectView'
|
||||
|
||||
import ProjectList from '@/components/project/views/ProjectList.vue'
|
||||
import ProjectGantt from '@/components/project/views/ProjectGantt.vue'
|
||||
@@ -11,35 +11,6 @@ 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,
|
||||
@@ -56,24 +27,23 @@ const currentView = computed(() => {
|
||||
})
|
||||
|
||||
function redirectToDefaultViewIfNecessary() {
|
||||
if (props.viewId === 0 || !currentView.value) {
|
||||
if (props.viewId === 0 || !projectStore.projects[props.projectId]?.views.find(v => v.id === props.viewId)) {
|
||||
// Ideally, we would do that in the router redirect, but the projects (and therefore, the views)
|
||||
// are not always loaded then.
|
||||
|
||||
let view
|
||||
const defaultView = authStore.settings.frontendSettings.defaultView
|
||||
if (defaultView !== DEFAULT_PROJECT_VIEW_SETTINGS.FIRST) {
|
||||
view = currentProject.value?.views.find(v => v.viewKind === defaultView)
|
||||
if (authStore.settings.frontendSettings.defaultView !== DEFAULT_PROJECT_VIEW_SETTINGS.FIRST) {
|
||||
view = projectStore.projects[props.projectId]?.views.find(v => v.viewKind === authStore.settings.frontendSettings.defaultView)
|
||||
}
|
||||
|
||||
// Use the first view as fallback if the default view is not available
|
||||
if (view === undefined && currentProject.value?.views?.length > 0) {
|
||||
view = currentProject.value?.views[0]
|
||||
if (view === undefined && projectStore.projects[props.projectId]?.views?.length > 0) {
|
||||
view = projectStore.projects[props.projectId]?.views[0]
|
||||
}
|
||||
|
||||
if (view) {
|
||||
router.replace({
|
||||
name: 'project',
|
||||
name: 'project.view',
|
||||
params: {
|
||||
projectId: props.projectId,
|
||||
viewId: view.id,
|
||||
@@ -95,7 +65,11 @@ watch(
|
||||
)
|
||||
|
||||
// using a watcher instead of beforeEnter because beforeEnter is not called when only the viewId changes
|
||||
watchEffect(() => saveProjectView(props.projectId, props.viewId))
|
||||
watch(
|
||||
() => [props.projectId, props.viewId],
|
||||
() => saveProjectView(props.projectId, props.viewId),
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
@@ -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 '@/composables/useGanttTaskList'
|
||||
import {useGanttTaskList} from './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',
|
||||
name: 'project.view',
|
||||
params: {
|
||||
projectId: filters.projectId,
|
||||
viewId: filters.viewId,
|
||||
@@ -104,7 +104,7 @@ export function useGanttFilters(route: Ref<RouteLocationNormalized>, viewId: Ref
|
||||
ganttGetDefaultFilters,
|
||||
ganttRouteToFilters,
|
||||
ganttFiltersToRoute,
|
||||
['project'],
|
||||
['project.view'],
|
||||
)
|
||||
|
||||
const {
|
||||
@@ -28,11 +28,6 @@ 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,11 +133,6 @@ 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,11 +41,6 @@ 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,11 +24,6 @@ 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,14 +101,6 @@ 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,11 +48,6 @@ 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,12 +16,6 @@ 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,11 +25,6 @@ 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>()
|
||||