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 {getListTitle} from '@/helpers/getListTitle'
import {useEventListener} from '@vueuse/core'
import type NamespaceModel from '@/models/namespace'
import type ListModel from '@/models/list'
import type { IList } from '@/models/list'
import type { INamespace } from '@/models/namespace'
const drag = ref(false)
const dragOptions = {
@ -172,7 +172,7 @@ const loading = computed(() => store.state.loading && store.state.loadingModule
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(() => {
return namespaces.value.map(({lists}) => {
@ -195,7 +195,7 @@ useEventListener('resize', resize)
onMounted(() => resize())
function toggleFavoriteList(list: ListModel) {
function toggleFavoriteList(list: IList) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
@ -209,14 +209,14 @@ function resize() {
store.commit(MENU_ACTIVE, window.innerWidth >= 770)
}
function toggleLists(namespaceId: NamespaceModel['id']) {
function toggleLists(namespaceId: INamespace['id']) {
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
onBeforeMount(async () => {
const namespaces = await store.dispatch('namespaces/loadNamespaces') as NamespaceModel[]
const namespaces = await store.dispatch('namespaces/loadNamespaces') as INamespace[]
namespaces.forEach(n => {
if (typeof listsVisible.value[n.id] === 'undefined') {
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
// 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
@ -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) {
if (!e.newIndex && e.newIndex !== 0) return

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ import {computed, onMounted, onUnmounted, ref} from 'vue'
import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.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 {useStore} from 'vuex'
import {useRouter} from 'vue-router'
@ -63,7 +63,7 @@ const LOAD_NOTIFICATIONS_INTERVAL = 10000
const store = useStore()
const router = useRouter()
const allNotifications = ref<NotificationModel[]>([])
const allNotifications = ref<INotification[]>([])
const showNotifications = ref(false)
const popup = ref(null)

View File

@ -181,13 +181,13 @@ import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n'
import {RIGHTS} from '@/models/constants/rights'
import LinkShareModel from '@/models/linkShare'
import type ListModel from '@/models/list'
import LinkShareModel, { type ILinkShare } from '@/models/linkShare'
import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import type { IList } from '@/models/list'
const props = defineProps({
listId: {
@ -198,7 +198,7 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'})
const linkShares = ref<LinkShareModel[]>([])
const linkShares = ref<ILinkShare[]>([])
const linkShareService = shallowReactive(new LinkShareService())
const selectedRight = ref(RIGHTS.READ)
const name = ref('')
@ -217,7 +217,7 @@ watch(
const store = useStore()
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) {
return
@ -226,7 +226,7 @@ async function load(listId: ListModel['id']) {
linkShares.value = await linkShareService.getAll({listId})
}
async function add(listId: ListModel['id']) {
async function add(listId: IList['id']) {
const newLinkShare = new LinkShareModel({
right: selectedRight.value,
listId,
@ -242,7 +242,7 @@ async function add(listId: ListModel['id']) {
await load(listId)
}
async function remove(listId: ListModel['id']) {
async function remove(listId: IList['id']) {
try {
await linkShareService.delete(new LinkShareModel({
id: linkIdToDelete.value,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,17 +38,17 @@
</template>
<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 {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component'
import TaskService from '@/services/task'
import TaskModel from '@/models/task'
import { type ITask } from '@/models/task'
const props = defineProps({
modelValue: {
type: TaskModel,
type: Object as PropType<ITask>,
required: true,
},
})
@ -58,7 +58,7 @@ const {t} = useI18n({useScope: 'global'})
const store = useStore()
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.
const dueDate = ref<Date>()

View File

@ -30,17 +30,17 @@
</template>
<script setup lang="ts">
import {ref,computed, watch} from 'vue'
import {ref,computed, watch, type PropType} from 'vue'
import {useStore} from 'vuex'
import Editor from '@/components/input/AsyncEditor'
import TaskModel from '@/models/task'
import type { ITask } from '@/models/task'
const props = defineProps({
modelValue: {
type: TaskModel,
type: Object as PropType<ITask>,
required: true,
},
attachmentUpload: {
@ -54,7 +54,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const task = ref<TaskModel>({description: ''})
const task = ref<ITask>({description: ''})
const saved = ref(false)
// 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 {includesById} from '@/helpers/utils'
import type UserModel from '@/models/user'
import ListUserService from '@/services/listUsers'
import {success} from '@/message'
import type { IUser } from '@/models/user'
const props = defineProps({
taskId: {
@ -54,7 +54,7 @@ const props = defineProps({
default: false,
},
modelValue: {
type: Array as PropType<UserModel[]>,
type: Array as PropType<IUser[]>,
default: () => [],
},
})
@ -65,7 +65,7 @@ const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService())
const foundUsers = ref([])
const assignees = ref<UserModel[]>([])
const assignees = ref<IUser[]>([])
watch(
() => 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})
emit('update:modelValue', assignees.value)
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})
// 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 {useI18n} from 'vue-i18n'
import LabelModel from '@/models/label'
import LabelModel, { type ILabel } from '@/models/label'
import LabelTaskService from '@/services/labelTask'
import {success} from '@/message'
@ -52,7 +52,7 @@ import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({
modelValue: {
type: Array as PropType<LabelModel[]>,
type: Array as PropType<ILabel[]>,
default: () => [],
},
taskId: {
@ -71,7 +71,7 @@ const store = useStore()
const {t} = useI18n({useScope: 'global'})
const labelTaskService = shallowReactive(new LabelTaskService())
const labels = ref<LabelModel[]>([])
const labels = ref<ILabel[]>([])
const query = ref('')
watch(
@ -92,7 +92,7 @@ function findLabel(newQuery: string) {
query.value = newQuery
}
async function addLabel(label: LabelModel, showNotification = true) {
async function addLabel(label: ILabel, showNotification = true) {
const bubble = () => {
emit('update:modelValue', 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) {
await store.dispatch('tasks/removeLabel', {label, taskId: props.taskId})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,27 @@
import AttachmentModel from '@/models/attachment'
import FileModel from '@/models/file'
import AttachmentModel, { type IAttachment } from '@/models/attachment'
import type {IFile} from '@/models/file'
import AttachmentService from '@/services/attachment'
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 files = [file]
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 response = await attachmentService.create(attachmentModel, files)
console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
response.success?.map((attachment: AttachmentModel) => {
response.success?.map((attachment: IAttachment) => {
store.dispatch('tasks/addTaskAttachment', {
taskId,
attachment,

View File

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

View File

@ -1,7 +1,7 @@
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) {
return i18n.global.t('namespace.pseudo.sharedLists.title')
}

View File

@ -1,18 +1,10 @@
import {createNewIndexer} from '../indexes'
import type {LabelState} from '@/store/types'
import type {ILabel} from '@/models/label'
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
* @param {Object} state
@ -20,7 +12,7 @@ interface labelState {
* @param {String} query
* @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)
return search(query)
@ -36,6 +28,6 @@ export function filterLabelsByQuery(state: labelState, labelsToHide: label[], qu
* @param {Array} ids
* @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))
}

View File

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

View File

@ -2,7 +2,11 @@ import {objectToCamelCase} from '@/helpers/case'
import {omitBy, isNil} from '@/helpers/utils'
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.

View File

@ -1,12 +1,21 @@
import AbstractModel from './abstractModel'
import UserModel from './user'
import FileModel from './file'
import UserModel, {type IUser} from './user'
import FileModel, {type IFile} from './file'
import type {IAbstract} from './abstractModel'
export default class AttachmentModel extends AbstractModel {
export interface IAttachment extends IAbstract {
id: number
taskId: number
createdBy: UserModel
file: FileModel
createdBy: IUser
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
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 {
avatarProvider: AVATAR_PROVIDERS
export interface IAvatar extends IAbstract {
avatarProvider: AvatarProvider
}
export default class AvatarModel extends AbstractModel implements IAvatar {
declare avatarProvider: AvatarProvider
defaults() {
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
url: string
thumb: string
@ -9,6 +9,17 @@ export default class BackgroundImageModel extends AbstractModel {
authorName: 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() {
return {

View File

@ -1,17 +1,31 @@
import AbstractModel from './abstractModel'
import UserModel from './user'
import TaskModel from './task'
import AbstractModel, { type IAbstract } from './abstractModel'
import UserModel, { type IUser } from './user'
import TaskModel, { type ITask } from './task'
export default class BucketModel extends AbstractModel {
export interface IBucket extends IAbstract {
id: number
title: string
listId: number
limit: number
tasks: TaskModel[]
tasks: ITask[]
isDoneBucket: boolean
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
updated: Date

View File

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

View File

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

View File

@ -1,11 +1,19 @@
import AbstractModel from './abstractModel'
export default class FileModel extends AbstractModel {
export interface IFile {
id: number
mime: string
name: string
size: number
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) {
super(data)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,15 @@
import AbstractModel from './abstractModel'
export default class PasswordResetModel extends AbstractModel {
export interface IPasswordReset {
token: string
newPassword: string
email: string
}
export default class PasswordResetModel extends AbstractModel implements IPasswordReset {
token: string
declare newPassword: string
declare email: string
constructor(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
oldPassword: string
}
export default class PasswordUpdateModel extends AbstractModel implements IPasswordUpdate {
declare newPassword: string
declare oldPassword: string
defaults() {
return {

View File

@ -1,7 +1,7 @@
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
title: string
description: string
@ -15,7 +15,26 @@ export default class SavedFilterModel extends AbstractModel {
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
updated: Date

View File

@ -1,11 +1,20 @@
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
entity: string // 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

View File

@ -1,12 +1,14 @@
import AbstractModel from './abstractModel'
import UserModel from './user'
import LabelModel from './label'
import AttachmentModel from './attachment'
import type { Priority } from '@/models/constants/priorities'
import AbstractModel from '@/models/abstractModel'
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 type ListModel from './list'
import type { Priority } from './constants/priorities'
import type { IBucket } from './bucket'
const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype
export const TASK_DEFAULT_COLOR = '#1973ff'
@ -24,15 +26,15 @@ export interface RepeatAfter {
amount: number
}
export default class TaskModel extends AbstractModel {
export interface ITask {
id: number
title: string
description: string
done: boolean
doneAt: Date | null
priority: Priority
labels: LabelModel[]
assignees: UserModel[]
labels: ILabel[]
assignees: IUser[]
dueDate: Date | null
startDate: Date | null
@ -41,26 +43,64 @@ export default class TaskModel extends AbstractModel {
repeatFromCurrentDate: boolean
repeatMode: TaskRepeatMode
reminderDates: Date[]
parentTaskId: TaskModel['id']
parentTaskId: ITask['id']
hexColor: string
percentDone: number
relatedTasks: { [relationKind: string]: TaskModel } // FIXME: use relationKinds
attachments: AttachmentModel[]
relatedTasks: { [relationKind: string]: ITask } // FIXME: use relationKinds
attachments: IAttachment[]
identifier: string
index: number
isFavorite: boolean
subscription: SubscriptionModel
subscription: ISubscription
position: 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
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)
this.id = Number(this.id)
@ -120,6 +160,7 @@ export default class TaskModel extends AbstractModel {
this.listId = Number(this.listId)
}
bucketId: number
defaults() {
return {

View File

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

View File

@ -1,12 +1,22 @@
import AbstractModel from './abstractModel'
import UserModel from './user'
import type TaskModel from './task'
import UserModel, { type IUser } from './user'
import type { ITask } from './task'
export default class TaskCommentModel extends AbstractModel {
export interface ITaskComment {
id: number
taskId: TaskModel['id']
taskId: ITask['id']
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
updated: Date

View File

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

View File

@ -1,16 +1,28 @@
import AbstractModel from './abstractModel'
import UserModel from './user'
import TeamMemberModel from './teamMember'
import UserModel, { type IUser } from './user'
import TeamMemberModel, { type ITeamMember } from './teamMember'
import {RIGHTS, type Right} from '@/models/constants/rights'
export default class TeamModel extends AbstractModel {
id: 0
export interface ITeam {
id: number
name: string
description: string
members: TeamMemberModel[]
members: ITeamMember[]
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
updated: Date

View File

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

View File

@ -1,9 +1,14 @@
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
teamId: ListModel['id']
teamId: IList['id']
}
export default class TeamMemberModel extends UserModel implements ITeamMember {
declare admin: boolean
declare teamId: IList['id']
defaults() {
return {

View File

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

View File

@ -1,14 +1,22 @@
import AbstractModel from './abstractModel'
import type TeamModel from './team'
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.
* It is extended in a way so it can be used for namespaces as well for lists.
*/
export default class TeamShareBaseModel extends AbstractModel {
teamId: TeamModel['id']
right: Right
export default class TeamShareBaseModel extends AbstractModel implements ITeamShareBase {
declare teamId: ITeam['id']
declare right: Right
created: Date
updated: Date

View File

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

View File

@ -1,7 +1,7 @@
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
email: string
username: string
@ -9,7 +9,18 @@ export default class UserModel extends AbstractModel {
created: 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) {
super(data)
@ -28,6 +39,7 @@ export default class UserModel extends AbstractModel {
email: '',
username: '',
name: '',
created: null,
updated: null,
settings: null,

View File

@ -1,8 +1,13 @@
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
export default class UserListModel extends UserShareBaseModel {
listId: ListModel['id']
export default class UserListModel extends UserShareBaseModel implements IUserList {
declare listId: IList['id']
defaults() {
return {

View File

@ -1,9 +1,13 @@
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
export default class UserNamespaceModel extends UserShareBaseModel {
namespaceId: NamespaceModel['id']
export default class UserNamespaceModel extends UserShareBaseModel implements IUserNamespace {
declare namespaceId: INamespace['id']
defaults() {
return {

View File

@ -1,16 +1,27 @@
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
emailRemindersEnabled: boolean
discoverableByName: boolean
discoverableByEmail: boolean
overdueTasksRemindersEnabled: boolean
defaultListId: undefined | ListModel['id']
defaultListId: undefined | IList['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
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() {
return {

View File

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

View File

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

View File

@ -2,9 +2,9 @@ import {AuthenticatedHTTPFactory} from '@/http-common'
import type {Method} from 'axios'
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 FileModel from '@/models/file'
import type { IFile } from '@/models/file'
interface Paths {
create : string
@ -12,6 +12,7 @@ interface Paths {
getAll : string
update : string
delete : string
reset?: string
}
function convertObject(o: Record<string, unknown>) {
@ -39,7 +40,7 @@ function prepareParams(params: Record<string, unknown | unknown[]>) {
return objectToSnakeCase(params)
}
export default class AbstractService<Model extends AbstractModel = AbstractModel> {
export default class AbstractService<Model extends IAbstract = IAbstract> {
/////////////////////////////
// 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.
* 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()
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({
url,
method,
@ -302,7 +303,7 @@ export default class AbstractService<Model extends AbstractModel = AbstractModel
* @param params Optional query parameters
* @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 === '') {
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.
* @param url
* @param file {FileModel}
* @param file {IFile}
* @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)
}

View File

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

View File

@ -1,7 +1,7 @@
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() {
super({
get: '/user/settings/avatar',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import AbstractService from './abstractService'
import TaskModel from '../models/task'
import TaskModel, { type ITask } from '../models/task'
import AttachmentService from './attachment'
import LabelService from './label'
@ -113,7 +113,7 @@ export default class TaskService extends AbstractService {
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 TaskCommentModel from '../models/taskComment'
import TaskCommentModel, { type ITaskComment } from '../models/taskComment'
import {formatISO} from 'date-fns'
export default class TaskCommentService extends AbstractService {
export default class TaskCommentService extends AbstractService<ITaskComment> {
constructor() {
super({
create: '/tasks/{taskId}/comments',

View File

@ -1,4 +1,6 @@
import type { ActionContext } from 'vuex'
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.
@ -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|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(() => {
if (loadFunc === null) {
context.commit(LOADING, true, {root: true})

View File

@ -25,7 +25,9 @@ import ListModel from '@/models/list'
import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
export const store = createStore({
import type { RootStoreState } from './types'
export const store = createStore<RootStoreState>({
strict: import.meta.env.DEV,
modules: {
config,
@ -37,7 +39,7 @@ export const store = createStore({
attachments,
labels,
},
state: {
state: () => ({
loading: false,
loadingModule: null,
// 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,
keyboardShortcutsActive: false,
quickActionsActive: false,
},
}),
mutations: {
[LOADING](state, loading) {
state.loading = loading

View File

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

View File

@ -1,3 +1,5 @@
import type { ActionContext } from 'vuex'
import {HTTPFactory, AuthenticatedHTTPFactory} from '@/http-common'
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
import {objectToSnakeCase} from '@/helpers/case'
@ -8,12 +10,10 @@ import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper'
import {success} from '@/message'
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 => {
if (typeof settings.weekStart === 'undefined' || settings.weekStart === '') {
@ -24,7 +24,7 @@ const defaultSettings = settings => {
export default {
namespaced: true,
state: () => ({
state: (): AuthState => ({
authenticated: false,
isLinkShareAuth: false,
info: null,
@ -34,13 +34,13 @@ export default {
settings: {},
}),
getters: {
authUser(state) {
authUser(state: AuthState) {
return state.authenticated && (
state.info &&
state.info.type === AUTH_TYPES.USER
)
},
authLinkShare(state) {
authLinkShare(state: AuthState) {
return state.authenticated && (
state.info &&
state.info.type === AUTH_TYPES.LINK_SHARE
@ -48,7 +48,7 @@ export default {
},
},
mutations: {
info(state, info) {
info(state: AuthState, info: Info) {
state.info = info
if (info !== null) {
state.avatarUrl = info.getAvatarUrl()
@ -60,31 +60,32 @@ export default {
state.isLinkShareAuth = info.id < 0
}
},
setUserSettings(state, settings) {
setUserSettings(state: AuthState, settings: IUserSettings) {
state.settings = defaultSettings(settings)
const info = state.info !== null ? state.info : {}
const info = state.info !== null ? state.info : {} as Info
info.name = settings.name
state.info = info
},
authenticated(state, authenticated) {
authenticated(state: AuthState, authenticated: boolean) {
state.authenticated = authenticated
},
isLinkShareAuth(state, is) {
state.isLinkShareAuth = is
isLinkShareAuth(state: AuthState, isLinkShareAuth: boolean) {
state.isLinkShareAuth = isLinkShareAuth
},
needsTotpPasscode(state, needs) {
state.needsTotpPasscode = needs
needsTotpPasscode(state: AuthState, needsTotpPasscode: boolean) {
state.needsTotpPasscode = needsTotpPasscode
},
reloadAvatar(state) {
reloadAvatar(state: AuthState) {
if (!state.info) return
state.avatarUrl = `${state.info.getAvatarUrl()}&=${+new Date()}`
},
lastUserRefresh(state) {
lastUserRefresh(state: AuthState) {
state.lastUserInfoRefresh = new Date()
},
},
actions: {
// Logs a user in with a set of credentials.
async login(ctx, credentials) {
async login(ctx: ActionContext<AuthState, RootStoreState>, credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
@ -115,7 +116,7 @@ export default {
// 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.
async register(ctx, credentials) {
async register(ctx: ActionContext<AuthState, RootStoreState>, credentials) {
const HTTP = HTTPFactory()
ctx.commit(LOADING, true, {root: true})
try {
@ -132,7 +133,7 @@ export default {
}
},
async openIdAuth(ctx, {provider, code}) {
async openIdAuth(ctx: ActionContext<AuthState, RootStoreState>, {provider, code}) {
const HTTP = HTTPFactory()
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 response = await HTTP.post('/shares/' + hash + '/auth', {
password: password,
@ -165,7 +166,7 @@ export default {
},
// 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.
// 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
if (
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()
if (!jwt) {
return
@ -243,7 +244,7 @@ export default {
}
},
async saveUserSettings(ctx, payload) {
async saveUserSettings(ctx: ActionContext<AuthState, RootStoreState>, payload) {
const {settings} = payload
const showMessage = payload.showMessage ?? true
const userSettingsService = new UserSettingsService()
@ -264,7 +265,7 @@ export default {
},
// 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
// 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.
@ -285,7 +286,7 @@ export default {
}
}, 5000)
},
logout(ctx) {
logout(ctx: ActionContext<AuthState, RootStoreState>) {
removeToken()
window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
ctx.dispatch('checkAuth')

View File

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

View File

@ -7,10 +7,15 @@ import {success} from '@/message'
import BucketService from '../../services/bucket'
import {setLoading} from '../helper'
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
function getTaskIndicesById(state, taskId) {
function getTaskIndicesById(state: KanbanState, taskId: ITask['id']) {
let taskIndex
const bucketIndex = state.buckets.findIndex(({ tasks }) => {
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)
state.buckets[bucketIndex].tasks.push(task)
state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1)
@ -36,7 +41,7 @@ const addTaskToBucketAndSort = (state, task) => {
export default {
namespaced: true,
state: () => ({
state: (): KanbanState => ({
buckets: [],
listId: 0,
bucketLoading: {},
@ -45,11 +50,11 @@ export default {
}),
mutations: {
setListId(state, listId) {
setListId(state: KanbanState, listId: IList['id']) {
state.listId = parseInt(listId)
},
setBuckets(state, buckets) {
setBuckets(state: KanbanState, buckets: IBucket[]) {
state.buckets = buckets
buckets.forEach(b => {
state.taskPagesPerBucket[b.id] = 1
@ -57,31 +62,51 @@ export default {
})
},
addBucket(state, bucket) {
addBucket(state: KanbanState, bucket: IBucket) {
state.buckets.push(bucket)
},
removeBucket(state, bucket) {
removeBucket(state: KanbanState, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets.splice(bucketIndex, 1)
},
setBucketById(state, bucket) {
setBucketById(state: KanbanState, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets[bucketIndex] = bucket
},
setBucketByIndex(state, {bucketIndex, bucket}) {
setBucketByIndex(state: KanbanState, {
bucketIndex,
bucket,
} : {
bucketIndex: number,
bucket: IBucket
}) {
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]
bucket.tasks[taskIndex] = task
state.buckets[bucketIndex] = bucket
},
setTasksInBucketByBucketId(state, {bucketId, tasks}) {
setTasksInBucketByBucketId(state: KanbanState, {
bucketId,
tasks,
} : {
bucketId: IBucket['id'],
tasks: ITask[],
}) {
const bucketIndex = findIndexById(state.buckets, bucketId)
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 (state.buckets.length === 0) {
return
@ -133,7 +158,7 @@ export default {
}
},
addTaskToBucket(state, task) {
addTaskToBucket(state: KanbanState, task: ITask) {
const bucketIndex = findIndexById(state.buckets, task.bucketId)
const oldBucket = state.buckets[bucketIndex]
const newBucket = {
@ -146,7 +171,10 @@ export default {
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 oldBucket = state.buckets[bucketIndex]
const newBucket = {
@ -159,7 +187,7 @@ export default {
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 (state.buckets.length === 0) {
return
@ -168,8 +196,10 @@ export default {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, task.id)
if (
!bucketIndex ||
state.buckets[bucketIndex]?.id !== task.bucketId ||
state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id
!taskIndex ||
(state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
) {
return
}
@ -177,39 +207,40 @@ export default {
state.buckets[bucketIndex].tasks.splice(taskIndex, 1)
},
setBucketLoading(state, {bucketId, loading}) {
setBucketLoading(state: KanbanState, {bucketId, loading}) {
state.bucketLoading[bucketId] = loading
},
setTasksLoadedForBucketPage(state, {bucketId, page}) {
setTasksLoadedForBucketPage(state: KanbanState, {bucketId, page}) {
state.taskPagesPerBucket[bucketId] = page
},
setAllTasksLoadedForBucket(state, bucketId) {
setAllTasksLoadedForBucket(state: KanbanState, bucketId) {
state.allTasksLoadedForBucket[bucketId] = true
},
},
getters: {
getBucketById(state) {
getBucketById(state: KanbanState) {
return (bucketId) => findById(state.buckets, bucketId)
},
getTaskById(state) {
getTaskById(state: KanbanState) {
return (id) => {
const { bucketIndex, taskIndex } = getTaskIndicesById(state, id)
return {
bucketIndex,
taskIndex,
task: state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
task: bucketIndex && taskIndex && state.buckets[bucketIndex]?.tasks?.[taskIndex] || null,
}
}
},
},
actions: {
async loadBucketsForList(ctx, {listId, params}) {
async loadBucketsForList(ctx: ActionContext<KanbanState, RootStoreState>, {listId, params}) {
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
@ -219,7 +250,7 @@ export default {
const bucketService = new BucketService()
try {
const response = await bucketService.getAll({listId: listId}, params)
const response = await bucketService.getAll({listId}, params)
ctx.commit('setBuckets', response)
ctx.commit('setListId', listId)
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
if (isLoading) {
return
@ -270,7 +301,7 @@ export default {
const taskService = new TaskCollectionService()
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('setTasksLoadedForBucketPage', {bucketId, 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 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 bucketService = new BucketService()
@ -304,14 +335,14 @@ export default {
const response = await bucketService.delete(bucket)
ctx.commit('removeBucket', 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
} finally {
cancel()
}
},
async updateBucket(ctx, updatedBucketData) {
async updateBucket(ctx: ActionContext<KanbanState, RootStoreState>, updatedBucketData) {
const cancel = setLoading(ctx, 'kanban')
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)
if (bucket.title === title) {
if (bucket?.title === title) {
// bucket title has not changed
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 {setLoading} from '@/store/helper'
import {success} from '@/message'
import {i18n} from '@/i18n'
import type { LabelState, RootStoreState } from '@/store/types'
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
import {createNewIndexer} from '@/indexes'
import type { ILabel } from '@/models/label'
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 labels = await labelService.getAll({}, {}, page)
const labels = await labelService.getAll({}, {}, page) as ILabel[]
if (page < labelService.totalPages) {
const nextLabels = await getAllLabels(page + 1)
return labels.concat(nextLabels)
@ -20,45 +24,44 @@ async function getAllLabels(page = 1) {
export default {
namespaced: true,
state: () => ({
// The labels are stored as an object which has the label ids as keys.
state: (): LabelState => ({
labels: {},
loaded: false,
}),
mutations: {
setLabels(state, labels) {
setLabels(state: LabelState, labels: ILabel[]) {
labels.forEach(l => {
state.labels[l.id] = l
add(l)
})
},
setLabel(state, label) {
setLabel(state: LabelState, label: ILabel) {
state.labels[label.id] = label
update(label)
},
removeLabelById(state, label) {
removeLabelById(state: LabelState, label: ILabel) {
remove(label)
delete state.labels[label.id]
},
setLoaded(state, loaded) {
setLoaded(state: LabelState, loaded: boolean) {
state.loaded = loaded
},
},
getters: {
getLabelsByIds(state) {
return (ids) => getLabelsByIds(state, ids)
getLabelsByIds(state: LabelState) {
return (ids: ILabel['id'][]) => getLabelsByIds(state, ids)
},
filterLabelsByQuery(state) {
return (labelsToHide, query) => filterLabelsByQuery(state, labelsToHide, query)
filterLabelsByQuery(state: LabelState) {
return (labelsToHide: ILabel[], query: string) => filterLabelsByQuery(state, labelsToHide, query)
},
getLabelsByExactTitles(state) {
getLabelsByExactTitles(state: LabelState) {
return labelTitles => Object
.values(state.labels)
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
},
},
actions: {
async loadAllLabels(ctx, {forceLoad} = {}) {
async loadAllLabels(ctx: ActionContext<LabelState, RootStoreState>, {forceLoad} = {}) {
if (ctx.state.loaded && !forceLoad) {
return
}
@ -74,7 +77,7 @@ export default {
cancel()
}
},
async deleteLabel(ctx, label) {
async deleteLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()
@ -87,7 +90,7 @@ export default {
cancel()
}
},
async updateLabel(ctx, label) {
async updateLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()
@ -100,7 +103,7 @@ export default {
cancel()
}
},
async createLabel(ctx, label) {
async createLabel(ctx: ActionContext<LabelState, RootStoreState>, label: ILabel) {
const cancel = setLoading(ctx, 'labels')
const labelService = new LabelService()

View File

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

View File

@ -1,22 +1,27 @@
import type {ActionContext} from 'vuex'
import NamespaceService from '../../services/namespace'
import {setLoading} from '@/store/helper'
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'])
export default {
namespaced: true,
state: () => ({
state: (): NamespaceState => ({
namespaces: [],
}),
mutations: {
namespaces(state, namespaces) {
namespaces(state: NamespaceState, namespaces: INamespace[]) {
state.namespaces = namespaces
namespaces.forEach(n => {
add(n)
})
},
setNamespaceById(state, namespace) {
setNamespaceById(state: NamespaceState, namespace: INamespace) {
const namespaceIndex = state.namespaces.findIndex(n => n.id === namespace.id)
if (namespaceIndex === -1) {
@ -30,7 +35,7 @@ export default {
state.namespaces[namespaceIndex] = namespace
update(namespace)
},
setListInNamespaceById(state, list) {
setListInNamespaceById(state: NamespaceState, list: IList) {
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.
// 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)
add(namespace)
},
removeNamespaceById(state, namespaceId) {
removeNamespaceById(state: NamespaceState, namespaceId: INamespace['id']) {
for (const n in state.namespaces) {
if (state.namespaces[n].id === namespaceId) {
remove(state.namespaces[n])
@ -59,7 +64,7 @@ export default {
}
}
},
addListToNamespace(state, list) {
addListToNamespace(state: NamespaceState, list: IList) {
for (const n in state.namespaces) {
if (state.namespaces[n].id === list.namespaceId) {
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) {
// 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.
@ -83,7 +88,7 @@ export default {
},
},
getters: {
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => {
getListAndNamespaceById: (state: NamespaceState) => (listId: IList['id'], ignorePseudoNamespaces = false) => {
for (const n in state.namespaces) {
if (ignorePseudoNamespaces && state.namespaces[n].id < 0) {
@ -101,10 +106,10 @@ export default {
}
return null
},
getNamespaceById: state => namespaceId => {
getNamespaceById: (state: NamespaceState) => (namespaceId: INamespace['id']) => {
return state.namespaces.find(({id}) => id == namespaceId) || null
},
searchNamespace: (state, getters) => query => {
searchNamespace: (state: NamespaceState, getters) => (query: string) => {
return search(query)
?.filter(value => value > 0)
.map(getters.getNamespaceById)
@ -113,7 +118,7 @@ export default {
},
},
actions: {
async loadNamespaces(ctx) {
async loadNamespaces(ctx: ActionContext<NamespaceState, RootStoreState>) {
const cancel = setLoading(ctx, 'namespaces')
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
if (ctx.state.namespaces[0].id !== -2 && ctx.state.namespaces[1]?.id !== -2) {
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) {
ctx.state.namespaces.splice(0, 1)
}
},
async deleteNamespace(ctx, namespace) {
async deleteNamespace(ctx: ActionContext<NamespaceState, RootStoreState>, namespace: INamespace) {
const cancel = setLoading(ctx, 'namespaces')
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 namespaceService = new NamespaceService()

View File

@ -1,20 +1,25 @@
import router from '@/router'
import type { ActionContext } from 'vuex'
import {formatISO} from 'date-fns'
import TaskService from '@/services/task'
import TaskAssigneeService from '@/services/taskAssignee'
import TaskAssigneeModel from '../../models/taskAssignee'
import LabelTaskModel from '../../models/labelTask'
import TaskAssigneeModel from '@/models/taskAssignee'
import LabelTaskModel from '@/models/labelTask'
import LabelTaskService from '@/services/labelTask'
import {HAS_TASKS} from '../mutation-types'
import {setLoading} from '../helper'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {parseTaskText} from '@/modules/parseTaskText'
import TaskModel from '@/models/task'
import {formatISO} from 'date-fns'
import TaskModel, { type ITask } from '@/models/task'
import LabelTask from '@/models/labelTask'
import LabelModel from '@/models/label'
import LabelModel, { type ILabel } from '@/models/label'
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
function findPropertyByValue(object, key, value) {
@ -24,16 +29,16 @@ function findPropertyByValue(object, key, value) {
}
// Check if the user exists
function validateUsername(users, username) {
function validateUsername(users: IUser[], username: IUser['username']) {
return findPropertyByValue(users, 'username', username)
}
// Check if the label exists
function validateLabel(labels, label) {
function validateLabel(labels: ILabel[], label: ILabel) {
return findPropertyByValue(labels, 'title', label)
}
async function addLabelToTask(task, label) {
async function addLabelToTask(task: ITask, label: ILabel) {
const labelTask = new LabelTask({
taskId: task.id,
labelId: label.id,
@ -62,9 +67,9 @@ async function findAssignees(parsedTaskAssignees) {
export default {
namespaced: true,
state: () => ({}),
state: (): TaskState => ({}),
actions: {
async loadTasks(ctx, params) {
async loadTasks(ctx: ActionContext<TaskState, RootStoreState>, params) {
const taskService = new TaskService()
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 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 response = await taskService.delete(task)
ctx.commit('kanban/removeTaskInBucket', task, {root: true})
@ -99,7 +104,13 @@ export default {
// Adds a task attachment in store.
// 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)
if (t.task !== null) {
const attachments = [
@ -119,7 +130,13 @@ export default {
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 taskAssigneeService = new TaskAssigneeService()
@ -148,7 +165,13 @@ export default {
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 taskAssigneeService = new TaskAssigneeService()
@ -175,8 +198,14 @@ export default {
},
async addLabel(ctx, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
async addLabel(ctx: ActionContext<TaskState, RootStoreState>, {
label,
taskId,
} : {
label: ILabel,
taskId: ITask['id']
}) {
const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService()
const r = await labelTaskService.create(labelTask)
@ -205,8 +234,8 @@ export default {
return r
},
async removeLabel(ctx, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId: taskId, labelId: label.id})
async removeLabel(ctx: ActionContext<TaskState, RootStoreState>, {label, taskId}) {
const labelTask = new LabelTaskModel({taskId, labelId: label.id})
const labelTaskService = new LabelTaskService()
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
async addLabelsToTask({rootState, dispatch}, { task, parsedLabels }) {
async addLabelsToTask({rootState, dispatch}: ActionContext<TaskState, RootStoreState>, {
task,
parsedLabels,
}) {
if (parsedLabels.length <= 0) {
return task
}
@ -257,7 +289,10 @@ export default {
return task
},
findListId({ rootGetters }, { list: listName, listId }) {
findListId({ rootGetters }: ActionContext<TaskState, RootStoreState>, { list: listName, listId }: {
list: string,
listId: IList['id']
}) {
let foundListId = null
// Uses the following ways to get the list id of the new task:
@ -285,12 +320,14 @@ export default {
return foundListId
},
async createNewTask({dispatch, commit}, {
async createNewTask({dispatch, commit}: ActionContext<TaskState, RootStoreState>, {
title,
bucketId,
listId,
position,
}) {
} :
Partial<ITask>,
) {
const cancel = setLoading({commit}, 'tasks')
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 {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import type ListModel from '@/models/list'
import type { IList } from '@/models/list'
const {t} = useI18n({useScope: 'global'})
function useSavedFilter(listId: MaybeRef<ListModel['id']>) {
function useSavedFilter(listId: MaybeRef<IList['id']>) {
const filterService = shallowRef(new SavedFilterService())
const filter = ref(new SavedFilterModel())

View File

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

View File

@ -153,9 +153,9 @@ import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import draggable from 'zhyswan-vuedraggable'
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 === []) {
return
}
@ -274,7 +274,7 @@ export default defineComponent({
focusNewTaskInput() {
this.$refs.addTask.focusTaskInput()
},
updateTaskList(task: TaskModel) {
updateTaskList(task: ITask) {
if ( this.isAlphabeticalSorting ) {
// reload tasks with current filter and sorting
this.loadTasks(1, undefined, undefined, true)
@ -288,11 +288,11 @@ export default defineComponent({
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.isTaskEdit = true
},
updateTasks(updatedTask: TaskModel) {
updateTasks(updatedTask: ITask) {
for (const t in this.tasks) {
if (this.tasks[t].id === updatedTask.id) {
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 {useTaskList} from '@/composables/taskList'
import type TaskModel from '@/models/task'
import type { ITask } from '@/models/task'
const ACTIVE_COLUMNS_DEFAULT = {
id: true,
@ -253,7 +253,7 @@ const {
currentPage,
sortByParam,
} = taskList
const tasks: Ref<TaskModel[]> = taskList.tasks
const tasks: Ref<ITask[]> = taskList.tasks
Object.assign(params.value, {
filter_by: [],

View File

@ -30,7 +30,7 @@ import CreateEdit from '@/components/misc/create-edit.vue'
import Multiselect from '@/components/input/multiselect.vue'
import ListDuplicateModel from '@/models/listDuplicateModel'
import NamespaceModel from '@/models/namespace'
import type {INamespace} from '@/models/namespace'
import {success} from '@/message'
import {useTitle} from '@/composables/useTitle'
@ -44,9 +44,9 @@ const {
findNamespaces,
} = useNameSpaceSearch()
const selectedNamespace = ref<NamespaceModel>()
const selectedNamespace = ref<INamespace>()
function selectNamespace(namespace: NamespaceModel) {
function selectNamespace(namespace: INamespace) {
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 {CURRENT_LIST} from '@/store/mutation-types'
import type ListModel from '@/models/list'
import type { IList } from '@/models/list'
import { useList } from '@/composables/useList'
import { useTitle } from '@/composables/useTitle'
const props = defineProps({
listId: {
type: Number as PropType<ListModel['id']>,
type: Number as PropType<IList['id']>,
required: true,
},
})

View File

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

View File

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

View File

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

View File

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