feature/convert-abstract-service-to-ts #1798

Merged
konrad merged 32 commits from dpschen/frontend:feature/convert-abstract-service-to-ts into main 2022-09-06 09:26:49 +00:00
98 changed files with 1050 additions and 507 deletions
Showing only changes of commit 3766b5e51b - Show all commits

View File

@ -156,8 +156,8 @@ import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
import {useEventListener} from '@vueuse/core' import {useEventListener} from '@vueuse/core'
import type NamespaceModel from '@/models/namespace' import type { IList } from '@/models/list'
import type ListModel from '@/models/list' import type { INamespace } from '@/models/namespace'
const drag = ref(false) const drag = ref(false)
const dragOptions = { const dragOptions = {
@ -172,7 +172,7 @@ const loading = computed(() => store.state.loading && store.state.loadingModule
const namespaces = computed(() => { const namespaces = computed(() => {
return (store.state.namespaces.namespaces as NamespaceModel[]).filter(n => !n.isArchived) return (store.state.namespaces.namespaces as INamespace[]).filter(n => !n.isArchived)
}) })
const activeLists = computed(() => { const activeLists = computed(() => {
return namespaces.value.map(({lists}) => { return namespaces.value.map(({lists}) => {
@ -195,7 +195,7 @@ useEventListener('resize', resize)
onMounted(() => resize()) onMounted(() => resize())
function toggleFavoriteList(list: ListModel) { function toggleFavoriteList(list: IList) {
// The favorites pseudo list is always favorite // The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite // Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) { if (list.id === -1 || list.isArchived) {
@ -209,14 +209,14 @@ function resize() {
store.commit(MENU_ACTIVE, window.innerWidth >= 770) store.commit(MENU_ACTIVE, window.innerWidth >= 770)
} }
function toggleLists(namespaceId: NamespaceModel['id']) { function toggleLists(namespaceId: INamespace['id']) {
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId] listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
} }
const listsVisible = ref<{ [id: NamespaceModel['id']]: boolean }>({}) const listsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
// FIXME: async action will be unfinished when component mounts // FIXME: async action will be unfinished when component mounts
onBeforeMount(async () => { onBeforeMount(async () => {
const namespaces = await store.dispatch('namespaces/loadNamespaces') as NamespaceModel[] const namespaces = await store.dispatch('namespaces/loadNamespaces') as INamespace[]
namespaces.forEach(n => { namespaces.forEach(n => {
if (typeof listsVisible.value[n.id] === 'undefined') { if (typeof listsVisible.value[n.id] === 'undefined') {
listsVisible.value[n.id] = true listsVisible.value[n.id] = true
@ -224,7 +224,7 @@ onBeforeMount(async () => {
}) })
}) })
function updateActiveLists(namespace: NamespaceModel, activeLists: ListModel[]) { function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
// This is a bit hacky: since we do have to filter out the archived items from the list // This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it. // for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order // To work around this, we merge the active lists with the archived ones. Doing so breaks the order
@ -241,7 +241,7 @@ function updateActiveLists(namespace: NamespaceModel, activeLists: ListModel[])
}) })
} }
const listUpdating = ref<{ [id: NamespaceModel['id']]: boolean }>({}) const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
async function saveListPosition(e: SortableEvent) { async function saveListPosition(e: SortableEvent) {
if (!e.newIndex && e.newIndex !== 0) return if (!e.newIndex && e.newIndex !== 0) return

View File

@ -76,24 +76,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, watchEffect} from 'vue' import {ref, computed, watchEffect, type PropType} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import TaskSubscription from '@/components/misc/subscription.vue'
import ListModel from '@/models/list' import type {IList} from '@/models/list'
import type SubscriptionModel from '@/models/subscription' import type { ISubscription } from '@/models/subscription'
const props = defineProps({ const props = defineProps({
list: { list: {
type: ListModel, type: Object as PropType<IList>,
required: true, required: true,
}, },
}) })
const subscription = ref<SubscriptionModel | null>(null) const subscription = ref<ISubscription | null>(null)
watchEffect(() => { watchEffect(() => {
subscription.value = props.list.subscription ?? null subscription.value = props.list.subscription ?? null
}) })

View File

@ -43,9 +43,9 @@ import ListService from '@/services/list'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import type ListModel from '@/models/list'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import type { IList } from '@/models/list'
const background = ref<string | null>(null) const background = ref<string | null>(null)
const backgroundLoading = ref(false) const backgroundLoading = ref(false)
@ -53,7 +53,7 @@ const blurHashUrl = ref('')
const props = defineProps({ const props = defineProps({
list: { list: {
type: Object as PropType<ListModel>, type: Object as PropType<IList>,
required: true, required: true,
}, },
showArchived: { showArchived: {
@ -86,7 +86,7 @@ async function loadBackground() {
const store = useStore() const store = useStore()
function toggleFavoriteList(list: ListModel) { function toggleFavoriteList(list: IList) {
// The favorites pseudo list is always favorite // The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite // Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) { if (list.id === -1 || list.isArchived) {

View File

@ -39,7 +39,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import SubscriptionService from '@/services/subscription' import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription' import SubscriptionModel, { type ISubscription } from '@/models/subscription'
import {success} from '@/message' import {success} from '@/message'
@ -51,7 +51,7 @@ const props = defineProps({
default: true, default: true,
}, },
subscription: { subscription: {
type: Object as PropType<SubscriptionModel>, type: Object as PropType<ISubscription>,
default: null, default: null,
}, },
type: { type: {

View File

@ -54,15 +54,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, onMounted} from 'vue' import {ref, onMounted, type PropType} from 'vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import TaskSubscription from '@/components/misc/subscription.vue'
import type { INamespace } from '@/models/namespace'
const props = defineProps({ const props = defineProps({
namespace: { namespace: {
type: Object, // NamespaceModel type: Object as PropType<INamespace>,
required: true, required: true,
}, },
}) })

View File

@ -52,7 +52,7 @@ import {computed, onMounted, onUnmounted, ref} from 'vue'
import NotificationService from '@/services/notification' import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import NotificationModel, { NOTIFICATION_NAMES as names} from '@/models/notification' import { NOTIFICATION_NAMES as names, type INotification} from '@/models/notification'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
@ -63,7 +63,7 @@ const LOAD_NOTIFICATIONS_INTERVAL = 10000
const store = useStore() const store = useStore()
const router = useRouter() const router = useRouter()
const allNotifications = ref<NotificationModel[]>([]) const allNotifications = ref<INotification[]>([])
const showNotifications = ref(false) const showNotifications = ref(false)
const popup = ref(null) const popup = ref(null)

View File

@ -181,13 +181,13 @@ import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {RIGHTS} from '@/models/constants/rights' import {RIGHTS} from '@/models/constants/rights'
import LinkShareModel from '@/models/linkShare' import LinkShareModel, { type ILinkShare } from '@/models/linkShare'
import type ListModel from '@/models/list'
import LinkShareService from '@/services/linkShare' import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message' import {success} from '@/message'
import type { IList } from '@/models/list'
const props = defineProps({ const props = defineProps({
listId: { listId: {
@ -198,7 +198,7 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const linkShares = ref<LinkShareModel[]>([]) const linkShares = ref<ILinkShare[]>([])
const linkShareService = shallowReactive(new LinkShareService()) const linkShareService = shallowReactive(new LinkShareService())
const selectedRight = ref(RIGHTS.READ) const selectedRight = ref(RIGHTS.READ)
const name = ref('') const name = ref('')
@ -217,7 +217,7 @@ watch(
const store = useStore() const store = useStore()
const frontendUrl = computed(() => store.state.config.frontendUrl) const frontendUrl = computed(() => store.state.config.frontendUrl)
async function load(listId: ListModel['id']) { async function load(listId: IList['id']) {
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here // If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
if (listId === 0) { if (listId === 0) {
return return
@ -226,7 +226,7 @@ async function load(listId: ListModel['id']) {
linkShares.value = await linkShareService.getAll({listId}) linkShares.value = await linkShareService.getAll({listId})
} }
async function add(listId: ListModel['id']) { async function add(listId: IList['id']) {
const newLinkShare = new LinkShareModel({ const newLinkShare = new LinkShareModel({
right: selectedRight.value, right: selectedRight.value,
listId, listId,
@ -242,7 +242,7 @@ async function add(listId: ListModel['id']) {
await load(listId) await load(listId)
} }
async function remove(listId: ListModel['id']) { async function remove(listId: IList['id']) {
try { try {
await linkShareService.delete(new LinkShareModel({ await linkShareService.delete(new LinkShareModel({
id: linkIdToDelete.value, id: linkIdToDelete.value,

View File

@ -143,18 +143,22 @@ import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import UserNamespaceService from '@/services/userNamespace' import UserNamespaceService from '@/services/userNamespace'
import UserNamespaceModel from '@/models/userNamespace' import UserNamespaceModel, { type IUserNamespace } from '@/models/userNamespace'
import UserListModel from '@/models/userList'
import UserListService from '@/services/userList' import UserListService from '@/services/userList'
import UserListModel, { type IUserList } from '@/models/userList'
import UserService from '@/services/user' import UserService from '@/services/user'
import UserModel from '@/models/user' import UserModel, { type IUser } from '@/models/user'
import TeamNamespaceService from '@/services/teamNamespace' import TeamNamespaceService from '@/services/teamNamespace'
import TeamNamespaceModel from '@/models/teamNamespace' import TeamNamespaceModel, { type ITeamNamespace } from '@/models/teamNamespace'
import TeamListModel from '@/models/teamList'
import TeamListService from '@/services/teamList' import TeamListService from '@/services/teamList'
import TeamListModel, { type ITeamList } from '@/models/teamList'
import TeamService from '@/services/team' import TeamService from '@/services/team'
import TeamModel from '@/models/team' import TeamModel, { type ITeam } from '@/models/team'
import {RIGHTS} from '@/models/constants/rights' import {RIGHTS} from '@/models/constants/rights'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
@ -183,10 +187,10 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
// This user service is either a userNamespaceService or a userListService, depending on the type we are using // This user service is either a userNamespaceService or a userListService, depending on the type we are using
let stuffService: ShallowReactive<UserNamespaceService | UserListService | TeamListService | TeamNamespaceService> let stuffService: UserNamespaceService | UserListService | TeamListService | TeamNamespaceService
let stuffModel: UserNamespaceModel | UserListModel | TeamListModel | TeamNamespaceModel let stuffModel: IUserNamespace | IUserList | ITeamList | ITeamNamespace
let searchService: ShallowReactive<UserService | TeamService> let searchService: UserService | TeamService
let sharable: Ref<UserModel | TeamModel> let sharable: Ref<IUser | ITeam>
const searchLabel = ref('') const searchLabel = ref('')
const selectedRight = ref({}) const selectedRight = ref({})

View File

@ -76,14 +76,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue' import {ref, reactive, computed, shallowReactive, watch, nextTick, type PropType} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskModel from '@/models/task' import TaskModel, { type ITask } from '@/models/task'
import EditLabels from './partials/editLabels.vue' import EditLabels from './partials/editLabels.vue'
import Reminders from './partials/reminders.vue' import Reminders from './partials/reminders.vue'
import ColorPicker from '../input/colorPicker.vue' import ColorPicker from '../input/colorPicker.vue'
@ -93,14 +93,16 @@ import {success} from '@/message'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const router = useRouter() const router = useRouter()
const props = defineProps<{ const props = defineProps({
task?: TaskModel | null, task: {
}>() type: Object as PropType<ITask | null>,
},
})
const taskService = shallowReactive(new TaskService()) const taskService = shallowReactive(new TaskService())
const editorActive = ref(false) const editorActive = ref(false)
let taskEditTask: TaskModel | undefined let taskEditTask: ITask | undefined
// FIXME: this initialization should not be necessary here // FIXME: this initialization should not be necessary here

View File

@ -147,8 +147,7 @@
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import AttachmentService from '../../../services/attachment' import AttachmentService from '../../../services/attachment'
import AttachmentModel from '../../../models/attachment' import AttachmentModel, { type IAttachment } from '@/models/attachment'
import type FileModel from '@/models/file'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import {mapState} from 'vuex' import {mapState} from 'vuex'
@ -157,6 +156,7 @@ import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments'
import {formatDate, formatDateSince, formatDateLong} from '@/helpers/time/formatDate' import {formatDate, formatDateSince, formatDateLong} from '@/helpers/time/formatDate'
import BaseButton from '@/components/base/BaseButton' import BaseButton from '@/components/base/BaseButton'
import type { IFile } from '@/models/file'
export default defineComponent({ export default defineComponent({
name: 'attachments', name: 'attachments',
@ -192,7 +192,7 @@ export default defineComponent({
setup(props) { setup(props) {
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
function copyUrl(attachment: AttachmentModel) { function copyUrl(attachment: IAttachment) {
copy(generateAttachmentUrl(props.taskId, attachment.id)) copy(generateAttachmentUrl(props.taskId, attachment.id))
} }
@ -235,7 +235,7 @@ export default defineComponent({
formatDateSince, formatDateSince,
formatDateLong, formatDateLong,
downloadAttachment(attachment: AttachmentModel) { downloadAttachment(attachment: IAttachment) {
this.attachmentService.download(attachment) this.attachmentService.download(attachment)
}, },
uploadNewAttachment() { uploadNewAttachment() {
@ -245,7 +245,7 @@ export default defineComponent({
this.uploadFiles(this.$refs.files.files) this.uploadFiles(this.$refs.files.files)
}, },
uploadFiles(files: FileModel[]) { uploadFiles(files: IFile[]) {
uploadFiles(this.attachmentService, this.taskId, files) uploadFiles(this.attachmentService, this.taskId, files)
}, },
async deleteAttachment() { async deleteAttachment() {

View File

@ -10,15 +10,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed, type PropType} from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import {getChecklistStatistics} from '@/helpers/checklistFromText' import {getChecklistStatistics} from '@/helpers/checklistFromText'
import TaskModel from '@/models/task' import type {ITask} from '@/models/task'
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
}) })

View File

@ -159,12 +159,12 @@ import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import TaskCommentService from '@/services/taskComment' import TaskCommentService from '@/services/taskComment'
import TaskCommentModel from '@/models/taskComment' import TaskCommentModel, { type ITaskComment } from '@/models/taskComment'
import {uploadFile} from '@/helpers/attachments' import {uploadFile} from '@/helpers/attachments'
import {success} from '@/message' import {success} from '@/message'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import type TaskModel from '@/models/task' import type { ITask } from '@/models/task'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
type: Number, type: Number,
@ -178,7 +178,7 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const store = useStore()
const comments = ref<TaskCommentModel[]>([]) const comments = ref<ITaskComment[]>([])
const showDeleteModal = ref(false) const showDeleteModal = ref(false)
const commentToDelete = reactive(new TaskCommentModel()) const commentToDelete = reactive(new TaskCommentModel())
@ -188,8 +188,8 @@ const commentEdit = reactive(new TaskCommentModel())
const newComment = reactive(new TaskCommentModel()) const newComment = reactive(new TaskCommentModel())
const saved = ref<TaskModel['id'] | null>(null) const saved = ref<ITask['id'] | null>(null)
const saving = ref<TaskModel['id'] | null>(null) const saving = ref<ITask['id'] | null>(null)
const userAvatar = computed(() => store.state.auth.info.getAvatarUrl(48)) const userAvatar = computed(() => store.state.auth.info.getAvatarUrl(48))
const currentUserId = computed(() => store.state.auth.info.id) const currentUserId = computed(() => store.state.auth.info.id)
@ -215,7 +215,7 @@ function attachmentUpload(...args) {
const taskCommentService = shallowReactive(new TaskCommentService()) const taskCommentService = shallowReactive(new TaskCommentService())
async function loadComments(taskId: TaskModel['id']) { async function loadComments(taskId: ITask['id']) {
if (!enabled.value) { if (!enabled.value) {
return return
} }
@ -259,12 +259,12 @@ async function addComment() {
} }
} }
function toggleEdit(comment: TaskCommentModel) { function toggleEdit(comment: ITaskComment) {
isCommentEdit.value = !isCommentEdit.value isCommentEdit.value = !isCommentEdit.value
Object.assign(commentEdit, comment) Object.assign(commentEdit, comment)
} }
function toggleDelete(commentId: TaskCommentModel['id']) { function toggleDelete(commentId: ITaskComment['id']) {
showDeleteModal.value = !showDeleteModal.value showDeleteModal.value = !showDeleteModal.value
commentToDelete.id = commentId commentToDelete.id = commentId
} }
@ -294,7 +294,7 @@ async function editComment() {
} }
} }
async function deleteComment(commentToDelete: TaskCommentModel) { async function deleteComment(commentToDelete: ITaskComment) {
try { try {
await taskCommentService.delete(commentToDelete) await taskCommentService.delete(commentToDelete)
const index = comments.value.findIndex(({id}) => id === commentToDelete.id) const index = comments.value.findIndex(({id}) => id === commentToDelete.id)

View File

@ -27,13 +27,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, toRefs} from 'vue' import {computed, toRefs, type PropType} from 'vue'
import TaskModel from '@/models/task' import type { ITask } from '@/models/task'
import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate' import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
}) })

View File

@ -38,17 +38,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount} from 'vue' import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount, type PropType} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskModel from '@/models/task' import { type ITask } from '@/models/task'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
}) })
@ -58,7 +58,7 @@ const {t} = useI18n({useScope: 'global'})
const store = useStore() const store = useStore()
const taskService = shallowReactive(new TaskService()) const taskService = shallowReactive(new TaskService())
const task = ref<TaskModel>() const task = ref<ITask>()
// We're saving the due date seperately to prevent null errors in very short periods where the task is null. // We're saving the due date seperately to prevent null errors in very short periods where the task is null.
const dueDate = ref<Date>() const dueDate = ref<Date>()

View File

@ -30,17 +30,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref,computed, watch} from 'vue' import {ref,computed, watch, type PropType} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import TaskModel from '@/models/task' import type { ITask } from '@/models/task'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
attachmentUpload: { attachmentUpload: {
@ -54,7 +54,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const task = ref<TaskModel>({description: ''}) const task = ref<ITask>({description: ''})
const saved = ref(false) const saved = ref(false)
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description. // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.

View File

@ -37,9 +37,9 @@ import Multiselect from '@/components/input/multiselect.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
import type UserModel from '@/models/user'
import ListUserService from '@/services/listUsers' import ListUserService from '@/services/listUsers'
import {success} from '@/message' import {success} from '@/message'
import type { IUser } from '@/models/user'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -54,7 +54,7 @@ const props = defineProps({
default: false, default: false,
}, },
modelValue: { modelValue: {
type: Array as PropType<UserModel[]>, type: Array as PropType<IUser[]>,
default: () => [], default: () => [],
}, },
}) })
@ -65,7 +65,7 @@ const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService()) const listUserService = shallowReactive(new ListUserService())
const foundUsers = ref([]) const foundUsers = ref([])
const assignees = ref<UserModel[]>([]) const assignees = ref<IUser[]>([])
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -78,13 +78,13 @@ watch(
}, },
) )
async function addAssignee(user: UserModel) { async function addAssignee(user: IUser) {
await store.dispatch('tasks/addAssignee', {user: user, taskId: props.taskId}) await store.dispatch('tasks/addAssignee', {user: user, taskId: props.taskId})
emit('update:modelValue', assignees.value) emit('update:modelValue', assignees.value)
success({message: t('task.assignee.assignSuccess')}) success({message: t('task.assignee.assignSuccess')})
} }
async function removeAssignee(user: UserModel) { async function removeAssignee(user: IUser) {
await store.dispatch('tasks/removeAssignee', {user: user, taskId: props.taskId}) await store.dispatch('tasks/removeAssignee', {user: user, taskId: props.taskId})
// Remove the assignee from the list // Remove the assignee from the list

View File

@ -43,7 +43,7 @@ import {type PropType, ref, computed, shallowReactive, watch} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import LabelModel from '@/models/label' import LabelModel, { type ILabel } from '@/models/label'
import LabelTaskService from '@/services/labelTask' import LabelTaskService from '@/services/labelTask'
import {success} from '@/message' import {success} from '@/message'
@ -52,7 +52,7 @@ import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Array as PropType<LabelModel[]>, type: Array as PropType<ILabel[]>,
default: () => [], default: () => [],
}, },
taskId: { taskId: {
@ -71,7 +71,7 @@ const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const labelTaskService = shallowReactive(new LabelTaskService()) const labelTaskService = shallowReactive(new LabelTaskService())
const labels = ref<LabelModel[]>([]) const labels = ref<ILabel[]>([])
const query = ref('') const query = ref('')
watch( watch(
@ -92,7 +92,7 @@ function findLabel(newQuery: string) {
query.value = newQuery query.value = newQuery
} }
async function addLabel(label: LabelModel, showNotification = true) { async function addLabel(label: ILabel, showNotification = true) {
const bubble = () => { const bubble = () => {
emit('update:modelValue', labels.value) emit('update:modelValue', labels.value)
emit('change', labels.value) emit('change', labels.value)
@ -110,7 +110,7 @@ async function addLabel(label: LabelModel, showNotification = true) {
} }
} }
async function removeLabel(label: LabelModel) { async function removeLabel(label: ILabel) {
if (props.taskId !== 0) { if (props.taskId !== 0) {
await store.dispatch('tasks/removeLabel', {label, taskId: props.taskId}) await store.dispatch('tasks/removeLabel', {label, taskId: props.taskId})
} }

View File

@ -32,18 +32,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed} from 'vue' import {ref, computed, type PropType} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import TaskModel from '@/models/task' import type {ITask} from '@/models/task'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
canWrite: { canWrite: {

View File

@ -74,7 +74,7 @@ import User from '../../../components/misc/user.vue'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import Labels from '../../../components/tasks/partials/labels.vue' import Labels from '../../../components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary.vue' import ChecklistSummary from './checklist-summary.vue'
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task' import {TASK_DEFAULT_COLOR, type ITask} from '@/models/task'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
@ -96,7 +96,7 @@ export default defineComponent({
}, },
props: { props: {
task: { task: {
type: Object as PropType<TaskModel>, type: Object as PropType<ITask>,
required: true, required: true,
}, },
loading: { loading: {
@ -117,7 +117,7 @@ export default defineComponent({
formatISO, formatISO,
formatDateSince, formatDateSince,
colorIsDark, colorIsDark,
async toggleTaskDone(task: TaskModel) { async toggleTaskDone(task: ITask) {
this.loadingInternal = true this.loadingInternal = true
try { try {
const done = !task.done const done = !task.done

View File

@ -11,12 +11,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type LabelModel from '@/models/label'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { ILabel } from '@/models/label'
defineProps({ defineProps({
labels: { labels: {
type: Array as PropType<LabelModel[]>, type: Array as PropType<ILabel[]>,
required: true, required: true,
}, },
}) })

View File

@ -21,15 +21,12 @@ import {reactive, ref, watch} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import ListModel from '@/models/list' import ListModel, { type IList } from '@/models/list'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<ListModel>, type: Object as PropType<IList>,
validator(value) {
return value instanceof ListModel
},
required: false, required: false,
}, },
}) })
@ -38,7 +35,7 @@ const emit = defineEmits(['update:modelValue'])
const store = useStore() const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const list: ListModel= reactive(new ListModel()) const list: IList = reactive(new ListModel())
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -57,7 +54,7 @@ function findLists(query: string) {
foundLists.value = store.getters['lists/searchList'](query) foundLists.value = store.getters['lists/searchList'](query)
} }
function select(l: ListModel | null) { function select(l: IList | null) {
Object.assign(list, l) Object.assign(list, l)
emit('update:modelValue', list) emit('update:modelValue', list)
} }

View File

@ -63,14 +63,14 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, watch, type PropType} from 'vue' import {ref, reactive, watch, type PropType} from 'vue'
import {error} from '@/message'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {TASK_REPEAT_MODES, type RepeatAfter} from '@/models/task'
import type TaskModel from '@/models/task' import {error} from '@/message'
import {TASK_REPEAT_MODES, type ITask, type RepeatAfter} from '@/models/task'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<TaskModel>, type: Object as PropType<ITask>,
default: () => ({}), default: () => ({}),
required: true, required: true,
}, },
@ -84,7 +84,7 @@ const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue', 'change'])
const task = ref<TaskModel>() const task = ref<ITask>()
const repeatAfter = reactive({ const repeatAfter = reactive({
amount: 0, amount: 0,
type: '', type: '',

View File

@ -98,7 +98,7 @@
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import TaskModel from '../../../models/task' import TaskModel, { type ITask } from '../../../models/task'
import PriorityLabel from './priorityLabel.vue' import PriorityLabel from './priorityLabel.vue'
import TaskService from '../../../services/task' import TaskService from '../../../services/task'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -129,7 +129,7 @@ export default defineComponent({
}, },
props: { props: {
theTask: { theTask: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
isArchived: { isArchived: {

View File

@ -1,22 +1,27 @@
import AttachmentModel from '@/models/attachment' import AttachmentModel, { type IAttachment } from '@/models/attachment'
import FileModel from '@/models/file' import type {IFile} from '@/models/file'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import { store } from '@/store' import { store } from '@/store'
export function uploadFile(taskId: number, file: FileModel, onSuccess: () => Function) { export function uploadFile(taskId: number, file: IFile, onSuccess: () => Function) {
const attachmentService = new AttachmentService() const attachmentService = new AttachmentService()
const files = [file] const files = [file]
return uploadFiles(attachmentService, taskId, files, onSuccess) return uploadFiles(attachmentService, taskId, files, onSuccess)
} }
export async function uploadFiles(attachmentService: AttachmentService, taskId: number, files: FileModel[], onSuccess : Function = () => {}) { export async function uploadFiles(
attachmentService: AttachmentService,
taskId: number,
files: IFile[],
onSuccess: Function = () => {},
) {
const attachmentModel = new AttachmentModel({taskId}) const attachmentModel = new AttachmentModel({taskId})
const response = await attachmentService.create(attachmentModel, files) const response = await attachmentService.create(attachmentModel, files)
console.debug(`Uploaded attachments for task ${taskId}, response was`, response) console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
response.success?.map((attachment: AttachmentModel) => { response.success?.map((attachment: IAttachment) => {
store.dispatch('tasks/addTaskAttachment', { store.dispatch('tasks/addTaskAttachment', {
taskId, taskId,
attachment, attachment,

View File

@ -1,8 +1,7 @@
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import type { IList } from '@/models/list'
import type ListModal from '@/models/list' export function getListTitle(l: IList) {
export function getListTitle(l: ListModal) {
if (l.id === -1) { if (l.id === -1) {
return i18n.global.t('list.pseudo.favorites.title') return i18n.global.t('list.pseudo.favorites.title')
} }

View File

@ -1,7 +1,7 @@
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import NamespaceModel from '@/models/namespace' import type {INamespace} from '@/models/namespace'
export const getNamespaceTitle = (n: NamespaceModel) => { export const getNamespaceTitle = (n: INamespace) => {
if (n.id === -1) { if (n.id === -1) {
return i18n.global.t('namespace.pseudo.sharedLists.title') return i18n.global.t('namespace.pseudo.sharedLists.title')
} }

View File

@ -1,18 +1,10 @@
import {createNewIndexer} from '../indexes' import {createNewIndexer} from '../indexes'
import type {LabelState} from '@/store/types'
import type {ILabel} from '@/models/label'
const {search} = createNewIndexer('labels', ['title', 'description']) const {search} = createNewIndexer('labels', ['title', 'description'])
export interface label {
id: number,
title: string,
}
interface labelState {
labels: {
[k: number]: label,
},
}
/** /**
* Checks if a list of labels is available in the store and filters them then query * Checks if a list of labels is available in the store and filters them then query
* @param {Object} state * @param {Object} state
@ -20,7 +12,7 @@ interface labelState {
* @param {String} query * @param {String} query
* @returns {Array} * @returns {Array}
*/ */
export function filterLabelsByQuery(state: labelState, labelsToHide: label[], query: string) { export function filterLabelsByQuery(state: LabelState, labelsToHide: ILabel[], query: string) {
const labelIdsToHide: number[] = labelsToHide.map(({id}) => id) const labelIdsToHide: number[] = labelsToHide.map(({id}) => id)
return search(query) return search(query)
@ -36,6 +28,6 @@ export function filterLabelsByQuery(state: labelState, labelsToHide: label[], qu
* @param {Array} ids * @param {Array} ids
* @returns {Array} * @returns {Array}
*/ */
export function getLabelsByIds(state: labelState, ids: number[]) { export function getLabelsByIds(state: LabelState, ids: ILabel['id'][]) {
return Object.values(state.labels).filter(({id}) => ids.includes(id)) return Object.values(state.labels).filter(({id}) => ids.includes(id))
} }

View File

@ -1,4 +1,4 @@
import ListModel from '@/models/list' import type {IList} from '@/models/list'
const key = 'collapsedBuckets' const key = 'collapsedBuckets'
@ -11,7 +11,10 @@ const getAllState = () => {
return JSON.parse(saved) return JSON.parse(saved)
} }
export const saveCollapsedBucketState = (listId: ListModel['id'], collapsedBuckets) => { export const saveCollapsedBucketState = (
listId: IList['id'],
collapsedBuckets,
) => {
const state = getAllState() const state = getAllState()
state[listId] = collapsedBuckets state[listId] = collapsedBuckets
for (const bucketId in state[listId]) { for (const bucketId in state[listId]) {
@ -22,7 +25,7 @@ export const saveCollapsedBucketState = (listId: ListModel['id'], collapsedBucke
localStorage.setItem(key, JSON.stringify(state)) localStorage.setItem(key, JSON.stringify(state))
} }
export const getCollapsedBucketState = (listId : ListModel['id']) => { export const getCollapsedBucketState = (listId : IList['id']) => {
const state = getAllState() const state = getAllState()
if (typeof state[listId] !== 'undefined') { if (typeof state[listId] !== 'undefined') {
return state[listId] return state[listId]

View File

@ -1,6 +1,6 @@
import ListModel from '@/models/list' import type {IList} from '@/models/list'
export function getSavedFilterIdFromListId(listId: ListModel['id']) { export function getSavedFilterIdFromListId(listId: IList['id']) {
let filterId = listId * -1 - 1 let filterId = listId * -1 - 1
// FilterIds from listIds are always positive // FilterIds from listIds are always positive
if (filterId < 0) { if (filterId < 0) {

View File

@ -1,8 +1,8 @@
export function findIndexById(array : [], id : string | number) { export function findIndexById<T extends {id: string | number}>(array : T[], id : string | number) {
return array.findIndex(({id: currentId}) => currentId === id) return array.findIndex(({id: currentId}) => currentId === id)
} }
export function findById(array : [], id : string | number) { export function findById<T extends {id: string | number}>(array : T[], id : string | number) {
return array.find(({id: currentId}) => currentId === id) return array.find(({id: currentId}) => currentId === id)
} }

View File

@ -2,7 +2,11 @@ import {objectToCamelCase} from '@/helpers/case'
import {omitBy, isNil} from '@/helpers/utils' import {omitBy, isNil} from '@/helpers/utils'
import type {Right} from '@/models/constants/rights' import type {Right} from '@/models/constants/rights'
export default class AbstractModel { export interface IAbstract {
maxRight: Right | null
}
export default class AbstractModel implements IAbstract {
/** /**
* The max right the user has on this object, as returned by the x-max-right header from the api. * The max right the user has on this object, as returned by the x-max-right header from the api.

View File

@ -1,12 +1,21 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserModel from './user' import UserModel, {type IUser} from './user'
import FileModel from './file' import FileModel, {type IFile} from './file'
import type {IAbstract} from './abstractModel'
export default class AttachmentModel extends AbstractModel { export interface IAttachment extends IAbstract {
id: number id: number
taskId: number taskId: number
createdBy: UserModel createdBy: IUser
file: FileModel file: IFile
created: Date
}
export default class AttachmentModel extends AbstractModel implements IAttachment {
declare id: number
declare taskId: number
createdBy: IUser
file: IFile
konrad marked this conversation as resolved Outdated

Why use declare for some properties instead of only defining the property?

Why use `declare` for some properties instead of only defining the property?

I had to use declare everywhere where a property 'isn't defined'. All of them are, but typescript doesn't understand that this happens mostly in the constructor of the abstract service.
Not sure if this here is the right way to do things. Might be one reason why the build fails.

I had to use declare everywhere where a property 'isn't defined'. All of them are, but typescript doesn't understand that this happens mostly in the constructor of the abstract service. Not sure if this here is the right way to do things. Might be one reason why the build fails.

Makes sense!

Makes sense!
created: Date created: Date
constructor(data) { constructor(data) {

View File

@ -1,9 +1,13 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from './abstractModel'
export type AVATAR_PROVIDERS = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload' export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload'
export default class AvatarModel extends AbstractModel { export interface IAvatar extends IAbstract {
avatarProvider: AVATAR_PROVIDERS avatarProvider: AvatarProvider
}
export default class AvatarModel extends AbstractModel implements IAvatar {
declare avatarProvider: AvatarProvider
defaults() { defaults() {
return { return {

View File

@ -1,6 +1,6 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from './abstractModel'
export default class BackgroundImageModel extends AbstractModel { export interface IBackgroundImage extends IAbstract {
id: number id: number
url: string url: string
thumb: string thumb: string
@ -9,6 +9,17 @@ export default class BackgroundImageModel extends AbstractModel {
authorName: string authorName: string
} }
blurHash: string blurHash: string
}
export default class BackgroundImageModel extends AbstractModel implements IBackgroundImage {
declare id: number
declare url: string
declare thumb: string
declare info: {
author: string
authorName: string
}
declare blurHash: string
defaults() { defaults() {
return { return {

View File

@ -1,17 +1,31 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from './abstractModel'
import UserModel from './user' import UserModel, { type IUser } from './user'
import TaskModel from './task' import TaskModel, { type ITask } from './task'
export default class BucketModel extends AbstractModel { export interface IBucket extends IAbstract {
id: number id: number
title: string title: string
listId: number listId: number
limit: number limit: number
tasks: TaskModel[] tasks: ITask[]
isDoneBucket: boolean isDoneBucket: boolean
position: number position: number
createdBy: UserModel createdBy: IUser
created: Date
updated: Date
}
export default class BucketModel extends AbstractModel implements IBucket {
declare id: number
declare title: string
declare listId: number
declare limit: number
declare tasks: ITask[]
declare isDoneBucket: boolean
declare position: number
createdBy: IUser
created: Date created: Date
updated: Date updated: Date

View File

@ -1,8 +1,13 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from './abstractModel'
export default class CaldavTokenModel extends AbstractModel { export interface ICaldavToken extends IAbstract {
id: number id: number;
created: Date created: Date;
}
export default class CaldavTokenModel extends AbstractModel implements ICaldavToken {
declare id: number
declare created: Date
constructor(data? : Object) { constructor(data? : Object) {
super(data) super(data)

View File

@ -1,8 +1,13 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
export default class EmailUpdateModel extends AbstractModel { interface IEmailUpdate {
newEmail: string newEmail: string
password: string password: string
}
export default class EmailUpdateModel extends AbstractModel implements IEmailUpdate {
declare newEmail: string
declare password: string
defaults() { defaults() {
return { return {

View File

@ -1,11 +1,19 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
export default class FileModel extends AbstractModel { export interface IFile {
id: number id: number
mime: string mime: string
name: string name: string
size: number size: number
created: Date created: Date
}
export default class FileModel extends AbstractModel implements IFile {
declare id: number
declare mime: string
declare name: string
declare size: number
created: Date
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,19 +1,33 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserModel from './user' import UserModel, { type IUser } from './user'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
const DEFAULT_LABEL_BACKGROUND_COLOR = 'e8e8e8' const DEFAULT_LABEL_BACKGROUND_COLOR = 'e8e8e8'
export default class LabelModel extends AbstractModel {
export interface ILabel {
id: number id: number
title: string title: string
hexColor: string hexColor: string
description: string description: string
createdBy: UserModel createdBy: IUser
listId: number listId: number
textColor: string textColor: string
created: Date created: Date
updated: Date updated: Date
}
export default class LabelModel extends AbstractModel implements ILabel {
declare id: number
declare title: string
declare hexColor: string
declare description: string
declare createdBy: IUser
declare listId: number
declare textColor: string
created: Date
updated: Date
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,9 +1,15 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
export default class LabelTask extends AbstractModel { interface ILabel {
id: number id: number
taskId: number taskId: number
labelId: number labelId: number
}
export default class LabelTask extends AbstractModel implements ILabel {
declare id: number
declare taskId: number
declare labelId: number
defaults() { defaults() {
return { return {

View File

@ -1,18 +1,31 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from './abstractModel'
import UserModel from './user' import UserModel, { type IUser } from './user'
import {RIGHTS, type Right} from '@/models/constants/rights' import {RIGHTS, type Right} from '@/models/constants/rights'
export default class LinkShareModel extends AbstractModel { export interface ILinkShare extends IAbstract {
id: number id: number
hash: string hash: string
right: Right right: Right
sharedBy: UserModel sharedBy: IUser
sharingType: number // FIXME: use correct numbers sharingType: number // FIXME: use correct numbers
listId: number listId: number
name: string name: string
password: string password: string
created: Date created: Date
updated: Date updated: Date
}
export default class LinkShareModel extends AbstractModel implements ILinkShare {
declare id: number
declare hash: string
declare right: Right
sharedBy: IUser
declare sharingType: number // FIXME: use correct numbers
declare listId: number
declare name: string
declare password: string
created: Date
updated: Date
constructor(data) { constructor(data) {
// The constructor of AbstractModel handles all the default parsing. // The constructor of AbstractModel handles all the default parsing.

View File

@ -1,28 +1,49 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from '@/models/abstractModel'
import TaskModel from './task' import TaskModel, { type ITask } from '@/models/task'
import UserModel from './user' import UserModel, { type IUser } from '@/models/user'
import type NamespaceModel from './namespace' import SubscriptionModel, { type ISubscription } from '@/models/subscription'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' import type { INamespace } from '@/models/namespace'
import SubscriptionModel from '@/models/subscription'
export default class ListModel extends AbstractModel { import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
export interface IList extends IAbstract {
id: number id: number
title: string title: string
description: string description: string
owner: UserModel owner: IUser
tasks: TaskModel[] tasks: ITask[]
namespaceId: NamespaceModel['id'] namespaceId: INamespace['id']
isArchived: boolean isArchived: boolean
hexColor: string hexColor: string
identifier: string identifier: string
backgroundInformation: any backgroundInformation: any
isFavorite: boolean isFavorite: boolean
subscription: SubscriptionModel subscription: ISubscription
position: number position: number
backgroundBlurHash: string backgroundBlurHash: string
created: Date created: Date
updated: Date updated: Date
}
export default class ListModel extends AbstractModel implements IList {
declare id: number
declare title: string
declare description: string
owner: IUser
tasks: ITask[]
declare namespaceId: INamespace['id']
declare isArchived: boolean
declare hexColor: string
declare identifier: string
declare backgroundInformation: any
declare isFavorite: boolean
declare subscription: ISubscription
declare position: number
declare backgroundBlurHash: string
created: Date
updated: Date
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,11 +1,17 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import ListModel from './list' import ListModel, { type IList } from './list'
import NamespaceModel from './namespace' import type { INamespace } from './namespace'
export default class ListDuplicateModel extends AbstractModel { export interface ListDuplicate {
listId: number listId: number
namespaceId: NamespaceModel['id'] namespaceId: INamespace['id']
list: ListModel list: IList
}
export default class ListDuplicateModel extends AbstractModel implements ListDuplicate {
declare listId: number
declare namespaceId: INamespace['id']
list: IList
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,17 +1,31 @@
import AbstractModel from './abstractModel' import AbstractModel, { type IAbstract } from './abstractModel'
import ListModel from './list' import ListModel, { type IList } from './list'
import UserModel from './user' import UserModel, { type IUser } from './user'
import SubscriptionModel from '@/models/subscription' import SubscriptionModel, { type ISubscription } from '@/models/subscription'
export default class NamespaceModel extends AbstractModel { export interface INamespace extends IAbstract {
id: number id: number
title: string title: string
description: string description: string
owner: UserModel owner: IUser
lists: ListModel[] lists: IList[]
isArchived: boolean isArchived: boolean
hexColor: string hexColor: string
subscription: SubscriptionModel subscription: ISubscription
created: Date
updated: Date
}
export default class NamespaceModel extends AbstractModel implements INamespace {
declare id: number
declare title: string
declare description: string
owner: IUser
lists: IList[]
declare isArchived: boolean
declare hexColor: string
declare subscription: ISubscription
created: Date created: Date
updated: Date updated: Date

View File

@ -1,10 +1,10 @@
import AbstractModel from '@/models/abstractModel' import AbstractModel from '@/models/abstractModel'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import UserModel from '@/models/user' import UserModel, { type IUser } from '@/models/user'
import TaskModel from '@/models/task' import TaskModel, { type ITask } from '@/models/task'
import TaskCommentModel from '@/models/taskComment' import TaskCommentModel, { type ITaskComment } from '@/models/taskComment'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import TeamModel from '@/models/team' import TeamModel, { type ITeam } from '@/models/team'
export const NOTIFICATION_NAMES = { export const NOTIFICATION_NAMES = {
'TASK_COMMENT': 'task.comment', 'TASK_COMMENT': 'task.comment',
@ -15,32 +15,32 @@ export const NOTIFICATION_NAMES = {
} as const } as const
interface Notification { interface Notification {
doer: UserModel doer: IUser
} }
interface NotificationTask extends Notification { interface NotificationTask extends Notification {
task: TaskModel task: ITask
comment: TaskCommentModel comment: ITaskComment
} }
interface NotificationAssigned extends Notification { interface NotificationAssigned extends Notification {
task: TaskModel task: ITask
assignee: UserModel assignee: IUser
} }
interface NotificationDeleted extends Notification { interface NotificationDeleted extends Notification {
task: TaskModel task: ITask
} }
interface NotificationCreated extends Notification { interface NotificationCreated extends Notification {
task: TaskModel task: ITask
} }
interface NotificationMemberAdded extends Notification { interface NotificationMemberAdded extends Notification {
member: UserModel member: IUser
team: TeamModel team: ITeam
} }
export default class NotificationModel extends AbstractModel { export interface INotification {
id: number id: number
name: string name: string
notification: NotificationTask | NotificationAssigned | NotificationDeleted | NotificationCreated | NotificationMemberAdded notification: NotificationTask | NotificationAssigned | NotificationDeleted | NotificationCreated | NotificationMemberAdded
@ -48,6 +48,16 @@ export default class NotificationModel extends AbstractModel {
readAt: Date | null readAt: Date | null
created: Date created: Date
}
export default class NotificationModel extends AbstractModel implements INotification {
declare id: number
declare name: string
declare notification: NotificationTask | NotificationAssigned | NotificationDeleted | NotificationCreated | NotificationMemberAdded
declare read: boolean
readAt: Date | null
created: Date
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,9 +1,15 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
export default class PasswordResetModel extends AbstractModel { export interface IPasswordReset {
token: string token: string
newPassword: string newPassword: string
email: string email: string
}
export default class PasswordResetModel extends AbstractModel implements IPasswordReset {
token: string
declare newPassword: string
declare email: string
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,8 +1,13 @@
import AbstractModel from './abstractModel' import AbstractModel from '@/models/abstractModel'
export default class PasswordUpdateModel extends AbstractModel { export interface IPasswordUpdate {
newPassword: string newPassword: string
oldPassword: string oldPassword: string
}
export default class PasswordUpdateModel extends AbstractModel implements IPasswordUpdate {
declare newPassword: string
declare oldPassword: string
defaults() { defaults() {
return { return {

View File

@ -1,7 +1,7 @@
import AbstractModel from '@/models/abstractModel' import AbstractModel from '@/models/abstractModel'
import UserModel from '@/models/user' import UserModel, { type IUser } from '@/models/user'
export default class SavedFilterModel extends AbstractModel { export interface ISavedFilter {
id: 0 id: 0
title: string title: string
description: string description: string
@ -15,7 +15,26 @@ export default class SavedFilterModel extends AbstractModel {
filterIncludeNulls: boolean filterIncludeNulls: boolean
} }
owner: any owner: IUser
created: Date
updated: Date
}
export default class SavedFilterModel extends AbstractModel implements ISavedFilter {
declare id: 0
declare title: string
declare description: string
declare filters: {
sortBy: ('done' | 'id')[]
orderBy: ('asc' | 'desc')[]
filterBy: 'done'[]
filterValue: 'false'[]
filterComparator: 'equals'[]
filterConcat: 'and'
filterIncludeNulls: boolean
}
owner: IUser
created: Date created: Date
updated: Date updated: Date

View File

@ -1,11 +1,20 @@
import AbstractModel from '@/models/abstractModel' import AbstractModel from '@/models/abstractModel'
import UserModel from '@/models/user' import UserModel, { type IUser } from '@/models/user'
export default class SubscriptionModel extends AbstractModel { export interface ISubscription {
id: number id: number
entity: string // FIXME: correct type? entity: string // FIXME: correct type?
entityId: number // FIXME: correct type? entityId: number // FIXME: correct type?
user: UserModel user: IUser
created: Date
}
export default class SubscriptionModel extends AbstractModel implements ISubscription {
declare id: number
declare entity: string // FIXME: correct type?
declare entityId: number // FIXME: correct type?
user: IUser
created: Date created: Date

View File

@ -1,12 +1,14 @@
import AbstractModel from './abstractModel' import type { Priority } from '@/models/constants/priorities'
import UserModel from './user'
import LabelModel from './label' import AbstractModel from '@/models/abstractModel'
import AttachmentModel from './attachment' import UserModel, { type IUser } from '@/models/user'
import LabelModel, { type ILabel } from '@/models/label'
import AttachmentModel, {type IAttachment} from '@/models/attachment'
import SubscriptionModel, { type ISubscription } from '@/models/subscription'
import type { IList } from '@/models/list'
import SubscriptionModel from '@/models/subscription'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import type ListModel from './list' import type { IBucket } from './bucket'
import type { Priority } from './constants/priorities'
const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype
export const TASK_DEFAULT_COLOR = '#1973ff' export const TASK_DEFAULT_COLOR = '#1973ff'
@ -24,15 +26,15 @@ export interface RepeatAfter {
amount: number amount: number
} }
export default class TaskModel extends AbstractModel { export interface ITask {
id: number id: number
title: string title: string
description: string description: string
done: boolean done: boolean
doneAt: Date | null doneAt: Date | null
priority: Priority priority: Priority
labels: LabelModel[] labels: ILabel[]
assignees: UserModel[] assignees: IUser[]
dueDate: Date | null dueDate: Date | null
startDate: Date | null startDate: Date | null
@ -41,26 +43,64 @@ export default class TaskModel extends AbstractModel {
repeatFromCurrentDate: boolean repeatFromCurrentDate: boolean
repeatMode: TaskRepeatMode repeatMode: TaskRepeatMode
reminderDates: Date[] reminderDates: Date[]
parentTaskId: TaskModel['id'] parentTaskId: ITask['id']
hexColor: string hexColor: string
percentDone: number percentDone: number
relatedTasks: { [relationKind: string]: TaskModel } // FIXME: use relationKinds relatedTasks: { [relationKind: string]: ITask } // FIXME: use relationKinds
attachments: AttachmentModel[] attachments: IAttachment[]
identifier: string identifier: string
index: number index: number
isFavorite: boolean isFavorite: boolean
subscription: SubscriptionModel subscription: ISubscription
position: number position: number
kanbanPosition: number kanbanPosition: number
createdBy: UserModel createdBy: IUser
dpschen marked this conversation as resolved Outdated

Probably my lack of typescript knowledge speaking here: I've seen this a few times now, is there a special reason to use IList['id'] instead of IList.id?

Probably my lack of typescript knowledge speaking here: I've seen this a few times now, is there a special reason to use `IList['id']` instead of `IList.id`?

IList is a type and not a namespace. Afaik you can only access types in namespaces like IList.id. To access properties of types you need that bracket notation.

IList is a type and not a namespace. Afaik you can only access types in namespaces like `IList.id`. To access properties of types you need that bracket notation.
created: Date created: Date
updated: Date updated: Date
listId: ListModel['id'] // Meta, only used when creating a new task listId: IList['id'] // Meta, only used when creating a new task
bucketId: IBucket['id']
}
constructor(data: Partial<TaskModel>) { export default class TaskModel extends AbstractModel implements ITask {
id: number
title: string
declare description: string
declare done: boolean
doneAt: Date | null
declare priority: Priority
labels: ILabel[]
assignees: IUser[]
dueDate: Date | null
startDate: Date | null
endDate: Date | null
declare repeatAfter: number | RepeatAfter
declare repeatFromCurrentDate: boolean
declare repeatMode: TaskRepeatMode
reminderDates: Date[]
declare parentTaskId: ITask['id']
declare hexColor: string
declare percentDone: number
declare relatedTasks: { [relationKind: string]: ITask } // FIXME: use relationKinds
attachments: IAttachment[]
declare identifier: string
declare index: number
declare isFavorite: boolean
declare subscription: ISubscription
declare position: number
declare kanbanPosition: number
createdBy: IUser
created: Date
updated: Date
listId: IList['id'] // Meta, only used when creating a new task
constructor(data: Partial<ITask>) {
super(data) super(data)
this.id = Number(this.id) this.id = Number(this.id)
@ -120,6 +160,7 @@ export default class TaskModel extends AbstractModel {
this.listId = Number(this.listId) this.listId = Number(this.listId)
} }
bucketId: number
defaults() { defaults() {
return { return {

View File

@ -1,11 +1,17 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import type UserModel from './user' import type { ITask } from './task'
import type TaskModel from './task' import type { IUser } from './user'
export default class TaskAssigneeModel extends AbstractModel { export interface ITaskAssignee {
created: Date created: Date
userId: UserModel['id'] userId: IUser['id']
taskId: TaskModel['id'] taskId: ITask['id']
}
export default class TaskAssigneeModel extends AbstractModel implements ITaskAssignee {
created: Date
declare userId: IUser['id']
declare taskId: ITask['id']
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -1,12 +1,22 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserModel from './user' import UserModel, { type IUser } from './user'
import type TaskModel from './task' import type { ITask } from './task'
export default class TaskCommentModel extends AbstractModel { export interface ITaskComment {
id: number id: number
taskId: TaskModel['id'] taskId: ITask['id']
comment: string comment: string
author: UserModel author: IUser
created: Date
updated: Date
}
export default class TaskCommentModel extends AbstractModel implements ITaskComment {
declare id: number
declare taskId: ITask['id']
declare comment: string
author: IUser
created: Date created: Date
updated: Date updated: Date

View File

@ -1,6 +1,6 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserModel from './user' import UserModel, { type IUser } from './user'
import type TaskModel from './task' import type { ITask } from './task'
export const RELATION_KIND = { export const RELATION_KIND = {
'SUBTASK': 'subtask', 'SUBTASK': 'subtask',
@ -19,13 +19,23 @@ export const RELATION_KINDS = [...Object.values(RELATION_KIND)] as const
export type RelationKind = typeof RELATION_KINDS[number] export type RelationKind = typeof RELATION_KINDS[number]
export default class TaskRelationModel extends AbstractModel { export interface ITaskRelationModel {
id: number id: number
otherTaskId: TaskModel['id'] otherTaskId: ITask['id']
taskId: TaskModel['id'] taskId: ITask['id']
relationKind: RelationKind relationKind: RelationKind
createdBy: UserModel createdBy: IUser
created: Date
}
export default class TaskRelationModel extends AbstractModel implements ITaskRelationModel {
declare id: number
declare otherTaskId: ITask['id']
declare taskId: ITask['id']
declare relationKind: RelationKind
createdBy: IUser
created: Date created: Date
constructor(data) { constructor(data) {

View File

@ -1,16 +1,28 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserModel from './user' import UserModel, { type IUser } from './user'
import TeamMemberModel from './teamMember' import TeamMemberModel, { type ITeamMember } from './teamMember'
import {RIGHTS, type Right} from '@/models/constants/rights' import {RIGHTS, type Right} from '@/models/constants/rights'
export default class TeamModel extends AbstractModel { export interface ITeam {
id: 0 id: number
name: string name: string
description: string description: string
members: TeamMemberModel[] members: ITeamMember[]
right: Right right: Right
createdBy: UserModel createdBy: IUser
created: Date
updated: Date
}
export default class TeamModel extends AbstractModel implements ITeam {
declare id: number
declare name: string
declare description: string
members: ITeamMember[]
declare right: Right
createdBy: IUser
created: Date created: Date
updated: Date updated: Date

View File

@ -1,8 +1,12 @@
import TeamShareBaseModel from './teamShareBase' import TeamShareBaseModel from './teamShareBase'
import type ListModel from './list' import type { IList } from './list'
export default class TeamListModel extends TeamShareBaseModel { export interface ITeamList {
listId: ListModel['id'] listId: IList['id']
}
export default class TeamListModel extends TeamShareBaseModel implements ITeamList {
declare listId: IList['id']
defaults() { defaults() {
return { return {

View File

@ -1,9 +1,14 @@
import UserModel from './user' import UserModel from './user'
import type ListModel from './list' import type { IList } from './list'
export default class TeamMemberModel extends UserModel { export interface ITeamMember {
admin: boolean admin: boolean
teamId: ListModel['id'] teamId: IList['id']
}
export default class TeamMemberModel extends UserModel implements ITeamMember {
declare admin: boolean
declare teamId: IList['id']
defaults() { defaults() {
return { return {

View File

@ -1,8 +1,12 @@
import TeamShareBaseModel from './teamShareBase' import TeamShareBaseModel from './teamShareBase'
import type NamespaceModel from './namespace' import type { INamespace } from './namespace'
export default class TeamNamespaceModel extends TeamShareBaseModel { export interface ITeamNamespace {
namespaceId: NamespaceModel['id'] namespaceId: INamespace['id']
}
export default class TeamNamespaceModel extends TeamShareBaseModel implements ITeamNamespace {
declare namespaceId: INamespace['id']
defaults() { defaults() {
return { return {

View File

@ -1,14 +1,22 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import type TeamModel from './team'
import {RIGHTS, type Right} from '@/models/constants/rights' import {RIGHTS, type Right} from '@/models/constants/rights'
import type { ITeam } from './team'
export interface ITeamShareBase {
teamId: ITeam['id']
right: Right
created: Date
updated: Date
}
/** /**
* This class is a base class for common team sharing model. * This class is a base class for common team sharing model.
* It is extended in a way so it can be used for namespaces as well for lists. * It is extended in a way so it can be used for namespaces as well for lists.
*/ */
export default class TeamShareBaseModel extends AbstractModel { export default class TeamShareBaseModel extends AbstractModel implements ITeamShareBase {
teamId: TeamModel['id'] declare teamId: ITeam['id']
right: Right declare right: Right
created: Date created: Date
updated: Date updated: Date

View File

@ -1,9 +1,15 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
export default class TotpModel extends AbstractModel { export interface ITotp {
secret: string secret: string
enabled: boolean enabled: boolean
url: string url: string
}
export default class TotpModel extends AbstractModel implements ITotp{
declare secret: string
declare enabled: boolean
declare url: string
defaults() { defaults() {
return { return {

View File

@ -1,7 +1,7 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserSettingsModel from '@/models/userSettings' import UserSettingsModel, { type IUserSettings } from '@/models/userSettings'
export default class UserModel extends AbstractModel { export interface IUser {
id: number id: number
email: string email: string
username: string username: string
@ -9,7 +9,18 @@ export default class UserModel extends AbstractModel {
created: Date created: Date
updated: Date updated: Date
settings: UserSettingsModel settings: IUserSettings
}
export default class UserModel extends AbstractModel implements IUser {
declare id: number
declare email: string
declare username: string
declare name: string
created: Date
updated: Date
settings: IUserSettings
constructor(data) { constructor(data) {
super(data) super(data)
@ -28,6 +39,7 @@ export default class UserModel extends AbstractModel {
email: '', email: '',
username: '', username: '',
name: '', name: '',
created: null, created: null,
updated: null, updated: null,
settings: null, settings: null,

View File

@ -1,8 +1,13 @@
import UserShareBaseModel from './userShareBase' import UserShareBaseModel from './userShareBase'
import type ListModel from './list' import type { IList } from './list'
export interface IUserList {
listId: IList['id']
}
// This class extends the user share model with a 'rights' parameter which is used in sharing // This class extends the user share model with a 'rights' parameter which is used in sharing
export default class UserListModel extends UserShareBaseModel { export default class UserListModel extends UserShareBaseModel implements IUserList {
listId: ListModel['id'] declare listId: IList['id']
defaults() { defaults() {
return { return {

View File

@ -1,9 +1,13 @@
import UserShareBaseModel from './userShareBase' import UserShareBaseModel from './userShareBase'
import type NamespaceModel from './namespace' import type { INamespace } from './namespace'
export interface IUserNamespace {
namespaceId: INamespace['id']
}
// This class extends the user share model with a 'rights' parameter which is used in sharing // This class extends the user share model with a 'rights' parameter which is used in sharing
export default class UserNamespaceModel extends UserShareBaseModel { export default class UserNamespaceModel extends UserShareBaseModel implements IUserNamespace {
namespaceId: NamespaceModel['id'] declare namespaceId: INamespace['id']
defaults() { defaults() {
return { return {

View File

@ -1,16 +1,27 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import type ListModel from './list' import type { IList } from './list'
export default class UserSettingsModel extends AbstractModel { export interface IUserSettings {
name: string name: string
emailRemindersEnabled: boolean emailRemindersEnabled: boolean
discoverableByName: boolean discoverableByName: boolean
discoverableByEmail: boolean discoverableByEmail: boolean
overdueTasksRemindersEnabled: boolean overdueTasksRemindersEnabled: boolean
defaultListId: undefined | ListModel['id'] defaultListId: undefined | IList['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6 weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string timezone: string
}
export default class UserSettingsModel extends AbstractModel implements IUserSettings {
declare name: string
declare emailRemindersEnabled: boolean
declare discoverableByName: boolean
declare discoverableByEmail: boolean
declare overdueTasksRemindersEnabled: boolean
declare defaultListId: undefined | IList['id']
declare weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
declare timezone: string
defaults() { defaults() {
return { return {

View File

@ -1,13 +1,21 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import type UserModel from './user'
import {RIGHTS, type Right} from '@/models/constants/rights' import {RIGHTS, type Right} from '@/models/constants/rights'
import type { IUser } from './user'
export default class UserShareBaseModel extends AbstractModel { export interface IUserShareBase {
userId: UserModel['id'] userId: IUser['id']
right: Right right: Right
created: Date created: Date
updated: Date updated: Date
}
export default class UserShareBaseModel extends AbstractModel implements IUserShareBase {
declare userId: IUser['id']
declare right: Right
created: Date
updated: Date
constructor(data) { constructor(data) {
super(data) super(data)

View File

@ -3,7 +3,7 @@ import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
import {parseTaskText} from './parseTaskText' import {parseTaskText} from './parseTaskText'
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate' import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval' import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import {PRIORITIES} from '@/models/constants/priorities.ts' import {PRIORITIES} from '@/models/constants/priorities'
describe('Parse Task Text', () => { describe('Parse Task Text', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -2,9 +2,9 @@ import {AuthenticatedHTTPFactory} from '@/http-common'
import type {Method} from 'axios' import type {Method} from 'axios'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import AbstractModel from '@/models/abstractModel' import AbstractModel, { type IAbstract } from '@/models/abstractModel'
import type { Right } from '@/models/constants/rights' import type { Right } from '@/models/constants/rights'
import type FileModel from '@/models/file' import type { IFile } from '@/models/file'
interface Paths { interface Paths {
create : string create : string
@ -12,6 +12,7 @@ interface Paths {
getAll : string getAll : string
update : string update : string
delete : string delete : string
reset?: string
} }
function convertObject(o: Record<string, unknown>) { function convertObject(o: Record<string, unknown>) {
@ -39,7 +40,7 @@ function prepareParams(params: Record<string, unknown | unknown[]>) {
return objectToSnakeCase(params) return objectToSnakeCase(params)
} }
export default class AbstractService<Model extends AbstractModel = AbstractModel> { export default class AbstractService<Model extends IAbstract = IAbstract> {
///////////////////////////// /////////////////////////////
// Initial variable definitions // Initial variable definitions
@ -269,7 +270,7 @@ export default class AbstractService<Model extends AbstractModel = AbstractModel
* This is a more abstract implementation which only does a get request. * This is a more abstract implementation which only does a get request.
* Services which need more flexibility can use this. * Services which need more flexibility can use this.
*/ */
async getM(url : string, model = new AbstractModel({}) as Model, params: Record<string, unknown> = {}) { async getM(url : string, model : Model = new AbstractModel({}), params: Record<string, unknown> = {}) {
const cancel = this.setLoading() const cancel = this.setLoading()
model = this.beforeGet(model) model = this.beforeGet(model)
@ -285,7 +286,7 @@ export default class AbstractService<Model extends AbstractModel = AbstractModel
} }
} }
async getBlobUrl(url : string, method = 'GET' as Method, data = {}) { async getBlobUrl(url : string, method : Method = 'GET', data = {}) {
const response = await this.http({ const response = await this.http({
url, url,
method, method,
@ -302,7 +303,7 @@ export default class AbstractService<Model extends AbstractModel = AbstractModel
* @param params Optional query parameters * @param params Optional query parameters
* @param page The page to get * @param page The page to get
*/ */
async getAll(model : Model = new AbstractModel({}) as Model, params = {}, page = 1) { async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
if (this.paths.getAll === '') { if (this.paths.getAll === '') {
throw new Error('This model is not able to get data.') throw new Error('This model is not able to get data.')
} }
@ -408,10 +409,10 @@ export default class AbstractService<Model extends AbstractModel = AbstractModel
/** /**
* Uploads a file to a url. * Uploads a file to a url.
* @param url * @param url
* @param file {FileModel} * @param file {IFile}
* @param fieldName The name of the field the file is uploaded to. * @param fieldName The name of the field the file is uploaded to.
*/ */
uploadFile(url : string, file: FileModel, fieldName : string) { uploadFile(url : string, file: IFile, fieldName : string) {
return this.uploadBlob(url, new Blob([file]), fieldName, file.name) return this.uploadBlob(url, new Blob([file]), fieldName, file.name)
} }

View File

@ -1,8 +1,8 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import AttachmentModel from '../models/attachment' import AttachmentModel, { type IAttachment } from '../models/attachment'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import {downloadBlob} from '@/helpers/downloadBlob' import {downloadBlob} from '@/helpers/downloadBlob'
import type FileModel from '@/models/file' import type { IFile } from '@/models/file'
export default class AttachmentService extends AbstractService<AttachmentModel> { export default class AttachmentService extends AbstractService<AttachmentModel> {
constructor() { constructor() {
@ -13,7 +13,7 @@ export default class AttachmentService extends AbstractService<AttachmentModel>
}) })
} }
processModel(model: AttachmentModel) { processModel(model: IAttachment) {
model.created = formatISO(new Date(model.created)) model.created = formatISO(new Date(model.created))
return model return model
} }
@ -34,11 +34,11 @@ export default class AttachmentService extends AbstractService<AttachmentModel>
return data return data
} }
getBlobUrl(model: AttachmentModel) { getBlobUrl(model: IAttachment) {
return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id) return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id)
} }
async download(model: AttachmentModel) { async download(model: IAttachment) {
const url = await this.getBlobUrl(model) const url = await this.getBlobUrl(model)
return downloadBlob(url, model.file.name) return downloadBlob(url, model.file.name)
} }
@ -48,7 +48,7 @@ export default class AttachmentService extends AbstractService<AttachmentModel>
* @param files * @param files
* @returns {Promise<any|never>} * @returns {Promise<any|never>}
*/ */
create(model: AttachmentModel, files: FileModel[]) { create(model: IAttachment, files: IFile[]) {
const data = new FormData() const data = new FormData()
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
// TODO: Validation of file size // TODO: Validation of file size

View File

@ -1,7 +1,7 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import AvatarModel from '../models/avatar' import AvatarModel, { type IAvatar } from '../models/avatar'
export default class AvatarService extends AbstractService { export default class AvatarService extends AbstractService<IAvatar> {
constructor() { constructor() {
super({ super({
get: '/user/settings/avatar', get: '/user/settings/avatar',

View File

@ -1,8 +1,8 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import BackgroundImageModel from '../models/backgroundImage' import BackgroundImageModel, { type IBackgroundImage } from '../models/backgroundImage'
import ListModel from '../models/list' import ListModel from '@/models/list'
export default class BackgroundUnsplashService extends AbstractService { export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
constructor() { constructor() {
super({ super({
getAll: '/backgrounds/unsplash/search', getAll: '/backgrounds/unsplash/search',

View File

@ -1,6 +1,6 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import ListModel from '../models/list' import ListModel, { type IList } from '../models/list'
import type FileModel from '@/models/file' import type { IFile } from '@/models/file'
export default class BackgroundUploadService extends AbstractService { export default class BackgroundUploadService extends AbstractService {
constructor() { constructor() {
@ -22,7 +22,7 @@ export default class BackgroundUploadService extends AbstractService {
* @param file * @param file
* @returns {Promise<any|never>} * @returns {Promise<any|never>}
*/ */
create(listId: ListModel['id'], file: FileModel) { create(listId: IList['id'], file: IFile) {
return this.uploadFile( return this.uploadFile(
this.getReplacedRoute(this.paths.create, {listId}), this.getReplacedRoute(this.paths.create, {listId}),
file, file,

View File

@ -1,8 +1,8 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import BucketModel from '../models/bucket' import BucketModel, { type IBucket } from '../models/bucket'
import TaskService from '@/services/task' import TaskService from '@/services/task'
export default class BucketService extends AbstractService { export default class BucketService extends AbstractService<IBucket> {
constructor() { constructor() {
super({ super({
getAll: '/lists/{listId}/buckets', getAll: '/lists/{listId}/buckets',

View File

@ -1,8 +1,8 @@
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import CaldavTokenModel from '../models/caldavToken' import CaldavTokenModel, {type ICaldavToken} from '../models/caldavToken'
import AbstractService from './abstractService' import AbstractService from './abstractService'
export default class CaldavTokenService extends AbstractService { export default class CaldavTokenService extends AbstractService<ICaldavToken> {
constructor() { constructor() {
super({ super({
getAll: '/user/settings/token/caldav', getAll: '/user/settings/token/caldav',
@ -11,7 +11,7 @@ export default class CaldavTokenService extends AbstractService {
}) })
} }
processModel(model: Partial<CaldavTokenModel>) { processModel(model: Partial<ICaldavToken>) {
return { return {
...model, ...model,
created: formatISO(new Date(model.created)), created: formatISO(new Date(model.created)),

View File

@ -1,9 +1,9 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import LabelModel from '../models/label' import LabelModel, { type ILabel } from '@/models/label'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import {colorFromHex} from '@/helpers/color/colorFromHex' import {colorFromHex} from '@/helpers/color/colorFromHex'
export default class LabelService extends AbstractService { export default class LabelService extends AbstractService<ILabel> {
constructor() { constructor() {
super({ super({
create: '/labels', create: '/labels',

View File

@ -1,10 +1,10 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import ListModel from '../models/list' import ListModel, { type IList } from '@/models/list'
import TaskService from './task' import TaskService from './task'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import {colorFromHex} from '@/helpers/color/colorFromHex' import {colorFromHex} from '@/helpers/color/colorFromHex'
export default class ListService extends AbstractService { export default class ListService extends AbstractService<IList> {
constructor() { constructor() {
super({ super({
create: '/namespaces/{namespaceId}/lists', create: '/namespaces/{namespaceId}/lists',

View File

@ -1,9 +1,9 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import NamespaceModel from '../models/namespace' import NamespaceModel, { type INamespace } from '../models/namespace'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
import {colorFromHex} from '@/helpers/color/colorFromHex' import {colorFromHex} from '@/helpers/color/colorFromHex'
export default class NamespaceService extends AbstractService { export default class NamespaceService extends AbstractService<INamespace> {
constructor() { constructor() {
super({ super({
create: '/namespaces', create: '/namespaces',

View File

@ -1,5 +1,5 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import TaskModel from '../models/task' import TaskModel, { type ITask } from '../models/task'
import AttachmentService from './attachment' import AttachmentService from './attachment'
import LabelService from './label' import LabelService from './label'
@ -113,7 +113,7 @@ export default class TaskService extends AbstractService {
model.labels = model.labels.map(l => labelService.processModel(l)) model.labels = model.labels.map(l => labelService.processModel(l))
} }
return model as TaskModel return model as ITask
} }
} }

View File

@ -1,8 +1,8 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import TaskCommentModel from '../models/taskComment' import TaskCommentModel, { type ITaskComment } from '../models/taskComment'
import {formatISO} from 'date-fns' import {formatISO} from 'date-fns'
export default class TaskCommentService extends AbstractService { export default class TaskCommentService extends AbstractService<ITaskComment> {
constructor() { constructor() {
super({ super({
create: '/tasks/{taskId}/comments', create: '/tasks/{taskId}/comments',

View File

@ -1,4 +1,6 @@
import type { ActionContext } from 'vuex'
import {LOADING, LOADING_MODULE} from './mutation-types' import {LOADING, LOADING_MODULE} from './mutation-types'
import type { RootStoreState } from './types'
/** /**
* This helper sets the loading state with a 100ms delay to avoid flickering. * This helper sets the loading state with a 100ms delay to avoid flickering.
@ -7,7 +9,11 @@ import {LOADING, LOADING_MODULE} from './mutation-types'
* @param {null|String} module The module that is loading. This parameter allows components to listen for specific parts of the application loading. * @param {null|String} module The module that is loading. This parameter allows components to listen for specific parts of the application loading.
* @param {null|function} loadFunc If not null, this function will be executed instead of the default setting loading. * @param {null|function} loadFunc If not null, this function will be executed instead of the default setting loading.
*/ */
export const setLoading = (context, module = null, loadFunc = null) => { export function setLoading<State>(
context : ActionContext<State, RootStoreState>,
module : string | null = null,
loadFunc : (() => void) | null = null,
) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (loadFunc === null) { if (loadFunc === null) {
context.commit(LOADING, true, {root: true}) context.commit(LOADING, true, {root: true})

View File

@ -25,7 +25,9 @@ import ListModel from '@/models/list'
import ListService from '../services/list' import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl' import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
export const store = createStore({ import type { RootStoreState } from './types'
export const store = createStore<RootStoreState>({
strict: import.meta.env.DEV, strict: import.meta.env.DEV,
modules: { modules: {
config, config,
@ -37,7 +39,7 @@ export const store = createStore({
attachments, attachments,
labels, labels,
}, },
state: { state: () => ({
loading: false, loading: false,
loadingModule: null, loadingModule: null,
// This is used to highlight the current list in menu for all list related views // This is used to highlight the current list in menu for all list related views
@ -51,7 +53,7 @@ export const store = createStore({
menuActive: true, menuActive: true,
keyboardShortcutsActive: false, keyboardShortcutsActive: false,
quickActionsActive: false, quickActionsActive: false,
}, }),
mutations: { mutations: {
[LOADING](state, loading) { [LOADING](state, loading) {
state.loading = loading state.loading = loading

View File

@ -1,21 +1,24 @@
import {findIndexById} from '@/helpers/utils' import {findIndexById} from '@/helpers/utils'
import type { AttachmentState } from '@/store/types'
import type { IAttachment } from '@/models/attachment'
export default { export default {
namespaced: true, namespaced: true,
state: () => ({ state: (): AttachmentState => ({
attachments: [], attachments: [],
}), }),
mutations: { mutations: {
set(state, attachments) { set(state: AttachmentState, attachments: IAttachment[]) {
console.debug('Set attachments', attachments) console.debug('Set attachments', attachments)
state.attachments = attachments state.attachments = attachments
}, },
add(state, attachment) { add(state: AttachmentState, attachment: IAttachment) {
console.debug('Add attachement', attachment) console.debug('Add attachement', attachment)
state.attachments.push(attachment) state.attachments.push(attachment)
}, },
removeById(state, id) { removeById(state: AttachmentState, id: IAttachment['id']) {
const attachmentIndex = findIndexById(state.attachments, id) const attachmentIndex = findIndexById<IAttachment>(state.attachments, id)
state.attachments.splice(attachmentIndex, 1) state.attachments.splice(attachmentIndex, 1)
console.debug('Remove attachement', id) console.debug('Remove attachement', id)
}, },

View File

@ -1,3 +1,5 @@
import type { ActionContext } from 'vuex'
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common' import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n' import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
@ -8,12 +10,10 @@ import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper' import {setLoading} from '@/store/helper'
import {success} from '@/message' import {success} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider' import {redirectToProvider} from '@/helpers/redirectToProvider'
import type { RootStoreState, AuthState, Info} from '@/store/types'
import {AUTH_TYPES} from '@/store/types'
import type { IUserSettings } from '@/models/userSettings'
const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
}
const defaultSettings = settings => { const defaultSettings = settings => {
if (typeof settings.weekStart === 'undefined' || settings.weekStart === '') { if (typeof settings.weekStart === 'undefined' || settings.weekStart === '') {
@ -24,7 +24,7 @@ const defaultSettings = settings => {
export default { export default {
namespaced: true, namespaced: true,
state: () => ({ state: (): AuthState => ({
authenticated: false, authenticated: false,
isLinkShareAuth: false, isLinkShareAuth: false,
info: null, info: null,
@ -34,13 +34,13 @@ export default {
settings: {}, settings: {},
}), }),
getters: { getters: {
authUser(state) { authUser(state: AuthState) {
return state.authenticated && ( return state.authenticated && (
state.info && state.info &&
state.info.type === AUTH_TYPES.USER state.info.type === AUTH_TYPES.USER
) )
}, },
authLinkShare(state) { authLinkShare(state: AuthState) {
return state.authenticated && ( return state.authenticated && (
state.info && state.info &&
state.info.type === AUTH_TYPES.LINK_SHARE state.info.type === AUTH_TYPES.LINK_SHARE
@ -48,7 +48,7 @@ export default {
}, },
}, },
mutations: { mutations: {
info(state, info) { info(state: AuthState, info: Info) {
state.info = info state.info = info
if (info !== null) { if (info !== null) {
state.avatarUrl = info.getAvatarUrl() state.avatarUrl = info.getAvatarUrl()
@ -60,31 +60,32 @@ export default {
state.isLinkShareAuth = info.id < 0 state.isLinkShareAuth = info.id < 0
} }
}, },
setUserSettings(state, settings) { setUserSettings(state: AuthState, settings: IUserSettings) {
state.settings = defaultSettings(settings) state.settings = defaultSettings(settings)
const info = state.info !== null ? state.info : {} const info = state.info !== null ? state.info : {} as Info
info.name = settings.name info.name = settings.name
state.info = info state.info = info
}, },
authenticated(state, authenticated) { authenticated(state: AuthState, authenticated: boolean) {
state.authenticated = authenticated state.authenticated = authenticated
}, },
isLinkShareAuth(state, is) { isLinkShareAuth(state: AuthState, isLinkShareAuth: boolean) {
state.isLinkShareAuth = is state.isLinkShareAuth = isLinkShareAuth
}, },
needsTotpPasscode(state, needs) { needsTotpPasscode(state: AuthState, needsTotpPasscode: boolean) {
state.needsTotpPasscode = needs state.needsTotpPasscode = needsTotpPasscode
}, },
reloadAvatar(state) { reloadAvatar(state: AuthState) {
if (!state.info) return
state.avatarUrl = `${state.info.getAvatarUrl()}&=${+new Date()}` state.avatarUrl = `${state.info.getAvatarUrl()}&=${+new Date()}`
}, },
lastUserRefresh(state) { lastUserRefresh(state: AuthState) {
state.lastUserInfoRefresh = new Date() state.lastUserInfoRefresh = new Date()
}, },
}, },
actions: { actions: {
// Logs a user in with a set of credentials. // Logs a user in with a set of credentials.
async login(ctx, credentials) { async login(ctx: ActionContext<AuthState, RootStoreState>, credentials) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true}) ctx.commit(LOADING, true, {root: true})
@ -115,7 +116,7 @@ export default {
// Registers a new user and logs them in. // Registers a new user and logs them in.
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited. // Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
async register(ctx, credentials) { async register(ctx: ActionContext<AuthState, RootStoreState>, credentials) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true}) ctx.commit(LOADING, true, {root: true})
try { try {
@ -132,7 +133,7 @@ export default {
} }
}, },
async openIdAuth(ctx, {provider, code}) { async openIdAuth(ctx: ActionContext<AuthState, RootStoreState>, {provider, code}) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true}) ctx.commit(LOADING, true, {root: true})
@ -154,7 +155,7 @@ export default {
} }
}, },
async linkShareAuth(ctx, {hash, password}) { async linkShareAuth(ctx: ActionContext<AuthState, RootStoreState>, {hash, password}) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
const response = await HTTP.post('/shares/' + hash + '/auth', { const response = await HTTP.post('/shares/' + hash + '/auth', {
password: password, password: password,
@ -165,7 +166,7 @@ export default {
}, },
// Populates user information from jwt token saved in local storage in store // Populates user information from jwt token saved in local storage in store
checkAuth(ctx) { checkAuth(ctx: ActionContext<AuthState, RootStoreState>) {
// This function can be called from multiple places at the same time and shortly after one another. // This function can be called from multiple places at the same time and shortly after one another.
// To prevent hitting the api too frequently or race conditions, we check at most once per minute. // To prevent hitting the api too frequently or race conditions, we check at most once per minute.
@ -197,7 +198,7 @@ export default {
} }
}, },
redirectToProviderIfNothingElseIsEnabled({rootState}) { redirectToProviderIfNothingElseIsEnabled({rootState}: ActionContext<AuthState, RootStoreState>) {
const {auth} = rootState.config const {auth} = rootState.config
if ( if (
auth.local.enabled === false && auth.local.enabled === false &&
@ -209,7 +210,7 @@ export default {
} }
}, },
async refreshUserInfo({state, commit, dispatch}) { async refreshUserInfo({state, commit, dispatch}: ActionContext<AuthState, RootStoreState>) {
const jwt = getToken() const jwt = getToken()
if (!jwt) { if (!jwt) {
return return
@ -243,7 +244,7 @@ export default {
} }
}, },
async saveUserSettings(ctx, payload) { async saveUserSettings(ctx: ActionContext<AuthState, RootStoreState>, payload) {
const {settings} = payload const {settings} = payload
const showMessage = payload.showMessage ?? true const showMessage = payload.showMessage ?? true
const userSettingsService = new UserSettingsService() const userSettingsService = new UserSettingsService()
@ -264,7 +265,7 @@ export default {
}, },
// Renews the api token and saves it to local storage // Renews the api token and saves it to local storage
renewToken(ctx) { renewToken(ctx: ActionContext<AuthState, RootStoreState>) {
// FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a // FIXME: Timeout to avoid race conditions when authenticated as a user (=auth token in localStorage) and as a
// link share in another tab. Without the timeout both the token renew and link share auth are executed at // link share in another tab. Without the timeout both the token renew and link share auth are executed at
// the same time and one might win over the other. // the same time and one might win over the other.
@ -285,7 +286,7 @@ export default {
} }
}, 5000) }, 5000)
}, },
logout(ctx) { logout(ctx: ActionContext<AuthState, RootStoreState>) {
removeToken() removeToken()
window.localStorage.clear() // Clear all settings and history we might have saved in local storage. window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')

View File

@ -1,11 +1,14 @@
import type { ActionContext } from 'vuex'
import {parseURL} from 'ufo'
import {CONFIG} from '../mutation-types' import {CONFIG} from '../mutation-types'
import {HTTPFactory} from '@/http-common' import {HTTPFactory} from '@/http-common'
import {objectToCamelCase} from '@/helpers/case' import {objectToCamelCase} from '@/helpers/case'
import {parseURL} from 'ufo' import type { RootStoreState, ConfigState } from '@/store/types'
export default { export default {
namespaced: true, namespaced: true,
state: () => ({ state: (): ConfigState => ({
// These are the api defaults. // These are the api defaults.
version: '', version: '',
frontendUrl: '', frontendUrl: '',
@ -36,19 +39,19 @@ export default {
}, },
}), }),
getters: { getters: {
migratorsEnabled: state => state.availableMigrators?.length > 0, migratorsEnabled: (state: ConfigState) => state.availableMigrators?.length > 0,
apiBase() { apiBase() {
const {host, protocol} = parseURL(window.API_URL) const {host, protocol} = parseURL(window.API_URL)
return protocol + '//' + host return protocol + '//' + host
}, },
}, },
mutations: { mutations: {
[CONFIG](state, config) { [CONFIG](state: ConfigState, config: ConfigState) {
Object.assign(state, config) Object.assign(state, config)
}, },
}, },
actions: { actions: {
async update(ctx) { async update(ctx: ActionContext<ConfigState, RootStoreState>) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info') const {data: config} = await HTTP.get('info')
ctx.commit(CONFIG, objectToCamelCase(config)) ctx.commit(CONFIG, objectToCamelCase(config))

View File

@ -7,10 +7,15 @@ import {success} from '@/message'
import BucketService from '../../services/bucket' import BucketService from '../../services/bucket'
import {setLoading} from '../helper' import {setLoading} from '../helper'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
import type { ActionContext } from 'vuex'
import type { RootStoreState, KanbanState } from '@/store/types'
import type { ITask } from '@/models/task'
import type { IList } from '@/models/list'
import type { IBucket } from '@/models/bucket'
const TASKS_PER_BUCKET = 25 const TASKS_PER_BUCKET = 25
function getTaskIndicesById(state, taskId) { function getTaskIndicesById(state: KanbanState, taskId: ITask['id']) {
let taskIndex let taskIndex
const bucketIndex = state.buckets.findIndex(({ tasks }) => { const bucketIndex = state.buckets.findIndex(({ tasks }) => {
taskIndex = findIndexById(tasks, taskId) taskIndex = findIndexById(tasks, taskId)
@ -23,7 +28,7 @@ function getTaskIndicesById(state, taskId) {
} }
} }
const addTaskToBucketAndSort = (state, task) => { const addTaskToBucketAndSort = (state: KanbanState, task: ITask) => {
const bucketIndex = findIndexById(state.buckets, task.bucketId) const bucketIndex = findIndexById(state.buckets, task.bucketId)
state.buckets[bucketIndex].tasks.push(task) state.buckets[bucketIndex].tasks.push(task)
state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1) state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1)
@ -36,7 +41,7 @@ const addTaskToBucketAndSort = (state, task) => {
export default { export default {
namespaced: true, namespaced: true,
state: () => ({ state: (): KanbanState => ({
buckets: [], buckets: [],
listId: 0, listId: 0,
bucketLoading: {}, bucketLoading: {},
@ -45,11 +50,11 @@ export default {
}), }),
mutations: { mutations: {
setListId(state, listId) { setListId(state: KanbanState, listId: IList['id']) {
state.listId = parseInt(listId) state.listId = parseInt(listId)
}, },
setBuckets(state, buckets) { setBuckets(state: KanbanState, buckets: IBucket[]) {
state.buckets = buckets state.buckets = buckets
buckets.forEach(b => { buckets.forEach(b => {
state.taskPagesPerBucket[b.id] = 1 state.taskPagesPerBucket[b.id] = 1
@ -57,31 +62,51 @@ export default {
}) })
}, },
addBucket(state, bucket) { addBucket(state: KanbanState, bucket: IBucket) {
state.buckets.push(bucket) state.buckets.push(bucket)
}, },
removeBucket(state, bucket) { removeBucket(state: KanbanState, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id) const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets.splice(bucketIndex, 1) state.buckets.splice(bucketIndex, 1)
}, },
setBucketById(state, bucket) { setBucketById(state: KanbanState, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id) const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets[bucketIndex] = bucket state.buckets[bucketIndex] = bucket
}, },
setBucketByIndex(state, {bucketIndex, bucket}) { setBucketByIndex(state: KanbanState, {
bucketIndex,
bucket,
} : {
bucketIndex: number,
bucket: IBucket
}) {
state.buckets[bucketIndex] = bucket state.buckets[bucketIndex] = bucket
}, },
setTaskInBucketByIndex(state, {bucketIndex, taskIndex, task}) { setTaskInBucketByIndex(state: KanbanState, {
bucketIndex,
taskIndex,
task,
} : {
bucketIndex: number,
taskIndex: number,
task: ITask
}) {
const bucket = state.buckets[bucketIndex] const bucket = state.buckets[bucketIndex]
bucket.tasks[taskIndex] = task bucket.tasks[taskIndex] = task
state.buckets[bucketIndex] = bucket state.buckets[bucketIndex] = bucket
}, },
setTasksInBucketByBucketId(state, {bucketId, tasks}) { setTasksInBucketByBucketId(state: KanbanState, {
bucketId,
tasks,
} : {
bucketId: IBucket['id'],
tasks: ITask[],
}) {
const bucketIndex = findIndexById(state.buckets, bucketId) const bucketIndex = findIndexById(state.buckets, bucketId)
state.buckets[bucketIndex] = { state.buckets[bucketIndex] = {
...state.buckets[bucketIndex], ...state.buckets[bucketIndex],
@ -89,7 +114,7 @@ export default {
} }
}, },
setTaskInBucket(state, task) { setTaskInBucket(state: KanbanState, task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) { if (state.buckets.length === 0) {
return return
@ -133,7 +158,7 @@ export default {
} }
}, },
addTaskToBucket(state, task) { addTaskToBucket(state: KanbanState, task: ITask) {
const bucketIndex = findIndexById(state.buckets, task.bucketId) const bucketIndex = findIndexById(state.buckets, task.bucketId)
const oldBucket = state.buckets[bucketIndex] const oldBucket = state.buckets[bucketIndex]
const newBucket = { const newBucket = {
@ -146,7 +171,10 @@ export default {
state.buckets[bucketIndex] = newBucket state.buckets[bucketIndex] = newBucket
}, },
addTasksToBucket(state, {tasks, bucketId}) { addTasksToBucket(state: KanbanState, {tasks, bucketId}: {
dpschen marked this conversation as resolved Outdated

Shouldn't the state parameter be typed as well here?

Shouldn't the `state` parameter be typed as well here?

It should already be typed by the vuex Module type

It should already be typed by the [vuex Module type](https://kolaente.dev/vikunja/frontend/src/commit/c1f5f92fa164637040198c1a1b8de73f9ed9e0c7/src/store/modules/kanban.ts#L41)
tasks: ITask[];
bucketId: IBucket['id'];
}) {
const bucketIndex = findIndexById(state.buckets, bucketId) const bucketIndex = findIndexById(state.buckets, bucketId)
const oldBucket = state.buckets[bucketIndex] const oldBucket = state.buckets[bucketIndex]
const newBucket = { const newBucket = {
@ -159,7 +187,7 @@ export default {
state.buckets[bucketIndex] = newBucket state.buckets[bucketIndex] = newBucket
}, },
removeTaskInBucket(state, task) { removeTaskInBucket(state: KanbanState, task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task // If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) { if (state.buckets.length === 0) {
return return
@ -168,8 +196,10 @@ export default {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, task.id) const { bucketIndex, taskIndex } = getTaskIndicesById(state, task.id)
if ( if (
!bucketIndex ||
state.buckets[bucketIndex]?.id !== task.bucketId || state.buckets[bucketIndex]?.id !== task.bucketId ||
state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id !taskIndex ||
(state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
) { ) {
return return
} }
@ -177,39 +207,40 @@ export default {
state.buckets[bucketIndex].tasks.splice(taskIndex, 1) state.buckets[bucketIndex].tasks.splice(taskIndex, 1)
}, },
setBucketLoading(state, {bucketId, loading}) { setBucketLoading(state: KanbanState, {bucketId, loading}) {
state.bucketLoading[bucketId] = loading state.bucketLoading[bucketId] = loading
}, },
setTasksLoadedForBucketPage(state, {bucketId, page}) { setTasksLoadedForBucketPage(state: KanbanState, {bucketId, page}) {
state.taskPagesPerBucket[bucketId] = page state.taskPagesPerBucket[bucketId] = page
}, },
setAllTasksLoadedForBucket(state, bucketId) { setAllTasksLoadedForBucket(state: KanbanState, bucketId) {
state.allTasksLoadedForBucket[bucketId] = true state.allTasksLoadedForBucket[bucketId] = true
}, },
}, },
getters: { getters: {
getBucketById(state) { getBucketById(state: KanbanState) {
return (bucketId) => findById(state.buckets, bucketId) return (bucketId) => findById(state.buckets, bucketId)
}, },
getTaskById(state) { getTaskById(state: KanbanState) {
return (id) => { return (id) => {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, id) const { bucketIndex, taskIndex } = getTaskIndicesById(state, id)
return { return {
bucketIndex, bucketIndex,
taskIndex, taskIndex,
task: state.buckets[bucketIndex]?.tasks?.[taskIndex] || null, task: bucketIndex && taskIndex && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
} }
} }
}, },
}, },
actions: { actions: {
async loadBucketsForList(ctx, {listId, params}) { async loadBucketsForList(ctx: ActionContext<KanbanState, RootStoreState>, {listId, params}) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
@ -219,7 +250,7 @@ export default {
const bucketService = new BucketService() const bucketService = new BucketService()
try { try {
const response = await bucketService.getAll({listId: listId}, params) const response = await bucketService.getAll({listId}, params)
ctx.commit('setBuckets', response) ctx.commit('setBuckets', response)
ctx.commit('setListId', listId) ctx.commit('setListId', listId)
return response return response
@ -228,7 +259,7 @@ export default {
} }
}, },
async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) { async loadNextTasksForBucket(ctx: ActionContext<KanbanState, RootStoreState>, {listId, ps = {}, bucketId}) {
const isLoading = ctx.state.bucketLoading[bucketId] ?? false const isLoading = ctx.state.bucketLoading[bucketId] ?? false
if (isLoading) { if (isLoading) {
return return
@ -270,7 +301,7 @@ export default {
const taskService = new TaskCollectionService() const taskService = new TaskCollectionService()
try { try {
const tasks = await taskService.getAll({listId: listId}, params, page) const tasks = await taskService.getAll({listId}, params, page)
ctx.commit('addTasksToBucket', {tasks, bucketId: bucketId}) ctx.commit('addTasksToBucket', {tasks, bucketId: bucketId})
ctx.commit('setTasksLoadedForBucketPage', {bucketId, page}) ctx.commit('setTasksLoadedForBucketPage', {bucketId, page})
if (taskService.totalPages <= page) { if (taskService.totalPages <= page) {
@ -283,7 +314,7 @@ export default {
} }
}, },
async createBucket(ctx, bucket) { async createBucket(ctx: ActionContext<KanbanState, RootStoreState>, bucket: IBucket) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketService = new BucketService() const bucketService = new BucketService()
@ -296,7 +327,7 @@ export default {
} }
}, },
async deleteBucket(ctx, {bucket, params}) { async deleteBucket(ctx: ActionContext<KanbanState, RootStoreState>, {bucket, params}) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketService = new BucketService() const bucketService = new BucketService()
@ -304,14 +335,14 @@ export default {
const response = await bucketService.delete(bucket) const response = await bucketService.delete(bucket)
ctx.commit('removeBucket', bucket) ctx.commit('removeBucket', bucket)
// We reload all buckets because tasks are being moved from the deleted bucket // We reload all buckets because tasks are being moved from the deleted bucket
ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params: params}) ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params})
return response return response
} finally { } finally {
cancel() cancel()
} }
}, },
async updateBucket(ctx, updatedBucketData) { async updateBucket(ctx: ActionContext<KanbanState, RootStoreState>, updatedBucketData) {
const cancel = setLoading(ctx, 'kanban') const cancel = setLoading(ctx, 'kanban')
const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id) const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id)
@ -339,10 +370,10 @@ export default {
} }
}, },
async updateBucketTitle(ctx, { id, title }) { async updateBucketTitle(ctx: ActionContext<KanbanState, RootStoreState>, { id, title }) {
const bucket = findById(ctx.state.buckets, id) const bucket = findById(ctx.state.buckets, id)
if (bucket.title === title) { if (bucket?.title === title) {
// bucket title has not changed // bucket title has not changed
return return
} }

View File

@ -1,15 +1,19 @@
import type { ActionContext } from 'vuex'
import {i18n} from '@/i18n'
import {success} from '@/message'
import LabelService from '@/services/label' import LabelService from '@/services/label'
import {setLoading} from '@/store/helper' import {setLoading} from '@/store/helper'
import {success} from '@/message' import type { LabelState, RootStoreState } from '@/store/types'
import {i18n} from '@/i18n'
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels' import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import type { ILabel } from '@/models/label'
const {add, remove, update} = createNewIndexer('labels', ['title', 'description']) const {add, remove, update} = createNewIndexer('labels', ['title', 'description'])
async function getAllLabels(page = 1) { async function getAllLabels(page = 1): Promise<ILabel[]> {
const labelService = new LabelService() const labelService = new LabelService()
const labels = await labelService.getAll({}, {}, page) const labels = await labelService.getAll({}, {}, page) as ILabel[]
if (page < labelService.totalPages) { if (page < labelService.totalPages) {
const nextLabels = await getAllLabels(page + 1) const nextLabels = await getAllLabels(page + 1)
return labels.concat(nextLabels) return labels.concat(nextLabels)
@ -20,45 +24,44 @@ async function getAllLabels(page = 1) {
export default { export default {
namespaced: true, namespaced: true,
state: () => ({ state: (): LabelState => ({
// The labels are stored as an object which has the label ids as keys.
labels: {}, labels: {},
loaded: false, loaded: false,
}), }),
mutations: { mutations: {
setLabels(state, labels) { setLabels(state: LabelState, labels: ILabel[]) {
labels.forEach(l => { labels.forEach(l => {
state.labels[l.id] = l state.labels[l.id] = l
add(l) add(l)
}) })
}, },
setLabel(state, label) { setLabel(state: LabelState, label: ILabel) {
state.labels[label.id] = label state.labels[label.id] = label
update(label) update(label)
}, },
removeLabelById(state, label) { removeLabelById(state: LabelState, label: ILabel) {
remove(label) remove(label)
delete state.labels[label.id] delete state.labels[label.id]
}, },
setLoaded(state, loaded) { setLoaded(state: LabelState, loaded: boolean) {
state.loaded = loaded state.loaded = loaded
}, },
}, },
getters: { getters: {
getLabelsByIds(state) { getLabelsByIds(state: LabelState) {
return (ids) => getLabelsByIds(state, ids) return (ids: ILabel['id'][]) => getLabelsByIds(state, ids)
}, },
filterLabelsByQuery(state) { filterLabelsByQuery(state: LabelState) {
return (labelsToHide, query) => filterLabelsByQuery(state, labelsToHide, query) return (labelsToHide: ILabel[], query: string) => filterLabelsByQuery(state, labelsToHide, query)
}, },
getLabelsByExactTitles(state) { getLabelsByExactTitles(state: LabelState) {
return labelTitles => Object return labelTitles => Object
.values(state.labels) .values(state.labels)
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase())) .filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
}, },
}, },
actions: { actions: {
async loadAllLabels(ctx, {forceLoad} = {}) { async loadAllLabels(ctx: ActionContext<LabelState, RootStoreState>, {forceLoad} = {}) {
if (ctx.state.loaded && !forceLoad) { if (ctx.state.loaded && !forceLoad) {
return return
} }
@ -74,7 +77,7 @@ export default {
cancel() cancel()
} }
}, },
async deleteLabel(ctx, label) { async deleteLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService() const labelService = new LabelService()
@ -87,7 +90,7 @@ export default {
cancel() cancel()
} }
}, },
async updateLabel(ctx, label) { async updateLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService() const labelService = new LabelService()
@ -100,7 +103,7 @@ export default {
cancel() cancel()
} }
}, },
async createLabel(ctx, label) { async createLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels') const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService() const labelService = new LabelService()

View File

@ -2,6 +2,9 @@ import ListService from '@/services/list'
import {setLoading} from '@/store/helper' import {setLoading} from '@/store/helper'
import {removeListFromHistory} from '@/modules/listHistory' import {removeListFromHistory} from '@/modules/listHistory'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import type {ListState, RootStoreState} from '@/store/types'
import type {ActionContext} from 'vuex'
import type {IList} from '@/models/list'
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description']) const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
@ -10,37 +13,37 @@ const FavoriteListsNamespace = -2
export default { export default {
namespaced: true, namespaced: true,
// The state is an object which has the list ids as keys. // The state is an object which has the list ids as keys.
state: () => ({}), state: (): ListState => ({}),
mutations: { mutations: {
setList(state, list) { setList(state: ListState, list: IList) {
state[list.id] = list state[list.id] = list
update(list) update(list)
}, },
setLists(state, lists) { setLists(state: ListState, lists: IList[]) {
lists.forEach(l => { lists.forEach(l => {
state[l.id] = l state[l.id] = l
add(l) add(l)
}) })
}, },
removeListById(state, list) { removeListById(state: ListState, list: IList) {
remove(list) remove(list)
delete state[list.id] delete state[list.id]
}, },
}, },
getters: { getters: {
getListById: state => id => { getListById: (state: ListState) => (id: IList['id']) => {
if (typeof state[id] !== 'undefined') { if (typeof state[id] !== 'undefined') {
return state[id] return state[id]
} }
return null return null
}, },
findListByExactname: state => name => { findListByExactname: (state: ListState) => (name: string) => {
const list = Object.values(state).find(l => { const list = Object.values(state).find(l => {
return l.title.toLowerCase() === name.toLowerCase() return l.title.toLowerCase() === name.toLowerCase()
}) })
return typeof list === 'undefined' ? null : list return typeof list === 'undefined' ? null : list
}, },
searchList: state => (query, includeArchived = false) => { searchList: (state: ListState) => (query: string, includeArchived = false) => {
return search(query) return search(query)
?.filter(value => value > 0) ?.filter(value => value > 0)
.map(id => state[id]) .map(id => state[id])
@ -49,14 +52,14 @@ export default {
}, },
}, },
actions: { actions: {
toggleListFavorite(ctx, list) { toggleListFavorite(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
return ctx.dispatch('updateList', { return ctx.dispatch('updateList', {
...list, ...list,
isFavorite: !list.isFavorite, isFavorite: !list.isFavorite,
}) })
}, },
async createList(ctx, list) { async createList(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
const listService = new ListService() const listService = new ListService()
@ -71,7 +74,7 @@ export default {
} }
}, },
async updateList(ctx, list) { async updateList(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
const listService = new ListService() const listService = new ListService()
@ -106,7 +109,7 @@ export default {
} }
}, },
async deleteList(ctx, list) { async deleteList(ctx: ActionContext<ListState, RootStoreState>, list: IList) {
const cancel = setLoading(ctx, 'lists') const cancel = setLoading(ctx, 'lists')
const listService = new ListService() const listService = new ListService()

View File

@ -1,22 +1,27 @@
import type {ActionContext} from 'vuex'
import NamespaceService from '../../services/namespace' import NamespaceService from '../../services/namespace'
import {setLoading} from '@/store/helper' import {setLoading} from '@/store/helper'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import type {NamespaceState, RootStoreState} from '@/store/types'
import type {INamespace} from '@/models/namespace'
import type {IList} from '@/models/list'
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description']) const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
export default { export default {
namespaced: true, namespaced: true,
state: () => ({ state: (): NamespaceState => ({
namespaces: [], namespaces: [],
}), }),
mutations: { mutations: {
namespaces(state, namespaces) { namespaces(state: NamespaceState, namespaces: INamespace[]) {
state.namespaces = namespaces state.namespaces = namespaces
namespaces.forEach(n => { namespaces.forEach(n => {
add(n) add(n)
}) })
}, },
setNamespaceById(state, namespace) { setNamespaceById(state: NamespaceState, namespace: INamespace) {
const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id) const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
if (namespaceIndex === -1) { if (namespaceIndex === -1) {
@ -30,7 +35,7 @@ export default {
state.namespaces[namespaceIndex] = namespace state.namespaces[namespaceIndex] = namespace
update(namespace) update(namespace)
}, },
setListInNamespaceById(state, list) { setListInNamespaceById(state: NamespaceState, list: IList) {
for (const n in state.namespaces) { for (const n in state.namespaces) {
// We don't have the namespace id on the list which means we need to loop over all lists until we find it. // We don't have the namespace id on the list which means we need to loop over all lists until we find it.
// FIXME: Not ideal at all - we should fix that at the api level. // FIXME: Not ideal at all - we should fix that at the api level.
@ -46,11 +51,11 @@ export default {
} }
} }
}, },
addNamespace(state, namespace) { addNamespace(state: NamespaceState, namespace: INamespace) {
state.namespaces.push(namespace) state.namespaces.push(namespace)
add(namespace) add(namespace)
}, },
removeNamespaceById(state, namespaceId) { removeNamespaceById(state: NamespaceState, namespaceId: INamespace['id']) {
for (const n in state.namespaces) { for (const n in state.namespaces) {
if (state.namespaces[n].id === namespaceId) { if (state.namespaces[n].id === namespaceId) {
remove(state.namespaces[n]) remove(state.namespaces[n])
@ -59,7 +64,7 @@ export default {
} }
} }
}, },
addListToNamespace(state, list) { addListToNamespace(state: NamespaceState, list: IList) {
for (const n in state.namespaces) { for (const n in state.namespaces) {
if (state.namespaces[n].id === list.namespaceId) { if (state.namespaces[n].id === list.namespaceId) {
state.namespaces[n].lists.push(list) state.namespaces[n].lists.push(list)
@ -67,7 +72,7 @@ export default {
} }
} }
}, },
removeListFromNamespaceById(state, list) { removeListFromNamespaceById(state: NamespaceState, list: IList) {
for (const n in state.namespaces) { for (const n in state.namespaces) {
// We don't have the namespace id on the list which means we need to loop over all lists until we find it. // We don't have the namespace id on the list which means we need to loop over all lists until we find it.
// FIXME: Not ideal at all - we should fix that at the api level. // FIXME: Not ideal at all - we should fix that at the api level.
@ -83,7 +88,7 @@ export default {
}, },
}, },
getters: { getters: {
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => { getListAndNamespaceById: (state: NamespaceState) => (listId: IList['id'], ignorePseudoNamespaces = false) => {
for (const n in state.namespaces) { for (const n in state.namespaces) {
if (ignorePseudoNamespaces && state.namespaces[n].id < 0) { if (ignorePseudoNamespaces && state.namespaces[n].id < 0) {
@ -101,10 +106,10 @@ export default {
} }
return null return null
}, },
getNamespaceById: state => namespaceId => { getNamespaceById: (state: NamespaceState) => (namespaceId: INamespace['id']) => {
return state.namespaces.find(({id}) => id == namespaceId) || null return state.namespaces.find(({id}) => id == namespaceId) || null
}, },
searchNamespace: (state, getters) => query => { searchNamespace: (state: NamespaceState, getters) => (query: string) => {
return search(query) return search(query)
?.filter(value => value > 0) ?.filter(value => value > 0)
.map(getters.getNamespaceById) .map(getters.getNamespaceById)
@ -113,7 +118,7 @@ export default {
}, },
}, },
actions: { actions: {
async loadNamespaces(ctx) { async loadNamespaces(ctx: ActionContext<NamespaceState, RootStoreState>) {
const cancel = setLoading(ctx, 'namespaces') const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
@ -133,20 +138,20 @@ export default {
} }
}, },
loadNamespacesIfFavoritesDontExist(ctx) { loadNamespacesIfFavoritesDontExist(ctx: ActionContext<NamespaceState, RootStoreState>) {
// The first or second namespace should be the one holding all favorites // The first or second namespace should be the one holding all favorites
if (ctx.state.namespaces[0].id !== -2 && ctx.state.namespaces[1]?.id !== -2) { if (ctx.state.namespaces[0].id !== -2 && ctx.state.namespaces[1]?.id !== -2) {
return ctx.dispatch('loadNamespaces') return ctx.dispatch('loadNamespaces')
} }
}, },
removeFavoritesNamespaceIfEmpty(ctx) { removeFavoritesNamespaceIfEmpty(ctx: ActionContext<NamespaceState, RootStoreState>) {
if (ctx.state.namespaces[0].id === -2 && ctx.state.namespaces[0].lists.length === 0) { if (ctx.state.namespaces[0].id === -2 && ctx.state.namespaces[0].lists.length === 0) {
ctx.state.namespaces.splice(0, 1) ctx.state.namespaces.splice(0, 1)
} }
}, },
async deleteNamespace(ctx, namespace) { async deleteNamespace(ctx: ActionContext<NamespaceState, RootStoreState>, namespace: INamespace) {
const cancel = setLoading(ctx, 'namespaces') const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
@ -159,7 +164,7 @@ export default {
} }
}, },
async createNamespace(ctx, namespace) { async createNamespace(ctx: ActionContext<NamespaceState, RootStoreState>, namespace: INamespace) {
const cancel = setLoading(ctx, 'namespaces') const cancel = setLoading(ctx, 'namespaces')
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()

View File

@ -1,20 +1,25 @@
import router from '@/router' import router from '@/router'
import type { ActionContext } from 'vuex'
import {formatISO} from 'date-fns'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskAssigneeService from '@/services/taskAssignee' import TaskAssigneeService from '@/services/taskAssignee'
import TaskAssigneeModel from '../../models/taskAssignee' import TaskAssigneeModel from '@/models/taskAssignee'
import LabelTaskModel from '../../models/labelTask' import LabelTaskModel from '@/models/labelTask'
import LabelTaskService from '@/services/labelTask' import LabelTaskService from '@/services/labelTask'
import {HAS_TASKS} from '../mutation-types' import {HAS_TASKS} from '../mutation-types'
import {setLoading} from '../helper' import {setLoading} from '../helper'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {parseTaskText} from '@/modules/parseTaskText' import {parseTaskText} from '@/modules/parseTaskText'
import TaskModel from '@/models/task' import TaskModel, { type ITask } from '@/models/task'
import {formatISO} from 'date-fns'
import LabelTask from '@/models/labelTask' import LabelTask from '@/models/labelTask'
import LabelModel from '@/models/label' import LabelModel, { type ILabel } from '@/models/label'
import UserService from '@/services/user' import UserService from '@/services/user'
import type { RootStoreState, TaskState } from '@/store/types'
import type { IUser } from '@/models/user'
import type { IAttachment } from '@/models/attachment'
import type { IList } from '@/models/list'
// IDEA: maybe use a small fuzzy search here to prevent errors // IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value) { function findPropertyByValue(object, key, value) {
@ -24,16 +29,16 @@ function findPropertyByValue(object, key, value) {
} }
// Check if the user exists // Check if the user exists
function validateUsername(users, username) { function validateUsername(users: IUser[], username: IUser['username']) {
return findPropertyByValue(users, 'username', username) return findPropertyByValue(users, 'username', username)
} }
// Check if the label exists // Check if the label exists
function validateLabel(labels, label) { function validateLabel(labels: ILabel[], label: ILabel) {
return findPropertyByValue(labels, 'title', label) return findPropertyByValue(labels, 'title', label)
} }
async function addLabelToTask(task, label) { async function addLabelToTask(task: ITask, label: ILabel) {
const labelTask = new LabelTask({ const labelTask = new LabelTask({
taskId: task.id, taskId: task.id,
labelId: label.id, labelId: label.id,
@ -62,9 +67,9 @@ async function findAssignees(parsedTaskAssignees) {
export default { export default {
namespaced: true, namespaced: true,
state: () => ({}), state: (): TaskState => ({}),
actions: { actions: {
async loadTasks(ctx, params) { async loadTasks(ctx: ActionContext<TaskState, RootStoreState>, params) {
const taskService = new TaskService() const taskService = new TaskService()
const cancel = setLoading(ctx, 'tasks') const cancel = setLoading(ctx, 'tasks')
@ -77,7 +82,7 @@ export default {
} }
}, },
async update(ctx, task) { async update(ctx: ActionContext<TaskState, RootStoreState>, task: ITask) {
const cancel = setLoading(ctx, 'tasks') const cancel = setLoading(ctx, 'tasks')
const taskService = new TaskService() const taskService = new TaskService()
@ -90,7 +95,7 @@ export default {
} }
}, },
async delete(ctx, task) { async delete(ctx: ActionContext<TaskState, RootStoreState>, task: ITask) {
const taskService = new TaskService() const taskService = new TaskService()
const response = await taskService.delete(task) const response = await taskService.delete(task)
ctx.commit('kanban/removeTaskInBucket', task, {root: true}) ctx.commit('kanban/removeTaskInBucket', task, {root: true})
@ -99,7 +104,13 @@ export default {
// Adds a task attachment in store. // Adds a task attachment in store.
// This is an action to be able to commit other mutations // This is an action to be able to commit other mutations
addTaskAttachment(ctx, {taskId, attachment}) { addTaskAttachment(ctx: ActionContext<TaskState, RootStoreState>, {
taskId,
attachment,
}: {
taskId: ITask['id']
attachment: IAttachment
}) {
const t = ctx.rootGetters['kanban/getTaskById'](taskId) const t = ctx.rootGetters['kanban/getTaskById'](taskId)
if (t.task !== null) { if (t.task !== null) {
const attachments = [ const attachments = [
@ -119,7 +130,13 @@ export default {
ctx.commit('attachments/add', attachment, {root: true}) ctx.commit('attachments/add', attachment, {root: true})
}, },
async addAssignee(ctx, {user, taskId}) { async addAssignee(ctx: ActionContext<TaskState, RootStoreState>, {
user,
taskId,
}: {
user: IUser,
taskId: ITask['id']
}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId}) const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
@ -148,7 +165,13 @@ export default {
return r return r
}, },
async removeAssignee(ctx, {user, taskId}) { async removeAssignee(ctx: ActionContext<TaskState, RootStoreState>, {
user,
taskId,
}: {
user: IUser,
taskId: ITask['id']
}) {
const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId}) const taskAssignee = new TaskAssigneeModel({userId: user.id, taskId: taskId})
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
@ -175,8 +198,14 @@ export default {
}, },
async addLabel(ctx, {label, taskId}) { async addLabel(ctx: ActionContext<TaskState, RootStoreState>, {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id}) label,
taskId,
} : {
label: ILabel,
taskId: ITask['id']
}) {
const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
const r = await labelTaskService.create(labelTask) const r = await labelTaskService.create(labelTask)
@ -205,8 +234,8 @@ export default {
return r return r
}, },
async removeLabel(ctx, {label, taskId}) { async removeLabel(ctx: ActionContext<TaskState, RootStoreState>, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id}) const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
const response = await labelTaskService.delete(labelTask) const response = await labelTaskService.delete(labelTask)
@ -234,7 +263,10 @@ export default {
}, },
// Do everything that is involved in finding, creating and adding the label to the task // Do everything that is involved in finding, creating and adding the label to the task
async addLabelsToTask({rootState, dispatch}, { task, parsedLabels }) { async addLabelsToTask({rootState, dispatch}: ActionContext<TaskState, RootStoreState>, {
task,
parsedLabels,
}) {
if (parsedLabels.length <= 0) { if (parsedLabels.length <= 0) {
return task return task
} }
@ -257,7 +289,10 @@ export default {
return task return task
}, },
findListId({ rootGetters }, { list: listName, listId }) { findListId({ rootGetters }: ActionContext<TaskState, RootStoreState>, { list: listName, listId }: {
list: string,
listId: IList['id']
}) {
let foundListId = null let foundListId = null
// Uses the following ways to get the list id of the new task: // Uses the following ways to get the list id of the new task:
@ -285,12 +320,14 @@ export default {
return foundListId return foundListId
}, },
async createNewTask({dispatch, commit}, { async createNewTask({dispatch, commit}: ActionContext<TaskState, RootStoreState>, {
title, title,
bucketId, bucketId,
listId, listId,
position, position,
}) { } :
Partial<ITask>,
) {
const cancel = setLoading({commit}, 'tasks') const cancel = setLoading({commit}, 'tasks')
const parsedTask = parseTaskText(title, getQuickAddMagicMode()) const parsedTask = parseTaskText(title, getQuickAddMagicMode())

116
src/store/types.ts Normal file
View File

@ -0,0 +1,116 @@
import type { IBucket } from '@/models/bucket'
import type { IUserSettings } from '@/models/userSettings'
import type { IList } from '@/models/list'
import type { IAttachment } from '@/models/attachment'
import type { ILabel } from '@/models/label'
import type { INamespace } from '@/models/namespace'
export interface RootStoreState {
loading: boolean,
loadingModule: null,
currentList: IList,
background: string,
blurHash: string,
hasTasks: boolean,
menuActive: boolean,
keyboardShortcutsActive: boolean,
quickActionsActive: boolean,
// modules
attachments: AttachmentState,
auth: AuthState,
config: ConfigState,
kanban: KanbanState,
labels: LabelState,
lists: ListState,
namespaces: NamespaceState,
tasks: TaskState,
}
export interface AttachmentState {
attachments: IAttachment[],

I think that's the user id of the currently authenticated user.

I *think* that's the user id of the currently authenticated user.

Meaning IUser['id']?

Meaning `IUser['id']`?

Yes.

Yes.
}
export const AUTH_TYPES = {
'UNKNOWN': 0,
'USER': 1,
'LINK_SHARE': 2,
} as const
export interface Info {
id: number // what kind of id is this?
type: typeof AUTH_TYPES[keyof typeof AUTH_TYPES],
getAvatarUrl: () => string
settings: IUserSettings
name: string
email: string
exp: any
}
export interface AuthState {
authenticated: boolean,
isLinkShareAuth: boolean,
info: Info | null,
needsTotpPasscode: boolean,
avatarUrl: string,
lastUserInfoRefresh: Date | null,
settings: IUserSettings,
}
export interface ConfigState {
version: string,
frontendUrl: string,
motd: string,
linkSharingEnabled: boolean,
maxFileSize: '20MB',
registrationEnabled: boolean,
availableMigrators: [],
taskAttachmentsEnabled: boolean,
totpEnabled: boolean,
enabledBackgroundProviders: [],
legal: {
imprintUrl: string,
privacyPolicyUrl: string,
},
caldavEnabled: boolean,
userDeletionEnabled: boolean,
taskCommentsEnabled: boolean,
auth: {
local: {
enabled: boolean,
},
openidConnect: {
enabled: boolean,
redirectUrl: string,
providers: [],
},
},
}
export interface KanbanState {
buckets: IBucket[],
listId: IList['id'],
bucketLoading: {},
taskPagesPerBucket: {
[id: IBucket['id']]: number
},
allTasksLoadedForBucket: {
[id: IBucket['id']]: boolean
},
}
export interface LabelState {
labels: {
[id: ILabel['id']]: ILabel
},
loaded: boolean,
}
export interface ListState {
[id: IList['id']]: IList
}
export interface NamespaceState {
namespaces: INamespace[]
}
export interface TaskState {}

View File

@ -68,11 +68,11 @@ import SavedFilterService from '@/services/savedFilter'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import type ListModel from '@/models/list' import type { IList } from '@/models/list'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
function useSavedFilter(listId: MaybeRef<ListModel['id']>) { function useSavedFilter(listId: MaybeRef<IList['id']>) {
const filterService = shallowRef(new SavedFilterService()) const filterService = shallowRef(new SavedFilterService())
const filter = ref(new SavedFilterModel()) const filter = ref(new SavedFilterModel())

View File

@ -113,7 +113,7 @@
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import LabelModel from '../../models/label' import LabelModel, { type ILabel } from '../../models/label'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types' import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -150,7 +150,7 @@ export default defineComponent({
loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels', loading: state => state[LOADING] && state[LOADING_MODULE] === 'labels',
}), }),
methods: { methods: {
deleteLabel(label: LabelModel) { deleteLabel(label: ILabel) {
this.showDeleteModal = false this.showDeleteModal = false
this.isLabelEdit = false this.isLabelEdit = false
return this.$store.dispatch('labels/deleteLabel', label) return this.$store.dispatch('labels/deleteLabel', label)
@ -158,7 +158,7 @@ export default defineComponent({
editLabelSubmit() { editLabelSubmit() {
return this.$store.dispatch('labels/updateLabel', this.labelEditLabel) return this.$store.dispatch('labels/updateLabel', this.labelEditLabel)
}, },
editLabel(label: LabelModel) { editLabel(label: ILabel) {
if (label.createdBy.id !== this.userInfo.id) { if (label.createdBy.id !== this.userInfo.id) {
return return
} }
@ -180,7 +180,7 @@ export default defineComponent({
this.editorActive = false this.editorActive = false
this.$nextTick(() => this.editorActive = true) this.$nextTick(() => this.editorActive = true)
}, },
showDeleteDialoge(label: LabelModel) { showDeleteDialoge(label: ILabel) {
this.labelToDelete = label this.labelToDelete = label
this.showDeleteModal = true this.showDeleteModal = true
}, },

View File

@ -153,9 +153,9 @@ import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {calculateItemPosition} from '../../helpers/calculateItemPosition' import {calculateItemPosition} from '../../helpers/calculateItemPosition'
import type TaskModel from '@/models/task' import type { ITask } from '@/models/task'
function sortTasks(tasks: TaskModel[]) { function sortTasks(tasks: ITask[]) {
if (tasks === null || tasks === []) { if (tasks === null || tasks === []) {
return return
} }
@ -274,7 +274,7 @@ export default defineComponent({
focusNewTaskInput() { focusNewTaskInput() {
this.$refs.addTask.focusTaskInput() this.$refs.addTask.focusTaskInput()
}, },
updateTaskList(task: TaskModel) { updateTaskList(task: ITask) {
if ( this.isAlphabeticalSorting ) { if ( this.isAlphabeticalSorting ) {
// reload tasks with current filter and sorting // reload tasks with current filter and sorting
this.loadTasks(1, undefined, undefined, true) this.loadTasks(1, undefined, undefined, true)
@ -288,11 +288,11 @@ export default defineComponent({
this.$store.commit(HAS_TASKS, true) this.$store.commit(HAS_TASKS, true)
}, },
editTask(id: TaskModel['id']) { editTask(id: ITask['id']) {
this.taskEditTask = {...this.tasks.find(t => t.id === parseInt(id))} this.taskEditTask = {...this.tasks.find(t => t.id === parseInt(id))}
this.isTaskEdit = true this.isTaskEdit = true
}, },
updateTasks(updatedTask: TaskModel) { updateTasks(updatedTask: ITask) {
for (const t in this.tasks) { for (const t in this.tasks) {
if (this.tasks[t].id === updatedTask.id) { if (this.tasks[t].id === updatedTask.id) {
this.tasks[t] = updatedTask this.tasks[t] = updatedTask

View File

@ -197,7 +197,7 @@ import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup.vue' import Popup from '@/components/misc/popup.vue'
import {useTaskList} from '@/composables/taskList' import {useTaskList} from '@/composables/taskList'
import type TaskModel from '@/models/task' import type { ITask } from '@/models/task'
const ACTIVE_COLUMNS_DEFAULT = { const ACTIVE_COLUMNS_DEFAULT = {
id: true, id: true,
@ -253,7 +253,7 @@ const {
currentPage, currentPage,
sortByParam, sortByParam,
} = taskList } = taskList
const tasks: Ref<TaskModel[]> = taskList.tasks const tasks: Ref<ITask[]> = taskList.tasks
Object.assign(params.value, { Object.assign(params.value, {
filter_by: [], filter_by: [],

View File

@ -30,7 +30,7 @@ import CreateEdit from '@/components/misc/create-edit.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import ListDuplicateModel from '@/models/listDuplicateModel' import ListDuplicateModel from '@/models/listDuplicateModel'
import NamespaceModel from '@/models/namespace' import type {INamespace} from '@/models/namespace'
import {success} from '@/message' import {success} from '@/message'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
@ -44,9 +44,9 @@ const {
findNamespaces, findNamespaces,
} = useNameSpaceSearch() } = useNameSpaceSearch()
const selectedNamespace = ref<NamespaceModel>() const selectedNamespace = ref<INamespace>()
function selectNamespace(namespace: NamespaceModel) { function selectNamespace(namespace: INamespace) {
selectedNamespace.value = namespace selectedNamespace.value = namespace
} }

View File

@ -80,14 +80,14 @@ import ColorPicker from '@/components/input/colorPicker.vue'
import CreateEdit from '@/components/misc/create-edit.vue' import CreateEdit from '@/components/misc/create-edit.vue'
import {CURRENT_LIST} from '@/store/mutation-types' import {CURRENT_LIST} from '@/store/mutation-types'
import type ListModel from '@/models/list' import type { IList } from '@/models/list'
import { useList } from '@/composables/useList' import { useList } from '@/composables/useList'
import { useTitle } from '@/composables/useTitle' import { useTitle } from '@/composables/useTitle'
const props = defineProps({ const props = defineProps({
listId: { listId: {
type: Number as PropType<ListModel['id']>, type: Number as PropType<IList['id']>,
required: true, required: true,
}, },
}) })

View File

@ -49,7 +49,6 @@ import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import type TaskModel from '@/models/task'
import {formatDate} from '@/helpers/time/formatDate' import {formatDate} from '@/helpers/time/formatDate'
import {setTitle} from '@/helpers/setTitle' import {setTitle} from '@/helpers/setTitle'
@ -59,13 +58,14 @@ import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
import {DATE_RANGES} from '@/components/date/dateRanges' import {DATE_RANGES} from '@/components/date/dateRanges'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types' import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import LlamaCool from '@/assets/llama-cool.svg?component' import LlamaCool from '@/assets/llama-cool.svg?component'
import type { ITask } from '@/models/task'
const store = useStore() const store = useStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const tasks = ref<TaskModel[]>([]) const tasks = ref<ITask[]>([])
const showNothingToDo = ref<boolean>(false) const showNothingToDo = ref<boolean>(false)
setTimeout(() => showNothingToDo.value = true, 100) setTimeout(() => showNothingToDo.value = true, 100)
@ -182,7 +182,7 @@ async function loadPendingTasks(from: string, to: string) {
} }
// FIXME: this modification should happen in the store // FIXME: this modification should happen in the store
function updateTasks(updatedTask: TaskModel) { function updateTasks(updatedTask: ITask) {
for (const t in tasks.value) { for (const t in tasks.value) {
if (tasks.value[t].id === updatedTask.id) { if (tasks.value[t].id === updatedTask.id) {
tasks.value[t] = updatedTask tasks.value[t] = updatedTask

View File

@ -425,7 +425,7 @@
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import TaskService from '../../services/task' import TaskService from '../../services/task'
import TaskModel from '../../models/task' import TaskModel, { type ITask } from '@/models/task'
import { PRIORITIES as priorites } from '@/models/constants/priorities' import { PRIORITIES as priorites } from '@/models/constants/priorities'
import {RIGHTS as rights} from '@/models/constants/rights' import {RIGHTS as rights} from '@/models/constants/rights'
@ -452,10 +452,10 @@ import {CURRENT_LIST} from '@/store/mutation-types'
import {uploadFile} from '@/helpers/attachments' import {uploadFile} from '@/helpers/attachments'
import ChecklistSummary from '../../components/tasks/partials/checklist-summary.vue' import ChecklistSummary from '../../components/tasks/partials/checklist-summary.vue'
import CreatedUpdated from '@/components/tasks/partials/createdUpdated.vue' import CreatedUpdated from '@/components/tasks/partials/createdUpdated.vue'
import type ListModel from '@/models/list'
import { setTitle } from '@/helpers/setTitle' import { setTitle } from '@/helpers/setTitle'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
import type { IList } from '@/models/list'
function scrollIntoView(el) { function scrollIntoView(el) {
if (!el) { if (!el) {
@ -597,7 +597,7 @@ export default defineComponent({
return uploadFile(this.taskId, ...args) return uploadFile(this.taskId, ...args)
}, },
async loadTask(taskId: TaskModel['id']) { async loadTask(taskId: ITask['id']) {
if (taskId === undefined) { if (taskId === undefined) {
return return
} }
@ -703,7 +703,7 @@ export default defineComponent({
this.saveTask(true, this.toggleTaskDone) this.saveTask(true, this.toggleTaskDone)
}, },
async changeList(list: ListModel) { async changeList(list: IList) {
this.$store.commit('kanban/removeTaskInBucket', this.task) this.$store.commit('kanban/removeTaskInBucket', this.task)
this.task.listId = list.id this.task.listId = list.id
await this.saveTask() await this.saveTask()

View File

@ -162,23 +162,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed, ref} from 'vue' import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import TeamService from '../../services/team' import TeamService from '@/services/team'
import type TeamModel from '../../models/team' import TeamMemberService from '@/services/teamMember'
import TeamMemberService from '../../services/teamMember' import UserService from '@/services/user'
import type TeamMemberModel from '../../models/teamMember'
import type UserModel from '../../models/user'
import UserService from '../../services/user'
import {RIGHTS as Rights} from '@/models/constants/rights' import {RIGHTS as Rights} from '@/models/constants/rights'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useI18n} from 'vue-i18n'
import {success} from '@/message' import {success} from '@/message'
import type { ITeam } from '@/models/team'
import type { IUser } from '@/models/user'
import type { ITeamMember } from '@/models/teamMember'
const store = useStore() const store = useStore()
const route = useRoute() const route = useRoute()
@ -198,11 +198,11 @@ const teamService = ref<TeamService>(new TeamService())
const teamMemberService = ref<TeamMemberService>(new TeamMemberService()) const teamMemberService = ref<TeamMemberService>(new TeamMemberService())
const userService = ref<UserService>(new UserService()) const userService = ref<UserService>(new UserService())
const team = ref<TeamModel>() const team = ref<ITeam>()
const teamId = computed(() => route.params.id) const teamId = computed(() => route.params.id)
const memberToDelete = ref<TeamMemberModel>() const memberToDelete = ref<ITeamMember>()
const newMember = ref<UserModel>() const newMember = ref<IUser>()
const foundUsers = ref<UserModel[]>() const foundUsers = ref<IUser[]>()
const showDeleteModal = ref(false) const showDeleteModal = ref(false)
const showUserDeleteModal = ref(false) const showUserDeleteModal = ref(false)
@ -257,7 +257,7 @@ async function addUser() {
success({message: t('team.edit.userAddedSuccess')}) success({message: t('team.edit.userAddedSuccess')})
} }
async function toggleUserType(member: TeamMemberModel) { async function toggleUserType(member: ITeamMember) {
// FIXME: direct manipulation // FIXME: direct manipulation
member.admin = !member.admin member.admin = !member.admin
member.teamId = teamId.value member.teamId = teamId.value
@ -282,7 +282,7 @@ async function findUser(query: string) {
} }
const users = await userService.value.getAll({}, {s: query}) const users = await userService.value.getAll({}, {s: query})
foundUsers.value = users.filter((u: UserModel) => u.id !== userInfo.value.id) foundUsers.value = users.filter((u: IUser) => u.id !== userInfo.value.id)
} }
</script> </script>

View File

@ -77,8 +77,8 @@ import {success} from '@/message'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import CaldavTokenService from '@/services/caldavToken' import CaldavTokenService from '@/services/caldavToken'
import type CaldavTokenModel from '@/models/caldavToken'
import { formatDateShort } from '@/helpers/time/formatDate' import { formatDateShort } from '@/helpers/time/formatDate'
import type { ICaldavToken } from '@/models/caldavToken'
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
@ -86,19 +86,19 @@ const {t} = useI18n({useScope: 'global'})
useTitle(() => `${t('user.settings.caldav.title')} - ${t('user.settings.title')}`) useTitle(() => `${t('user.settings.caldav.title')} - ${t('user.settings.title')}`)
const service = shallowReactive(new CaldavTokenService()) const service = shallowReactive(new CaldavTokenService())
const tokens = ref<CaldavTokenModel[]>([]) const tokens = ref<ICaldavToken[]>([])
service.getAll().then((result: CaldavTokenModel[]) => { service.getAll().then((result: ICaldavToken[]) => {
tokens.value = result tokens.value = result
}) })
const newToken = ref<CaldavTokenModel>() const newToken = ref<ICaldavToken>()
async function createToken() { async function createToken() {
newToken.value = await service.create({}) as CaldavTokenModel newToken.value = await service.create({}) as ICaldavToken
tokens.value.push(newToken.value) tokens.value.push(newToken.value)
} }
async function deleteToken(token: CaldavTokenModel) { async function deleteToken(token: ICaldavToken) {
const r = await service.delete(token) const r = await service.delete(token)
tokens.value = tokens.value.filter(({id}) => id !== token.id) tokens.value = tokens.value.filter(({id}) => id !== token.id)
success(r) success(r)