Compare commits

...

33 Commits

Author SHA1 Message Date
Dominik Pschenitschni 8a5017b2d6
feat: add zod schemas
continuous-integration/drone/pr Build is passing Details
2022-09-13 17:11:49 +02:00
Dominik Pschenitschni 1a11b43ca8 feat: improve models
continuous-integration/drone/push Build is passing Details
2022-09-13 14:59:02 +00:00
renovate 61427987c2 fix(deps): update dependency date-fns to v2.29.3 (#2354)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2354
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-13 13:26:48 +00:00
Dominik Pschenitschni 7b398f73f6 feat: add fallback for useCopyToClipboard (#2343)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: #2343
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-13 12:56:29 +00:00
renovate 64726a6421 fix(deps): update dependency blurhash to v2 (#2351)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2351
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-13 12:54:46 +00:00
renovate 53858e0c31 chore(deps): update typescript-eslint monorepo to v5.37.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-09-12 18:02:50 +00:00
renovate 2f0f648d28 chore(deps): update dependency eslint to v8.23.1
continuous-integration/drone/push Build is passing Details
2022-09-12 06:14:59 +00:00
renovate 6e026cc7cc chore(deps): update dependency netlify-cli to v11.7.1
continuous-integration/drone/push Build is passing Details
2022-09-12 06:13:48 +00:00
renovate d0fefd3c08 chore(deps): update dependency caniuse-lite to v1.0.30001397
continuous-integration/drone/push Build is passing Details
2022-09-12 06:12:28 +00:00
renovate 4dd397e3d2 chore(deps): update dependency autoprefixer to v10.4.9
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-09-12 00:03:06 +00:00
drone 2a41ccb980 [skip ci] Updated translations via Crowdin 2022-09-11 00:31:16 +00:00
renovate 38d72b59df chore(deps): update dependency vitest to v0.23.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2022-09-10 08:02:50 +00:00
drone add080d214 [skip ci] Updated translations via Crowdin 2022-09-10 00:31:03 +00:00
renovate 65f9def438 chore(deps): update dependency typescript to v4.8.3 (#2341)
continuous-integration/drone/push Build is failing Details
Reviewed-on: #2341
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-09 06:57:05 +00:00
drone f1a726550e [skip ci] Updated translations via Crowdin 2022-09-09 00:31:14 +00:00
kolaente 63f5f446fd feat(link shares): hide the logo if a query parameter was passed
continuous-integration/drone/push Build is passing Details
2022-09-08 09:56:09 +00:00
kolaente b8d77a617b
chore: rearrange non-dev dependencies
continuous-integration/drone/push Build is passing Details
2022-09-08 11:34:48 +02:00
kolaente d822709991
chore: automerge renovate dev dependency updates
continuous-integration/drone/push Build is passing Details
2022-09-08 11:31:51 +02:00
renovate 86a04da470 fix(deps): update dependency vue to v3.2.39
continuous-integration/drone/push Build is failing Details
2022-09-08 08:25:34 +00:00
renovate e6fbf1cb50 chore(deps): update dependency vue-tsc to v0.40.13
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2022-09-08 04:03:13 +00:00
renovate 4fe6186ee6 chore(deps): update dependency sass to v1.54.9 (#2336)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2336
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 22:09:57 +00:00
kolaente 6bf5f6efd4
fix: dragging a list on mobile Safari
continuous-integration/drone/push Build is failing Details
2022-09-07 23:11:44 +02:00
renovate 03f448457a chore(deps): update dependency vue-tsc to v0.40.11 (#2333)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2333
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 18:23:17 +00:00
konrad 7f6f8963e7 feat: add keyboard shortcut to toggle task description edit (#2332)
continuous-integration/drone/push Build is failing Details
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: #2332
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-07 17:55:59 +00:00
kolaente 65fd2f14a0
feat: show user display name when searching for assignees on a list
continuous-integration/drone/push Build is passing Details
2022-09-07 17:05:44 +02:00
renovate 2e84c27d1e chore(deps): update dependency vite-svg-loader to v3.6.0 (#2327)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2327
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 14:29:59 +00:00
renovate ee79a1d604 chore(deps): update dependency postcss-preset-env to v7.8.1 (#2328)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2328
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 13:23:59 +00:00
renovate d17428d4d0 chore(deps): update dependency vue-tsc to v0.40.10 (#2326)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2326
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 13:23:30 +00:00
renovate bca53ec8ae chore(deps): update dependency vite-plugin-pwa to v0.12.7 (#2325)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2325
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 13:22:35 +00:00
renovate fef1af6ce7 chore(deps): update dependency @vue/eslint-config-typescript to v11.0.1 (#2324)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2324
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-06 15:06:58 +00:00
renovate 3209206260 fix(deps): pin dependency @types/lodash.clonedeep to 4.5.7 (#2323)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2323
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-06 12:20:22 +00:00
kolaente d819b9b0ba
fix: don't encode attachment upload file blob as json
continuous-integration/drone/push Build is passing Details
2022-09-06 13:02:49 +02:00
kolaente e95904351f
feat: add sponsor logo to readme (relm)
continuous-integration/drone/push Build is passing Details
2022-09-06 12:02:11 +02:00
171 changed files with 1545 additions and 440 deletions

View File

@ -42,3 +42,7 @@ yarn run build
```shell
yarn run lint
```
## Sponsors
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)

View File

@ -18,19 +18,24 @@
"browserslist:update": "npx browserslist@latest --update-db"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.2.0",
"@fortawesome/free-regular-svg-icons": "6.2.0",
"@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@github/hotkey": "2.0.1",
"@kyvg/vue3-notification": "2.4.1",
"@sentry/tracing": "7.12.1",
"@sentry/vue": "7.12.1",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "^4.5.7",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.13.0",
"@vueuse/core": "9.2.0",
"@vueuse/router": "9.2.0",
"blurhash": "1.1.5",
"axios": "0.27.2",
"blurhash": "2.0.0",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"date-fns": "2.29.2",
"date-fns": "2.29.3",
"dompurify": "2.4.0",
"easymde": "2.17.0",
"flatpickr": "4.6.13",
@ -45,7 +50,8 @@
"snake-case": "3.0.4",
"ufo": "0.8.5",
"v-tooltip": "4.0.0-beta.17",
"vue": "3.2.38",
"validator": "^13.7.0",
"vue": "3.2.39",
"vue-advanced-cropper": "2.8.3",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.6",
@ -60,42 +66,39 @@
"@cypress/vite-dev-server": "3.1.1",
"@cypress/vue": "4.2.0",
"@faker-js/faker": "7.5.0",
"@fortawesome/fontawesome-svg-core": "6.2.0",
"@fortawesome/free-regular-svg-icons": "6.2.0",
"@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@types/flexsearch": "0.7.3",
"@typescript-eslint/eslint-plugin": "5.36.2",
"@typescript-eslint/parser": "5.36.2",
"@types/validator": "^13.7.5",
"@typescript-eslint/eslint-plugin": "5.37.0",
"@typescript-eslint/parser": "5.37.0",
"@vitejs/plugin-legacy": "2.1.0",
"@vitejs/plugin-vue": "3.1.0",
"@vue/eslint-config-typescript": "11.0.0",
"@vue/eslint-config-typescript": "11.0.1",
"@vue/test-utils": "2.0.2",
"@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.8",
"axios": "0.27.2",
"autoprefixer": "10.4.9",
"browserslist": "4.21.3",
"caniuse-lite": "1.0.30001390",
"caniuse-lite": "1.0.30001397",
"cypress": "10.7.0",
"esbuild": "0.15.7",
"eslint": "8.23.0",
"eslint": "8.23.1",
"eslint-plugin-vue": "9.4.0",
"express": "4.18.1",
"happy-dom": "6.0.4",
"netlify-cli": "11.5.1",
"netlify-cli": "11.7.1",
"postcss": "8.4.16",
"postcss-preset-env": "7.8.0",
"postcss-preset-env": "7.8.1",
"rollup": "2.79.0",
"rollup-plugin-visualizer": "5.8.1",
"sass": "1.54.8",
"typescript": "4.8.2",
"sass": "1.54.9",
"typescript": "4.8.3",
"vite": "3.1.0",
"vite-plugin-pwa": "0.12.6",
"vite-svg-loader": "3.5.1",
"vitest": "0.23.1",
"vue-tsc": "0.40.9",
"vite-plugin-pwa": "0.12.7",
"vite-svg-loader": "3.6.0",
"vitest": "0.23.2",
"vue-tsc": "0.40.13",
"wait-on": "6.0.1",
"workbox-cli": "6.5.4"
"workbox-cli": "6.5.4",
"zod": "3.18.0"
},
"postcss": {
"plugins": {

View File

@ -19,6 +19,12 @@
"matchPackagePrefixes": [
"@vueuse/"
]
},
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"automergeStrategy": "squash",
"automergeType": "pr"
}
]
}

View File

@ -6,8 +6,9 @@
>
<div class="container has-text-centered link-share-view">
<div class="column is-10 is-offset-1">
<Logo class="logo"/>
<Logo class="logo" v-if="logoVisible"/>
<h1
:class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
class="title">
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
@ -31,6 +32,7 @@ import PoweredByLink from './PoweredByLink.vue'
const store = useStore()
const currentList = computed(() => store.state.currentList)
const background = computed(() => store.state.background)
const logoVisible = computed(() => store.state.logoVisible)
</script>
<style lang="scss" scoped>

View File

@ -156,8 +156,8 @@ import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle'
import {useEventListener} from '@vueuse/core'
import type { IList } from '@/models/list'
import type { INamespace } from '@/models/namespace'
import type {IList} from '@/modelTypes/IList'
import type {INamespace} from '@/modelTypes/INamespace'
const drag = ref(false)
const dragOptions = {
@ -250,9 +250,14 @@ async function saveListPosition(e: SortableEvent) {
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
const listsActive = activeLists.value[newNamespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
// To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
const list = listsActive[newIndex]
const listBefore = listsActive[newIndex - 1] ?? null
const listAfter = listsActive[newIndex + 1] ?? null
listUpdating.value[list.id] = true
const position = calculateItemPosition(

View File

@ -16,14 +16,29 @@
<p class="has-text-centered has-text-grey is-italic my-5" v-if="showPreviewText">
{{ emptyText }}
<template v-if="isEditEnabled">
<ButtonLink @click="toggleEdit" class="d-print-none">{{ $t('input.editor.edit') }}</ButtonLink>.
<ButtonLink
@click="toggleEdit"
v-shortcut="editShortcut"
class="d-print-none">
{{ $t('input.editor.edit') }}
</ButtonLink>.
</template>
</p>
<ul class="actions d-print-none" v-if="bottomActions.length > 0">
<li v-if="isEditEnabled && !showPreviewText && showSave">
<BaseButton v-if="showEditButton" @click="toggleEdit">{{ $t('input.editor.edit') }}</BaseButton>
<BaseButton v-else-if="isEditActive" @click="toggleEdit" class="done-edit">{{ $t('misc.save') }}</BaseButton>
<BaseButton
v-if="showEditButton"
@click="toggleEdit"
v-shortcut="editShortcut">
{{ $t('input.editor.edit') }}
</BaseButton>
<BaseButton
v-else-if="isEditActive"
@click="toggleEdit"
class="done-edit">
{{ $t('misc.save') }}
</BaseButton>
</li>
<li v-for="(action, k) in bottomActions" :key="k">
<BaseButton @click="action.action">{{ action.title }}</BaseButton>
@ -32,7 +47,11 @@
<template v-else-if="isEditEnabled && showSave">
<ul v-if="showEditButton" class="actions d-print-none">
<li>
<BaseButton @click="toggleEdit">{{ $t('input.editor.edit') }}</BaseButton>
<BaseButton
@click="toggleEdit"
v-shortcut="editShortcut">
{{ $t('input.editor.edit') }}
</BaseButton>
</li>
</ul>
<x-button
@ -110,6 +129,12 @@ export default defineComponent({
type: Boolean,
default: false,
},
// If a key is passed the editor will go in "edit" mode when the key is pressed.
// Disabled if an empty string is passed.
editShortcut: {
type: String,
default: '',
},
},
emits: ['update:modelValue', 'change'],
computed: {

View File

@ -83,8 +83,8 @@ import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
import type {IList} from '@/models/list'
import type { ISubscription } from '@/models/subscription'
import type {IList} from '@/modelTypes/IList'
import type {ISubscription} from '@/modelTypes/ISubscription'
const props = defineProps({
list: {

View File

@ -45,7 +45,7 @@ import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import BaseButton from '@/components/base/BaseButton.vue'
import type { IList } from '@/models/list'
import type {IList} from '@/modelTypes/IList'
const background = ref<string | null>(null)
const backgroundLoading = ref(false)

View File

@ -136,6 +136,10 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
title: 'keyboardShortcuts.task.reminder',
keys: ['alt', 'r'],
},
{
title: 'keyboardShortcuts.task.description',
keys: ['e'],
},
],
},
]

View File

@ -39,7 +39,8 @@ import BaseButton from '@/components/base/BaseButton.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import SubscriptionService from '@/services/subscription'
import SubscriptionModel, { type ISubscription } from '@/models/subscription'
import SubscriptionModel from '@/models/subscription'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {success} from '@/message'

View File

@ -59,7 +59,8 @@ import {ref, onMounted, type PropType} from 'vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
import type { INamespace } from '@/models/namespace'
import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription'
const props = defineProps({
namespace: {
@ -68,7 +69,7 @@ const props = defineProps({
},
})
const subscription = ref(null)
const subscription = ref<ISubscription | null>(null)
onMounted(() => {
subscription.value = props.namespace.subscription
})

View File

@ -181,13 +181,15 @@ import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import {RIGHTS} from '@/constants/rights'
import LinkShareModel, { type ILinkShare } from '@/models/linkShare'
import LinkShareModel from '@/models/linkShare'
import type {ILinkShare} from '@/modelTypes/ILinkShare'
import type {IList} from '@/modelTypes/IList'
import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import type { IList } from '@/models/list'
const props = defineProps({
listId: {

View File

@ -143,22 +143,29 @@ import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import UserNamespaceService from '@/services/userNamespace'
import UserNamespaceModel, { type IUserNamespace } from '@/models/userNamespace'
import UserNamespaceModel from '@/models/userNamespace'
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
import UserListService from '@/services/userList'
import UserListModel, { type IUserList } from '@/models/userList'
import UserListModel from '@/models/userList'
import type {IUserList} from '@/modelTypes/IUserList'
import UserService from '@/services/user'
import UserModel, { type IUser } from '@/models/user'
import UserModel from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
import TeamNamespaceService from '@/services/teamNamespace'
import TeamNamespaceModel, { type ITeamNamespace } from '@/models/teamNamespace'
import TeamNamespaceModel from '@/models/teamNamespace'
import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
import TeamListService from '@/services/teamList'
import TeamListModel, { type ITeamList } from '@/models/teamList'
import TeamListModel from '@/models/teamList'
import type { ITeamList } from '@/modelTypes/ITeamList'
import TeamService from '@/services/team'
import TeamModel, { type ITeam } from '@/models/team'
import TeamModel from '@/models/team'
import type {ITeam} from '@/modelTypes/ITeam'
import {RIGHTS} from '@/constants/rights'
import Multiselect from '@/components/input/multiselect.vue'

View File

@ -83,7 +83,8 @@ import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor'
import TaskService from '@/services/task'
import TaskModel, { type ITask } from '@/models/task'
import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import EditLabels from './partials/editLabels.vue'
import Reminders from './partials/reminders.vue'
import ColorPicker from '../input/colorPicker.vue'

View File

@ -147,7 +147,8 @@
import {defineComponent} from 'vue'
import AttachmentService from '../../../services/attachment'
import AttachmentModel, { type IAttachment } from '@/models/attachment'
import AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import User from '@/components/misc/user.vue'
import {mapState} from 'vuex'
@ -155,8 +156,8 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments'
import {formatDate, formatDateSince, formatDateLong} from '@/helpers/time/formatDate'
import BaseButton from '@/components/base/BaseButton'
import type { IFile } from '@/models/file'
import BaseButton from '@/components/base/BaseButton.vue'
import type { IFile } from '@/modelTypes/IFile'
import { getHumanSize } from '@/helpers/getHumanSize'
export default defineComponent({

View File

@ -14,7 +14,7 @@ import {computed, type PropType} from 'vue'
import { useI18n } from 'vue-i18n'
import {getChecklistStatistics} from '@/helpers/checklistFromText'
import type {ITask} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({
task: {

View File

@ -159,12 +159,15 @@ import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor'
import TaskCommentService from '@/services/taskComment'
import TaskCommentModel, { type ITaskComment } from '@/models/taskComment'
import TaskCommentModel from '@/models/taskComment'
import type {ITaskComment} from '@/modelTypes/ITaskComment'
import type {ITask} from '@/modelTypes/ITask'
import {uploadFile} from '@/helpers/attachments'
import {success} from '@/message'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import type { ITask } from '@/models/task'
const props = defineProps({
taskId: {
type: Number,

View File

@ -28,7 +28,7 @@
<script lang="ts" setup>
import {computed, toRefs, type PropType} from 'vue'
import type { ITask } from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
const props = defineProps({

View File

@ -44,7 +44,7 @@ import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component'
import TaskService from '@/services/task'
import { type ITask } from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({
modelValue: {

View File

@ -24,6 +24,7 @@
:placeholder="$t('task.description.placeholder')"
:empty-text="$t('task.description.empty')"
:show-save="true"
edit-shortcut="e"
v-model="task.description"
/>
</div>
@ -35,7 +36,7 @@ import {useStore} from '@/store'
import Editor from '@/components/input/AsyncEditor'
import type { ITask } from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({

View File

@ -10,7 +10,7 @@
@search="findUser"
:search-results="foundUsers"
@select="addAssignee"
label="username"
label="name"
:select-placeholder="$t('task.assignee.selectPlaceholder')"
v-model="assignees"
ref="multiselect"
@ -106,6 +106,11 @@ async function findUser(query: string) {
// Filter the results to not include users who are already assigned
foundUsers.value = response.filter(({id}) => !includesById(assignees.value, id))
.map(u => {
// Users may not have a display name set, so we fall back on the username in that case
u.name = u.name === '' ? u.username : u.name
return u
})
}
function clearAllFoundUsers() {

View File

@ -34,12 +34,14 @@
<script setup lang="ts">
import {ref, computed, type PropType} from 'vue'
import {useStore} from '@/store'
import {useRouter} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
import Done from '@/components/misc/Done.vue'
import type {ITask} from '@/models/task'
import { useRouter } from 'vue-router'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({
task: {

View File

@ -74,7 +74,8 @@ import User from '../../../components/misc/user.vue'
import Done from '@/components/misc/Done.vue'
import Labels from '../../../components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary.vue'
import {TASK_DEFAULT_COLOR, type ITask} from '@/models/task'
import {TASK_DEFAULT_COLOR} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark'

View File

@ -11,8 +11,8 @@
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
import type { ILabel } from '@/models/label'
import type {PropType} from 'vue'
import type {ILabel} from '@/modelTypes/ILabel'
defineProps({
labels: {

View File

@ -21,7 +21,8 @@ import {reactive, ref, watch} from 'vue'
import type {PropType} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n'
import ListModel, { type IList } from '@/models/list'
import ListModel from '@/models/list'
import type {IList} from '@/modelTypes/IList'
import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({

View File

@ -98,7 +98,8 @@
<script lang="ts">
import {defineComponent, type PropType} from 'vue'
import TaskModel, { type ITask } from '../../../models/task'
import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from './priorityLabel.vue'
import TaskService from '../../../services/task'
import Labels from '@/components/tasks/partials/labels.vue'

View File

@ -3,11 +3,42 @@ import {useI18n} from 'vue-i18n'
export function useCopyToClipboard() {
const {t} = useI18n({useScope: 'global'})
function fallbackCopyTextToClipboard(text: string) {
const textArea = document.createElement('textarea')
textArea.value = text
// Avoid scrolling to bottom
textArea.style.top = '0'
textArea.style.left = '0'
textArea.style.position = 'fixed'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
// NOTE: the execCommand is deprecated but as of 2022_09
// widely supported and works without https
const successful = document.execCommand('copy')
if (!successful) {
throw new Error()
}
} catch (err) {
error(t('misc.copyError'))
}
document.body.removeChild(textArea)
}
return async (text: string) => {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text)
return
}
try {
await navigator.clipboard.writeText(text)
} catch {
} catch(e) {
error(t('misc.copyError'))
}
}

View File

@ -4,6 +4,9 @@ import {isAppleDevice} from '@/helpers/isAppleDevice'
const directive: Directive = {
mounted(el, {value}) {
if(value === '') {
return
}
if (isAppleDevice() && value.includes('Control')) {
value = value.replace('Control', 'Meta')
}

View File

@ -1,5 +1,6 @@
import AttachmentModel, { type IAttachment } from '@/models/attachment'
import type {IFile} from '@/models/file'
import AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IFile} from '@/modelTypes/IFile'
import AttachmentService from '@/services/attachment'
import { store } from '@/store'

View File

@ -1,5 +1,5 @@
import {i18n} from '@/i18n'
import type { IList } from '@/models/list'
import type {IList} from '@/modelTypes/IList'
export function getListTitle(l: IList) {
if (l.id === -1) {

View File

@ -1,5 +1,5 @@
import {i18n} from '@/i18n'
import type {INamespace} from '@/models/namespace'
import type {INamespace} from '@/modelTypes/INamespace'
export const getNamespaceTitle = (n: INamespace) => {
if (n.id === -1) {

View File

@ -1,7 +1,7 @@
import {createNewIndexer} from '../indexes'
import type {LabelState} from '@/store/types'
import type {ILabel} from '@/models/label'
import type {ILabel} from '@/modelTypes/ILabel'
const {search} = createNewIndexer('labels', ['title', 'description'])

View File

@ -1,4 +1,4 @@
import type {IList} from '@/models/list'
import type {IList} from '@/modelTypes/IList'
const key = 'collapsedBuckets'

View File

@ -1,4 +1,4 @@
import type {IList} from '@/models/list'
import type {IList} from '@/modelTypes/IList'
export function getSavedFilterIdFromListId(listId: IList['id']) {
let filterId = listId * -1 - 1

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Upravit související úkoly tohoto úkolu",
"color": "Změnit barvu tohoto úkolu",
"move": "Přesunout tento úkol do jiného seznamu",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "Zobrazení seznamů",

View File

@ -870,7 +870,8 @@
"related": "Ändere die Abhängigen Aufgaben dieser Aufgabe",
"color": "Die Farbe dieser Aufgabe ändern",
"move": "Diese Aufgabe in eine andere Liste verschieben",
"reminder": "Erinnerungen für diese Aufgabe verwalten"
"reminder": "Erinnerungen für diese Aufgabe verwalten",
"description": "Aufgabenbeschreibung bearbeiten"
},
"list": {
"title": "Listenansicht",

View File

@ -870,7 +870,8 @@
"related": "Beziehige vo dere Uufgab bearbeite",
"color": "Die Farbe dieser Aufgabe ändern",
"move": "Diese Aufgabe in eine andere Liste verschieben",
"reminder": "Erinnerungen für diese Aufgabe verwalten"
"reminder": "Erinnerungen für diese Aufgabe verwalten",
"description": "Aufgabenbeschreibung bearbeiten"
},
"list": {
"title": "Listenansicht",

View File

@ -873,7 +873,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Modifier les tâches connexes de cette tâche",
"color": "Changer la couleur de cette tâche",
"move": "Déplacer cette tâche dans une autre liste",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "Vues en liste",

View File

@ -87,7 +87,7 @@
"language": "Lingua",
"defaultList": "Lista predefinita",
"timezone": "Fuso Orario",
"overdueTasksRemindersTime": "Orario email del promemoria attività in ritardo"
"overdueTasksRemindersTime": "Orario email attività in scadute"
},
"totp": {
"title": "Autenticazione a due fattori",
@ -562,12 +562,12 @@
}
},
"datemathHelp": {
"canuse": "You can use date math to filter for relative dates.",
"canuse": "Puoi usare le date calcolate per filtrare per date relative.",
"learnhow": "Scopri come funziona",
"title": "Date Math",
"intro": "Date Math allows you to specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.",
"similar": "These expressions are similar to the ones provided by {0} and {1}.",
"title": "Date Calcolate",
"intro": "Le Date Calcolate ti permettono di specificare date relative che vengono calcolate al volo da Vikunja quando viene applicato il filtro.",
"expression": "Ogni Data Calcolata inizia con una data base, che può essere {0}, o una data con {1} alla fine. Questa data base può essere seguita da una o più espressioni matematiche.",
"similar": "Queste espressioni sono simili a quelle fornite da {0} e {1}.",
"add1Day": "Aggiungi un giorno",
"minus1Day": "Sottrai un giorno",
"roundDay": "Arrotonda per difetto al giorno più vicino",
@ -781,7 +781,7 @@
"weeks": "Settimane",
"months": "Mesi",
"years": "Anni",
"invalidAmount": "Please enter more than 0."
"invalidAmount": "Inserisci più di 0."
},
"quickAddMagic": {
"hint": "Puoi usare l'Aggiunta Rapida Magica",
@ -791,15 +791,15 @@
"multiple": "Puoi usarlo più volte.",
"label1": "Per aggiungere un'etichetta, basta aggiungere il nome dell'etichetta preceduto da {prefix}.",
"label2": "Vikunja controllerà prima se l'etichetta esiste già e nel caso la creerà.",
"label3": "To use spaces, simply add a \" or ' around the label name.",
"label3": "Per usare gli spazi, basta aggiungere un \" o ' prima e dopo il nome dell'etichetta.",
"label4": "Per esempio: {prefix}\"Etichetta con spazi\".",
"priority1": "Per impostare la priorità di un'attività, aggiungi un numero 1-5, preceduto da {prefix}.",
"priority2": "Più alto è il numero, più alta è la priorità.",
"assignees": "Per assegnare direttamente l'attività a un utente, aggiungere il suo nome utente preceduto da {prefix} all'attività.",
"list1": "Per impostare una lista di appartenenza all'attività, inserisci il suo nome prefisso con {prefix}.",
"list2": "Ciò restituirà un errore se la lista non esiste.",
"list3": "To use spaces, simply add a \" or ' around the list name.",
"list4": "For example: {prefix}\"List with spaces\".",
"list3": "Per usare gli spazi, basta aggiungere un \" o ' prima e dopo il nome della lista.",
"list4": "Per esempio: {prefix}\"Etichetta con spazi\".",
"dateAndTime": "Data e ora",
"date": "Qualsiasi data verrà utilizzata come data di scadenza della nuova attività. È possibile utilizzare le date in uno qualsiasi di questi formati:",
"dateWeekday": "qualsiasi giorno della settimana, userà la data più vicina",
@ -870,7 +870,8 @@
"related": "Modifica le attività collegate a questa",
"color": "Cambia il colore di questa attività",
"move": "Sposta questa attività in un altro elenco",
"reminder": "Gestisci i promemoria di questa attività"
"reminder": "Gestisci i promemoria di questa attività",
"description": "Attiva/Disattiva modifica della descrizione dell'attività"
},
"list": {
"title": "Viste Liste",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Zmodyfikuj zadania powiązane z tym zadaniem",
"color": "Zmień kolor tego zadania",
"move": "Przenieś to zadanie do innej listy",
"reminder": "Zarządzaj przypomnieniami o tym zadaniu"
"reminder": "Zarządzaj przypomnieniami o tym zadaniu",
"description": "Toggle editing of the task description"
},
"list": {
"title": "Widoki listy",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Modificar as tarefas relacionadas desta tarefa",
"color": "Alterar a cor desta tarefa",
"move": "Mover esta tarefa para outra lista",
"reminder": "Gerir lembretes desta tarefa"
"reminder": "Gerir lembretes desta tarefa",
"description": "Alternar edição da descrição da tarefa"
},
"list": {
"title": "Visualização em Lista",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Изменить связанные задачи",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -870,7 +870,8 @@
"related": "Sửa đổi các công việc liên kết",
"color": "Thay đổi màu công việc này",
"move": "Dời công việc này sang danh sách khác",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "Xem danh sách",

View File

@ -870,7 +870,8 @@
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Move this task to another list",
"reminder": "Manage reminders of this task"
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description"
},
"list": {
"title": "List Views",

View File

@ -0,0 +1,12 @@
import type { TypeOf } from 'zod'
import { AbstractSchema } from './abstract'
import { IdSchema } from './common/id'
export const LabelTaskSchema = AbstractSchema.extend({
id: IdSchema.nullable(),
taskId: IdSchema.nullable(),
labelId: IdSchema.nullable(),
})
export type LabelTask = TypeOf<typeof LabelTaskSchema>

View File

@ -0,0 +1,9 @@
import { object, nativeEnum, type TypeOf } from 'zod'
import {RIGHTS} from '@/constants/rights'
export const AbstractSchema = object({
maxRight: nativeEnum(RIGHTS).nullable(),
})
export type IAbstract = TypeOf<typeof AbstractSchema>

View File

@ -0,0 +1,19 @@
import type { TypeOf } from 'zod'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

11
src/modelSchema/avatar.ts Normal file
View File

@ -0,0 +1,11 @@
import { z, type TypeOf, 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>

View File

@ -0,0 +1,18 @@
import { type TypeOf, 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>

23
src/modelSchema/bucket.ts Normal file
View File

@ -0,0 +1,23 @@
import { type TypeOf, number, array, boolean } from 'zod'
import { UserSchema } from './user'
import { TaskSchema } from './task'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { AbstractSchema } from './abstract'
import { TextFieldSchema } from './common/textField'
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>

View File

@ -0,0 +1,14 @@
import type { TypeOf } from 'zod'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { AbstractSchema } from './abstract'
export const CaldavTokenSchema = AbstractSchema.extend({
id: IdSchema,
created: DateSchema,
})
export type CaldavToken = TypeOf<typeof CaldavTokenSchema>

View File

@ -0,0 +1,21 @@
import { nativeEnum, type TypeOf } 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>

View File

@ -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())

View File

@ -0,0 +1,79 @@
import { z, nativeEnum, type TypeOf, 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>

View File

@ -0,0 +1,11 @@
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))

View File

@ -0,0 +1,6 @@
import { number, preprocess } from 'zod'
export const IdSchema = preprocess(
(value: unknown) => Number(value),
number().positive().int(),
)

View File

@ -1,5 +1,19 @@
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
import { nativeEnum, number, object, preprocess } from 'zod'
import { nativeEnum, number, object, preprocess, type TypeOf } from 'zod'
export enum REPEAT_TYPES {
HOURS = 'hours',
DAYS = 'days',
WEEKS = 'weeks',
MONTHS = 'months',
YEARS = 'years',
}
export type IRepeatType = typeof REPEAT_TYPES[keyof typeof REPEAT_TYPES]
export interface IRepeatAfter {
type: IRepeatType,
amount: number,
}
export const RepeatsSchema = preprocess(
(repeats: unknown) => {
@ -12,7 +26,7 @@ export const RepeatsSchema = preprocess(
const repeatAfterHours = (repeats / 60) / 60
const repeatAfter : IRepeatAfter = {
type: 'hours',
type: REPEAT_TYPES.HOURS,
amount: repeatAfterHours,
}
@ -20,16 +34,16 @@ export const RepeatsSchema = preprocess(
if (repeatAfterHours % 24 === 0) {
const repeatAfterDays = repeatAfterHours / 24
if (repeatAfterDays % 7 === 0) {
repeatAfter.type = 'weeks'
repeatAfter.type = REPEAT_TYPES.WEEKS
repeatAfter.amount = repeatAfterDays / 7
} else if (repeatAfterDays % 30 === 0) {
repeatAfter.type = 'months'
repeatAfter.type = REPEAT_TYPES.MONTHS
repeatAfter.amount = repeatAfterDays / 30
} else if (repeatAfterDays % 365 === 0) {
repeatAfter.type = 'years'
repeatAfter.type = REPEAT_TYPES.YEARS
repeatAfter.amount = repeatAfterDays / 365
} else {
repeatAfter.type = 'days'
repeatAfter.type = REPEAT_TYPES.DAYS
repeatAfter.amount = repeatAfterDays
}
}
@ -40,4 +54,6 @@ export const RepeatsSchema = preprocess(
type: nativeEnum(REPEAT_TYPES),
amount: number().int(),
}),
)
)
export type RepeatAfter = TypeOf<typeof RepeatsSchema>

View File

@ -0,0 +1,3 @@
import {string} from 'zod'
export const TextFieldSchema = string().transform((value) => value.trim()).default('')

View File

@ -0,0 +1,9 @@
import { type TypeOf, 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>

13
src/modelSchema/file.ts Normal file
View File

@ -0,0 +1,13 @@
import { type TypeOf, object, number, string } from 'zod'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

32
src/modelSchema/label.ts Normal file
View File

@ -0,0 +1,32 @@
import { type TypeOf, string } from 'zod'
import { UserSchema } from './user'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { HexColorSchema } from './common/hexColor'
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>

View File

@ -0,0 +1,22 @@
import { type TypeOf, number, string, nativeEnum } from 'zod'
import { UserSchema } from './user'
import { IdSchema } from './common/id'
import { RIGHTS } from '@/constants/rights'
import { DateSchema } from './common/date'
import { AbstractSchema } from './abstract'
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>

31
src/modelSchema/list.ts Normal file
View File

@ -0,0 +1,31 @@
import { type TypeOf, boolean, number, string, array, any } from 'zod'
import { AbstractSchema } from './abstract'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

View File

@ -0,0 +1,13 @@
import type { TypeOf } from 'zod'
import { AbstractSchema } from './abstract'
import { IdSchema } from './common/id'
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>

View File

@ -0,0 +1,24 @@
import { type TypeOf, boolean, string, array } from 'zod'
import { AbstractSchema } from './abstract'
import { ListSchema } from './list'
import { UserSchema } from './user'
import { SubscriptionSchema } from './subscription'
import { IdSchema } from './common/id'
import { HexColorSchema } from './common/hexColor'
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>

View File

@ -0,0 +1,51 @@
import { type TypeOf, union, boolean, object, string } from 'zod'
import { AbstractSchema } from './abstract'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

View File

@ -0,0 +1,12 @@
import { type TypeOf, 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>

View File

@ -0,0 +1,13 @@
import { type TypeOf, 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>

View File

@ -0,0 +1,20 @@
import { type TypeOf, string } from 'zod'
import { AbstractSchema } from './abstract'
import { DateSchema } from './common/date'
import { FilterSchema } from './common/filter'
import { IdSchema } from './common/id'
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>

View File

@ -0,0 +1,18 @@
import { type TypeOf, 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>

79
src/modelSchema/task.ts Normal file
View File

@ -0,0 +1,79 @@
import { type ZodType, type TypeOf,nativeEnum, boolean, number, string, array, record, unknown, lazy } from 'zod'
import { AttachmentSchema } from './attachment'
import { LabelSchema } from './label'
import { UserSchema } from './user'
import { SubscriptionSchema } from './subscription'
import { PRIORITIES } from '@/constants/priorities'
import { RepeatsSchema } from './common/repeats'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { HexColorSchema } from './common/hexColor'
import { AbstractSchema } from './abstract'
import { TextFieldSchema } from './common/textField'
import { TASK_REPEAT_MODES } from '@/types/IRepeatMode'
import { RelationKindSchema } from './common/RelationKind'
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 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>

View File

@ -0,0 +1,14 @@
import type { TypeOf } from 'zod'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

View File

@ -0,0 +1,18 @@
import { type TypeOf, string } from 'zod'
import { AbstractSchema } from './abstract'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

View File

@ -0,0 +1,21 @@
import { RELATION_KIND } from '@/types/IRelationKind'
import { type TypeOf, nativeEnum } from 'zod'
import { AbstractSchema } from './abstract'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { UserSchema } from './user'
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>

24
src/modelSchema/team.ts Normal file
View File

@ -0,0 +1,24 @@
import { type TypeOf, array, nativeEnum, string } from 'zod'
import { RIGHTS } from '@/constants/rights'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { AbstractSchema } from './abstract'
import { UserSchema } from './user'
import { TeamMemberSchema } from './teamMember'
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>

View File

@ -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>

View File

@ -0,0 +1,12 @@
import { type TypeOf, 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>

View File

@ -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>

View File

@ -0,0 +1,18 @@
import { type TypeOf, nativeEnum } from 'zod'
import { AbstractSchema } from './abstract'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
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>

7
src/modelSchema/token.ts Normal file
View File

@ -0,0 +1,7 @@
import { object, string, type TypeOf } from 'zod'
export const TokenSchema = object({
token: string(),
})
export type IToken = TypeOf<typeof TokenSchema>

11
src/modelSchema/totp.ts Normal file
View File

@ -0,0 +1,11 @@
import { type TypeOf, 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>

20
src/modelSchema/user.ts Normal file
View File

@ -0,0 +1,20 @@
import { type TypeOf, 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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,28 @@
import { type TypeOf, 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>

View File

@ -0,0 +1,17 @@
import { type TypeOf, nativeEnum } from 'zod'
import { DateSchema } from './common/date'
import { IdSchema } from './common/id'
import { RIGHTS } from '@/constants/rights'
import { AbstractSchema } from './abstract'
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>

View File

@ -1,6 +1,7 @@
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

View File

@ -5,7 +5,7 @@ import type { IUser } from '@/modelTypes/IUser'
import type { IFile } from '@/modelTypes/IFile'
import type { IAttachment } from '@/modelTypes/IAttachment'
export default class AttachmentModel extends AbstractModel implements IAttachment {
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {
id = 0
taskId = 0
createdBy: IUser = UserModel

View File

@ -1,7 +1,7 @@
import AbstractModel from './abstractModel'
import type { IAvatar } from '@/modelTypes/IAvatar'
export default class AvatarModel extends AbstractModel implements IAvatar {
export default class AvatarModel extends AbstractModel<IAvatar> implements IAvatar {
avatarProvider: IAvatar['avatarProvider'] = 'default'
constructor(data: Partial<IAvatar>) {

View File

@ -1,7 +1,7 @@
import AbstractModel from './abstractModel'
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
export default class BackgroundImageModel extends AbstractModel implements IBackgroundImage {
export default class BackgroundImageModel extends AbstractModel<IBackgroundImage> implements IBackgroundImage {
id = 0
url = ''
thumb = ''

View File

@ -6,14 +6,14 @@ import type {IBucket} from '@/modelTypes/IBucket'
import type {ITask} from '@/modelTypes/ITask'
import type {IUser} from '@/modelTypes/IUser'
export default class BucketModel extends AbstractModel implements IBucket {
export default class BucketModel extends AbstractModel<IBucket> implements IBucket {
id = 0
title = ''
listId = ''
limit = 0
tasks: ITask[] = []
isDoneBucket: false
position: 0
isDoneBucket = false
position = 0
createdBy: IUser = null
created: Date = null

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