From f9a825b577f4266d9bd7e42baea9e28848bcd03c Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Tue, 4 Oct 2022 23:30:17 +0200 Subject: [PATCH 1/7] feat: useList composable --- src/router/index.ts | 1 + src/services/list.ts | 71 ++++++++++++++++++++++++++- src/views/list/settings/delete.vue | 24 ++++----- src/views/list/settings/duplicate.vue | 43 +++++----------- src/views/list/settings/edit.vue | 17 +++---- 5 files changed, 104 insertions(+), 52 deletions(-) diff --git a/src/router/index.ts b/src/router/index.ts index d031f34b5..1fa329511 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -300,6 +300,7 @@ const router = createRouter({ meta: { showAsModal: true, }, + props: route => ({ listId: Number(route.params.listId as string) }), }, { path: '/lists/:listId/settings/share', diff --git a/src/services/list.ts b/src/services/list.ts index 1d353de76..46f110399 100644 --- a/src/services/list.ts +++ b/src/services/list.ts @@ -1,8 +1,22 @@ +import {computed, ref, shallowReactive, unref, watch} from 'vue' +import {useRouter} from 'vue-router' +import {useI18n} from 'vue-i18n' +import type {MaybeRef} from '@vueuse/core' + +import {success} from '@/message' + import AbstractService from './abstractService' import ListModel from '@/models/list' -import type {IList} from '@/modelTypes/IList' import TaskService from './task' import {colorFromHex} from '@/helpers/color/colorFromHex' +import ListDuplicateModel from '@/models/listDuplicateModel' +import ListDuplicateService from './listDuplicateService' + +import {useListStore} from '@/stores/lists' +import {useNamespaceStore} from '@/stores/namespaces' + +import type {IList} from '@/modelTypes/IList' +import type {INamespace} from '@/modelTypes/INamespace' export default class ListService extends AbstractService { constructor() { @@ -62,4 +76,59 @@ export default class ListService extends AbstractService { cancel() } } +} + +export function useList(listId?: MaybeRef) { + const {t} = useI18n({useScope: 'global'}) + const router = useRouter() + + const listStore = useListStore() + const namespaceStore = useNamespaceStore() + + const currentListId = computed(() => unref(listId)) + const list = ref(new ListModel()) + + const listDuplicateService = shallowReactive(new ListDuplicateService()) + const isDuplicatingList = computed(() => listDuplicateService.loading) + + // load list + watch(() => unref(listId), async (watchedListId) => { + if (watchedListId === undefined) { + return + } + list.value = listStore.getListById(watchedListId) || list.value + + // TODO: load list from server + + }, {immediate: true}) + + async function duplicateList(namespaceId: INamespace['id']) { + const listDuplicate = new ListDuplicateModel({ + listId: currentListId.value, + namespaceId: namespaceId, + }) + + const duplicate = await listDuplicateService.create(listDuplicate) + + namespaceStore.addListToNamespace(duplicate.list) + listStore.setList(duplicate.list) + success({message: t('list.duplicate.success')}) + router.push({name: 'list.index', params: {listId: duplicate.list.id}}) + } + + async function deleteList() { + if (!list.value) { + return + } + + await listStore.deleteList(list.value) + success({message: t('list.delete.success')}) + router.push({name: 'home'}) + } + + return { + duplicateList, + + isDuplicatingList, + } } \ No newline at end of file diff --git a/src/views/list/settings/delete.vue b/src/views/list/settings/delete.vue index 857201919..dd2d046eb 100644 --- a/src/views/list/settings/delete.vue +++ b/src/views/list/settings/delete.vue @@ -27,34 +27,36 @@ diff --git a/src/views/list/settings/edit.vue b/src/views/list/settings/edit.vue index 4db978e6e..6284c5e38 100644 --- a/src/views/list/settings/edit.vue +++ b/src/views/list/settings/edit.vue @@ -70,7 +70,7 @@ export default { name: 'list-setting-edit' } -- 2.40.1 From af464668b376d862b95094c30e8cf85783ee60e5 Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Wed, 5 Oct 2022 14:40:31 +0200 Subject: [PATCH 2/7] feat: various formatting and type improvements --- src/components/home/TheNavigation.vue | 3 ++- src/components/home/contentAuth.vue | 3 ++- src/constants/lists.ts | 3 +++ src/constants/namespaces.ts | 5 +++++ src/helpers/getListTitle.ts | 4 +++- src/helpers/getNamespaceTitle.ts | 8 ++++--- src/modelTypes/IList.ts | 2 +- src/router/index.ts | 1 - src/services/abstractService.ts | 13 ++++++----- src/services/backgroundUnsplash.ts | 2 +- src/services/list.ts | 25 ++++++++++----------- src/stores/base.ts | 2 +- src/stores/lists.ts | 30 ++++++++++++++------------ src/stores/namespaces.ts | 20 ++++++++++++----- src/views/list/settings/archive.vue | 14 ++++++++++-- src/views/list/settings/background.vue | 3 +-- src/views/list/settings/edit.vue | 11 ++++------ 17 files changed, 90 insertions(+), 59 deletions(-) create mode 100644 src/constants/lists.ts create mode 100644 src/constants/namespaces.ts diff --git a/src/components/home/TheNavigation.vue b/src/components/home/TheNavigation.vue index 1bda78d3e..3586772ee 100644 --- a/src/components/home/TheNavigation.vue +++ b/src/components/home/TheNavigation.vue @@ -21,7 +21,7 @@ @@ -88,6 +88,7 @@ import {computed} from 'vue' import {RIGHTS as Rights} from '@/constants/rights' +import {LIST_ID} from '@/constants/lists' import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue' diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue index 02e05be2f..6583b55ea 100644 --- a/src/components/home/contentAuth.vue +++ b/src/components/home/contentAuth.vue @@ -15,7 +15,8 @@
+ :style="{'background-image': background && `url(${background})`}"> +
{ - if (n.id === -1) { + if (n.id === NAMESPACE_ID.SHARED_LIST) { return i18n.global.t('namespace.pseudo.sharedLists.title') } - if (n.id === -2) { + if (n.id === NAMESPACE_ID.FAVORITES) { return i18n.global.t('namespace.pseudo.favorites.title') } - if (n.id === -3) { + if (n.id === NAMESPACE_ID.FILTERS) { return i18n.global.t('namespace.pseudo.savedFilters.title') } return n.title diff --git a/src/modelTypes/IList.ts b/src/modelTypes/IList.ts index 9e0c84e91..dfafb7718 100644 --- a/src/modelTypes/IList.ts +++ b/src/modelTypes/IList.ts @@ -16,10 +16,10 @@ export interface IList extends IAbstract { hexColor: string identifier: string backgroundInformation: unknown | null // FIXME: improve type + backgroundBlurHash: string isFavorite: boolean subscription: ISubscription position: number - backgroundBlurHash: string created: Date updated: Date diff --git a/src/router/index.ts b/src/router/index.ts index 1fa329511..d031f34b5 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -300,7 +300,6 @@ const router = createRouter({ meta: { showAsModal: true, }, - props: route => ({ listId: Number(route.params.listId as string) }), }, { path: '/lists/:listId/settings/share', diff --git a/src/services/abstractService.ts b/src/services/abstractService.ts index 41d789433..713361362 100644 --- a/src/services/abstractService.ts +++ b/src/services/abstractService.ts @@ -2,7 +2,6 @@ import {AuthenticatedHTTPFactory} from '@/helpers/fetcher' import type {Method} from 'axios' import {objectToSnakeCase} from '@/helpers/case' -import AbstractModel from '@/models/abstractModel' import type {IAbstract} from '@/modelTypes/IAbstract' import type {Right} from '@/constants/rights' @@ -185,14 +184,14 @@ export default abstract class AbstractService) { - return data as Model + modelFactory(data : Partial = {}) { + return {...data} as Model } /** * This is the model factory for get requests. */ - modelGetFactory(data : Partial) { + modelGetFactory(data : Partial = {}) { return this.modelFactory(data) } @@ -270,7 +269,7 @@ export default abstract class AbstractService = {}) { + async getM(url : string, model : Model = this.modelGetFactory(), params: Record = {}) { const cancel = this.setLoading() model = this.beforeGet(model) @@ -293,7 +292,7 @@ export default abstract class AbstractService { constructor() { @@ -48,7 +49,7 @@ export default class ListService extends AbstractService { return model } - beforeCreate(list) { + beforeCreate(list: IList) { list.hexColor = colorFromHex(list.hexColor) return list } @@ -70,7 +71,7 @@ export default class ListService extends AbstractService { const cancel = this.setLoading() try { - const response = await this.http.delete(`/lists/${list.id}/background`, list) + const response = await this.http.delete(`/lists/${list.id}/background`) return response.data } finally { cancel() diff --git a/src/stores/base.ts b/src/stores/base.ts index e59ad45e7..9474cb659 100644 --- a/src/stores/base.ts +++ b/src/stores/base.ts @@ -4,7 +4,7 @@ import {defineStore, acceptHMRUpdate} from 'pinia' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import ListModel from '@/models/list' -import ListService from '../services/list' +import ListService from '@/services/list' import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl' import {useMenuActive} from '@/composables/useMenuActive' diff --git a/src/stores/lists.ts b/src/stores/lists.ts index 17db4debf..c7213cbdb 100644 --- a/src/stores/lists.ts +++ b/src/stores/lists.ts @@ -1,24 +1,24 @@ import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue' +import type {MaybeRef} from '@vueuse/core' import {acceptHMRUpdate, defineStore} from 'pinia' import {useI18n} from 'vue-i18n' -import ListService from '@/services/list' -import {setModuleLoading} from '@/stores/helper' -import {removeListFromHistory} from '@/modules/listHistory' -import {createNewIndexer} from '@/indexes' -import {useNamespaceStore} from './namespaces' - import type {IList} from '@/modelTypes/IList' -import type {MaybeRef} from '@vueuse/core' +import {LIST_ID} from '@/constants/lists' +import {NAMESPACE_ID} from "@/constants/namespaces" -import ListModel from '@/models/list' -import {success} from '@/message' +import {useNamespaceStore} from '@/stores/namespaces' import {useBaseStore} from '@/stores/base' -const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description']) +import ListModel from '@/models/list' +import {removeListFromHistory} from '@/modules/listHistory' +import ListService from '@/services/list' +import {setModuleLoading} from '@/stores/helper' +import {createNewIndexer} from '@/indexes' +import {success} from '@/message' -const FavoriteListsNamespace = -2 +const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description']) export interface ListState { [id: IList['id']]: IList @@ -35,7 +35,9 @@ export const useListStore = defineStore('list', () => { const getListById = computed(() => { - return (id: IList['id']) => typeof lists.value[id] !== 'undefined' ? lists.value[id] : null + return (id: IList['id']) => typeof lists.value[id] !== 'undefined' + ? lists.value[id] + : null }) const findListByExactname = computed(() => { @@ -85,7 +87,7 @@ export const useListStore = defineStore('list', () => { function toggleListFavorite(list: IList) { // The favorites pseudo list is always favorite // Archived lists cannot be marked favorite - if (list.id === -1 || list.isArchived) { + if (list.id === LIST_ID.FAVORITES || list.isArchived) { return } return updateList({ @@ -122,7 +124,7 @@ export const useListStore = defineStore('list', () => { // in order to not create a manipulation in pinia store we have to create a new copy const newList = { ...list, - namespaceId: FavoriteListsNamespace, + namespaceId: NAMESPACE_ID.FAVORITES, } namespaceStore.removeListFromNamespaceById(newList) diff --git a/src/stores/namespaces.ts b/src/stores/namespaces.ts index aa35f1606..3d5070f96 100644 --- a/src/stores/namespaces.ts +++ b/src/stores/namespaces.ts @@ -1,13 +1,17 @@ import {computed, readonly, ref} from 'vue' import {defineStore, acceptHMRUpdate} from 'pinia' -import NamespaceService from '../services/namespace' -import {setModuleLoading} from '@/stores/helper' -import {createNewIndexer} from '@/indexes' import type {INamespace} from '@/modelTypes/INamespace' import type {IList} from '@/modelTypes/IList' + +import {NAMESPACE_ID} from "@/constants/namespaces" + +import {setModuleLoading} from '@/stores/helper' import {useListStore} from '@/stores/lists' +import {createNewIndexer} from '@/indexes' +import NamespaceService from '@/services/namespace' + const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description']) export const useNamespaceStore = defineStore('namespace', () => { @@ -169,14 +173,20 @@ export const useNamespaceStore = defineStore('namespace', () => { function loadNamespacesIfFavoritesDontExist() { // The first or second namespace should be the one holding all favorites - if (namespaces.value[0].id === -2 || namespaces.value[1]?.id === -2) { + if ( + namespaces.value[0].id === NAMESPACE_ID.FAVORITES || + namespaces.value[1]?.id === NAMESPACE_ID.FAVORITES + ) { return } return loadNamespaces() } function removeFavoritesNamespaceIfEmpty() { - if (namespaces.value[0].id === -2 && namespaces.value[0].lists.length === 0) { + if ( + namespaces.value[0].id === NAMESPACE_ID.FAVORITES && + namespaces.value[0].lists.length === 0 + ) { namespaces.value.splice(0, 1) } } diff --git a/src/views/list/settings/archive.vue b/src/views/list/settings/archive.vue index 5295f9641..ce8ed9e71 100644 --- a/src/views/list/settings/archive.vue +++ b/src/views/list/settings/archive.vue @@ -3,10 +3,20 @@ @close="$router.back()" @submit="archiveList()" > - + diff --git a/src/views/list/settings/background.vue b/src/views/list/settings/background.vue index b74e037fb..68bb8a4b4 100644 --- a/src/views/list/settings/background.vue +++ b/src/views/list/settings/background.vue @@ -183,8 +183,7 @@ async function searchBackgrounds(page = 1) { }) } - -async function setBackground(backgroundId: string) { +async function setBackground(backgroundId: IBackgroundImage['id']) { // Don't set a background if we're in the process of setting one if (backgroundService.loading) { return diff --git a/src/views/list/settings/edit.vue b/src/views/list/settings/edit.vue index 6284c5e38..eb3227b8d 100644 --- a/src/views/list/settings/edit.vue +++ b/src/views/list/settings/edit.vue @@ -70,7 +70,7 @@ export default { name: 'list-setting-edit' } -- 2.40.1 From d2870ce493c279c9762494645e4147cc17411aa8 Mon Sep 17 00:00:00 2001 From: Dominik Pschenitschni Date: Wed, 5 Oct 2022 14:41:07 +0200 Subject: [PATCH 3/7] WIP: List composable --- .../quick-actions/quick-actions.vue | 12 +- src/constants/lists.ts | 2 +- src/constants/namespaces.ts | 2 +- src/helpers/getNamespaceTitle.ts | 2 +- src/router/index.ts | 1 + src/services/abstractService.ts | 102 ++---- src/services/backgroundUnsplash.ts | 10 +- src/services/backgroundUpload.ts | 11 +- src/services/list.ts | 104 ++---- src/services/task.ts | 2 +- src/stores/base.ts | 34 +- src/stores/lists.ts | 341 +++++++++++++++--- src/stores/namespaces.ts | 2 +- src/views/list/NewList.vue | 52 ++- src/views/list/settings/archive.vue | 31 +- src/views/list/settings/background.vue | 83 +---- src/views/list/settings/delete.vue | 18 +- src/views/list/settings/duplicate.vue | 9 +- src/views/list/settings/edit.vue | 25 +- src/views/list/settings/share.vue | 43 +-- 20 files changed, 491 insertions(+), 395 deletions(-) diff --git a/src/components/quick-actions/quick-actions.vue b/src/components/quick-actions/quick-actions.vue index 5769adbc3..93b412098 100644 --- a/src/components/quick-actions/quick-actions.vue +++ b/src/components/quick-actions/quick-actions.vue @@ -69,7 +69,7 @@ import BaseButton from '@/components/base/BaseButton.vue' import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue' import {useBaseStore} from '@/stores/base' -import {useListStore} from '@/stores/lists' +import {useList, useListStore} from '@/stores/lists' import {useNamespaceStore} from '@/stores/namespaces' import {useLabelStore} from '@/stores/labels' import {useTaskStore} from '@/stores/tasks' @@ -493,18 +493,16 @@ async function newTask() { await router.push({ name: 'task.detail', params: { id: task.id } }) } +const {createList} = useList() + async function newList() { if (currentList.value === null) { return } - const newList = await listStore.createList(new ListModel({ + + await createList({ title: query.value, namespaceId: currentList.value.namespaceId, - })) - success({ message: t('list.create.createdSuccess')}) - await router.push({ - name: 'list.index', - params: { listId: newList.id }, }) } diff --git a/src/constants/lists.ts b/src/constants/lists.ts index c242ff1c9..1bbf38078 100644 --- a/src/constants/lists.ts +++ b/src/constants/lists.ts @@ -1,3 +1,3 @@ export const LIST_ID = { - FAVORITES: -1 + FAVORITES: -1, } as const \ No newline at end of file diff --git a/src/constants/namespaces.ts b/src/constants/namespaces.ts index 6b9b3567d..f06013c5f 100644 --- a/src/constants/namespaces.ts +++ b/src/constants/namespaces.ts @@ -2,4 +2,4 @@ export const NAMESPACE_ID = { SHARED_LIST: -1, FAVORITES: -2, FILTERS: -3, -} as const; +} as const diff --git a/src/helpers/getNamespaceTitle.ts b/src/helpers/getNamespaceTitle.ts index aab824f5d..dea1af2cf 100644 --- a/src/helpers/getNamespaceTitle.ts +++ b/src/helpers/getNamespaceTitle.ts @@ -1,7 +1,7 @@ import {i18n} from '@/i18n' import type {INamespace} from '@/modelTypes/INamespace' -import {NAMESPACE_ID} from "@/constants/namespaces" +import {NAMESPACE_ID} from '@/constants/namespaces' export const getNamespaceTitle = (n: INamespace) => { if (n.id === NAMESPACE_ID.SHARED_LIST) { diff --git a/src/router/index.ts b/src/router/index.ts index d031f34b5..1fa329511 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -300,6 +300,7 @@ const router = createRouter({ meta: { showAsModal: true, }, + props: route => ({ listId: Number(route.params.listId as string) }), }, { path: '/lists/:listId/settings/share', diff --git a/src/services/abstractService.ts b/src/services/abstractService.ts index 713361362..ff27effae 100644 --- a/src/services/abstractService.ts +++ b/src/services/abstractService.ts @@ -14,6 +14,11 @@ interface Paths { reset?: string } +/** + * The replacement pattern for url paths, can be overwritten by implementations. + */ +const ROUTE_PARAMETER_PATTERN = /{([^}]+)}/ + function convertObject(o: Record) { if (o instanceof Date) { return o.toISOString() @@ -74,10 +79,8 @@ export default abstract class AbstractService { switch (config.method) { case 'post': - if (this.useUpdateInterceptor()) { - config.data = this.beforeUpdate(config.data) - config.data = objectToSnakeCase(config.data) - } + config.data = this.beforeUpdate(config.data) + config.data = objectToSnakeCase(config.data) break case 'put': if (this.useCreateInterceptor()) { @@ -86,10 +89,7 @@ export default abstract class AbstractService = {}) { + private getRouteReplacements(route: string, parameters: {[key: string]: unknown} = {}) { const replace$$1: Record = {} - let pattern = this.getRouteParameterPattern() - pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g') + const pattern = new RegExp(ROUTE_PARAMETER_PATTERN, 'g') for (let parameter; (parameter = pattern.exec(route)) !== null;) { replace$$1[parameter[0]] = parameters[parameter[1]] @@ -138,17 +123,10 @@ export default abstract class AbstractService) : string { + getReplacedRoute(path : string, pathparams : M) : string { const replacements = this.getRouteReplacements(path, pathparams) return Object.entries(replacements).reduce( (result, [parameter, value]) => result.replace(parameter, value as string), @@ -157,8 +135,8 @@ export default abstract class AbstractService = {}) { - return {...data} as Model + modelFactory>(data: DefaultModel = {}) { + return {...data} as DefaultModel } /** * This is the model factory for get requests. */ - modelGetFactory(data : Partial = {}) { - return this.modelFactory(data) + modelGetFactory>(data: GetModel = {}) { + return this.modelFactory(data) } /** * This is the model factory for get all requests. */ - modelGetAllFactory(data : Partial) { - return this.modelFactory(data) + modelGetAllFactory>(data: GetAllModel = {}) { + return this.modelFactory(data) } /** * This is the model factory for create requests. */ - modelCreateFactory(data : Partial) { - return this.modelFactory(data) + modelCreateFactory>(data: CreateModel = {}) { + return this.modelFactory(data) } /** * This is the model factory for update requests. */ - modelUpdateFactory(data : Partial) { - return this.modelFactory(data) + modelUpdateFactory>(data: UpdateModel = {}) { + return this.modelFactory(data) } ////////////// @@ -223,28 +201,21 @@ export default abstract class AbstractService = {}) { + async getM(url : string, model: Model = this.modelGetFactory(), params: Record = {}) { const cancel = this.setLoading() model = this.beforeGet(model) const finalUrl = this.getReplacedRoute(url, model) try { - const response = await this.http.get(finalUrl, {params: prepareParams(params)}) + const response = await this.http.get(finalUrl, {params: prepareParams(params)}) const result = this.modelGetFactory(response.data) result.maxRight = Number(response.headers['x-max-right']) as Right return result @@ -314,7 +285,7 @@ export default abstract class AbstractService(finalUrl, {params: prepareParams(params)}) this.resultCount = Number(response.headers['x-pagination-result-count']) this.totalPages = Number(response.headers['x-pagination-total-pages']) @@ -325,7 +296,7 @@ export default abstract class AbstractService this.modelGetAllFactory(entry)) } - return this.modelGetAllFactory(response.data) + return [this.modelGetAllFactory(response.data)] } finally { cancel() } @@ -333,9 +304,8 @@ export default abstract class AbstractService} */ - async create(model : Model) { + async create(model: M) { if (this.paths.create === '') { throw new Error('This model is not able to create data.') } @@ -344,7 +314,7 @@ export default abstract class AbstractService(finalUrl, model) const result = this.modelCreateFactory(response.data) if (typeof model.maxRight !== 'undefined') { result.maxRight = model.maxRight @@ -359,11 +329,11 @@ export default abstract class AbstractService(url : string, model: PostModel) { const cancel = this.setLoading() try { - const response = await this.http.post(url, model) + const response = await this.http.post(url, model) const result = this.modelUpdateFactory(response.data) if (typeof model.maxRight !== 'undefined') { result.maxRight = model.maxRight @@ -377,7 +347,7 @@ export default abstract class AbstractService(model: M) { if (this.paths.update === '') { throw new Error('This model is not able to update data.') } diff --git a/src/services/backgroundUnsplash.ts b/src/services/backgroundUnsplash.ts index 0378fe0c9..32ba970d3 100644 --- a/src/services/backgroundUnsplash.ts +++ b/src/services/backgroundUnsplash.ts @@ -1,7 +1,9 @@ -import AbstractService from './abstractService' -import BackgroundImageModel from '../models/backgroundImage' +import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage' + +import AbstractService from '@/services/abstractService' + +import BackgroundImageModel from '@/models/backgroundImage' import ListModel from '@/models/list' -import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage' export default class BackgroundUnsplashService extends AbstractService { constructor() { @@ -11,7 +13,7 @@ export default class BackgroundUnsplashService extends AbstractService) { + modelFactory(data) { return new BackgroundImageModel(data) } diff --git a/src/services/backgroundUpload.ts b/src/services/backgroundUpload.ts index 2d1ec54cc..5e726a8e4 100644 --- a/src/services/backgroundUpload.ts +++ b/src/services/backgroundUpload.ts @@ -1,10 +1,9 @@ import AbstractService from './abstractService' import ListModel from '@/models/list' -import type { IList } from '@/modelTypes/IList' -import type { IFile } from '@/modelTypes/IFile' +import type {IList} from '@/modelTypes/IList' -export default class BackgroundUploadService extends AbstractService { +export default class BackgroundUploadService extends AbstractService { constructor() { super({ create: '/lists/{listId}/backgrounds/upload', @@ -20,9 +19,9 @@ export default class BackgroundUploadService extends AbstractService { } /** - * Uploads a file to the server - */ - create(listId: IList['id'], file: IFile) { + * Uploads a file to the server + */ + create(listId: IList['id'], file: File) { return this.uploadFile( this.getReplacedRoute(this.paths.create, {listId}), file, diff --git a/src/services/list.ts b/src/services/list.ts index a0f8d45d0..f3167f0fe 100644 --- a/src/services/list.ts +++ b/src/services/list.ts @@ -1,23 +1,12 @@ -import {computed, ref, shallowReactive, unref, watch} from 'vue' -import {useRouter} from 'vue-router' -import {useI18n} from 'vue-i18n' -import type {MaybeRef} from '@vueuse/core' - import type {IList} from '@/modelTypes/IList' -import type {INamespace} from '@/modelTypes/INamespace' - -import {useListStore} from '@/stores/lists' -import {useNamespaceStore} from '@/stores/namespaces' import AbstractService from '@/services/abstractService' import TaskService from '@/services/task' -import ListDuplicateService from '@/services/listDuplicateService' import ListModel from '@/models/list' -import ListDuplicateModel from '@/models/listDuplicateModel' import {colorFromHex} from '@/helpers/color/colorFromHex' -import {success} from '@/message' +import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' export default class ListService extends AbstractService { constructor() { @@ -54,17 +43,35 @@ export default class ListService extends AbstractService { return list } - async background(list: Pick) { - if (list.backgroundInformation === null) { - return '' + // FIXME: move out of service + async getBlurHash(blurHashString: IList['backgroundBlurHash']) { + if (!blurHashString) { + return } + const blurHashBlob = await getBlobFromBlurHash(blurHashString) + if (!blurHashBlob) { + return + } + return URL.createObjectURL(blurHashBlob) + } - const response = await this.http({ - url: `/lists/${list.id}/background`, - method: 'GET', - responseType: 'blob', - }) - return window.URL.createObjectURL(new Blob([response.data])) + async loadBackground(list: Pick) { + if (list === null || !list.backgroundInformation) { + return + } + + const cancel = this.setLoading() + + try { + const response = await this.http({ + url: `/lists/${list.id}/background`, + method: 'GET', + responseType: 'blob', + }) + return URL.createObjectURL(new Blob([response.data])) + } finally { + cancel() + } } async removeBackground(list: Pick) { @@ -77,59 +84,4 @@ export default class ListService extends AbstractService { cancel() } } -} - -export function useList(listId?: MaybeRef) { - const {t} = useI18n({useScope: 'global'}) - const router = useRouter() - - const listStore = useListStore() - const namespaceStore = useNamespaceStore() - - const currentListId = computed(() => unref(listId)) - const list = ref(new ListModel()) - - const listDuplicateService = shallowReactive(new ListDuplicateService()) - const isDuplicatingList = computed(() => listDuplicateService.loading) - - // load list - watch(() => unref(listId), async (watchedListId) => { - if (watchedListId === undefined) { - return - } - list.value = listStore.getListById(watchedListId) || list.value - - // TODO: load list from server - - }, {immediate: true}) - - async function duplicateList(namespaceId: INamespace['id']) { - const listDuplicate = new ListDuplicateModel({ - listId: currentListId.value, - namespaceId: namespaceId, - }) - - const duplicate = await listDuplicateService.create(listDuplicate) - - namespaceStore.addListToNamespace(duplicate.list) - listStore.setList(duplicate.list) - success({message: t('list.duplicate.success')}) - router.push({name: 'list.index', params: {listId: duplicate.list.id}}) - } - - async function deleteList() { - if (!list.value) { - return - } - - await listStore.deleteList(list.value) - success({message: t('list.delete.success')}) - router.push({name: 'home'}) - } - - return { - duplicateList, - - isDuplicatingList, - } } \ No newline at end of file diff --git a/src/services/task.ts b/src/services/task.ts index b7f9ddd88..0d87b7740 100644 --- a/src/services/task.ts +++ b/src/services/task.ts @@ -100,7 +100,7 @@ export default class TaskService extends AbstractService { }) }) - // Process all attachments to preven parsing errors + // Process all attachments to prevent parsing errors if (model.attachments.length > 0) { const attachmentService = new AttachmentService() model.attachments.map(a => { diff --git a/src/stores/base.ts b/src/stores/base.ts index 9474cb659..c816a5c9b 100644 --- a/src/stores/base.ts +++ b/src/stores/base.ts @@ -1,8 +1,6 @@ import { readonly, ref} from 'vue' import {defineStore, acceptHMRUpdate} from 'pinia' -import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' - import ListModel from '@/models/list' import ListService from '@/services/list' import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl' @@ -90,20 +88,30 @@ export const useBaseStore = defineStore('base', () => { // The forceUpdate parameter is used only when updating a list background directly because in that case // the current list stays the same, but we want to show the new background right away. - if (list.id !== currentList.value?.id || forceUpdate) { - if (list.backgroundInformation) { - try { - const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash) + if ( + (list.id !== currentList.value?.id || forceUpdate) && + list.backgroundInformation + ) { + try { + const listService = new ListService() + const blurHashPromise = listService.getBlurHash(list.backgroundBlurHash).then(blurHash => { if (blurHash) { - setBlurHash(window.URL.createObjectURL(blurHash)) + setBlurHash(blurHash) + } + }) + const backgroundPromise = listService.loadBackground(list).then(background => { + if (background === undefined) { + throw new Error() } - - const listService = new ListService() - const background = await listService.background(list) setBackground(background) - } catch (e) { - console.error('Error getting background image for list', list.id, e) - } + }) + + await Promise.all([ + blurHashPromise, + backgroundPromise, + ]) + } catch (e) { + console.error('Error getting background image for list', list.id, e) } } diff --git a/src/stores/lists.ts b/src/stores/lists.ts index c7213cbdb..c73612871 100644 --- a/src/stores/lists.ts +++ b/src/stores/lists.ts @@ -1,22 +1,32 @@ -import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue' +import {watch, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue' import type {MaybeRef} from '@vueuse/core' import {acceptHMRUpdate, defineStore} from 'pinia' import {useI18n} from 'vue-i18n' +import {useRouter} from 'vue-router' import type {IList} from '@/modelTypes/IList' +import type {INamespace} from '@/modelTypes/INamespace' +import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage' import {LIST_ID} from '@/constants/lists' -import {NAMESPACE_ID} from "@/constants/namespaces" +import {NAMESPACE_ID} from '@/constants/namespaces' -import {useNamespaceStore} from '@/stores/namespaces' -import {useBaseStore} from '@/stores/base' +import ListService from '@/services/list' +import ListDuplicateService from '@/services/listDuplicateService' +import BackgroundUnsplashService from '@/services/backgroundUnsplash' import ListModel from '@/models/list' +import ListDuplicateModel from '@/models/listDuplicateModel' import {removeListFromHistory} from '@/modules/listHistory' -import ListService from '@/services/list' + import {setModuleLoading} from '@/stores/helper' +import {useBaseStore} from './base' +import {useNamespaceStore} from './namespaces' + import {createNewIndexer} from '@/indexes' import {success} from '@/message' +import { i18n } from '@/i18n' +import BackgroundUploadService from '@/services/backgroundUpload' const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description']) @@ -25,6 +35,8 @@ export interface ListState { } export const useListStore = defineStore('list', () => { + const router = useRouter() + const baseStore = useBaseStore() const namespaceStore = useNamespaceStore() @@ -55,7 +67,7 @@ export const useListStore = defineStore('list', () => { ?.filter(value => value > 0) .map(id => lists.value[id]) .filter(list => list.isArchived === includeArchived) - || [] + || [] as IList[] } }) @@ -79,11 +91,6 @@ export const useListStore = defineStore('list', () => { }) } - function removeListById(list: IList) { - remove(list) - delete lists.value[list.id] - } - function toggleListFavorite(list: IList) { // The favorites pseudo list is always favorite // Archived lists cannot be marked favorite @@ -96,6 +103,20 @@ export const useListStore = defineStore('list', () => { }) } + async function loadList(listId: List['id']) { + const cancel = setModuleLoading(this, setIsLoading) + const listService = new ListService() + + try { + const list = await listService.get(new ListModel({id: listId})) + setList(list) + namespaceStore.setListInNamespaceById(list) + return list + } finally { + cancel() + } + } + async function createList(list: IList) { const cancel = setModuleLoading(setIsLoading) const listService = new ListService() @@ -152,7 +173,8 @@ export const useListStore = defineStore('list', () => { try { const response = await listService.delete(list) - removeListById(list) + remove(list) + delete lists.value[list.id] namespaceStore.removeListFromNamespaceById(list) removeListFromHistory({id: list.id}) return response @@ -161,6 +183,118 @@ export const useListStore = defineStore('list', () => { } } + async function archiveList(list: IList) { + try { + const newList = await updateList({ + ...list, + isArchived: !list.isArchived, + }) + baseStore.setCurrentList(newList) + success({message: i18n.global.t('list.archive.success')}) + } finally { + router.back() + } + } + + const listDuplicateService = shallowReactive(new ListDuplicateService()) + const isDuplicatingList = computed(() => listDuplicateService.loading) + async function duplicateList(listId: IList['id'], namespaceId: INamespace['id'] | undefined) { + if (namespaceId === undefined) { + throw new Error() + } + + const listDuplicate = new ListDuplicateModel({ + listId, + namespaceId: namespaceId, + }) + + const duplicate = await listDuplicateService.create(listDuplicate) + + namespaceStore.addListToNamespace(duplicate.list) + setList(duplicate.list) + success({message: i18n.global.t('list.duplicate.success')}) + router.push({name: 'list.index', params: {listId: duplicate.list.id}}) + } + + + const backgroundService = shallowReactive(new BackgroundUnsplashService()) + async function setUnsplashBackground(listId: IList['id'], backgroundId: IBackgroundImage['id']) { + // Don't set a background if we're in the process of setting one + if (backgroundService.loading || listId === undefined) { + return + } + + const list: IList = await backgroundService.update({ + id: backgroundId, + listId, + }) + await baseStore.handleSetCurrentList({list, forceUpdate: true}) + namespaceStore.setListInNamespaceById(list) + setList(list) + success({message: i18n.global.t('list.background.success')}) + } + + const backgroundUploadService = shallowReactive(new BackgroundUploadService()) + const isUploadingBackground = computed(() => backgroundUploadService.loading) + async function uploadListBackground(listId: IList['id'], file: File | undefined) { + if (listId === undefined || file === undefined) { + return + } + + const list = await backgroundUploadService.create(listId, file) + + await baseStore.handleSetCurrentList({list, forceUpdate: true}) + namespaceStore.setListInNamespaceById(list) + setList(list) + success({message: i18n.global.t('list.background.success')}) + } + + async function removeListBackground(listId: IList['id']) { + const listService = new ListService() + const listWithBackground = getListById.value(listId) + if (listWithBackground === null) { + return + } + const list = await listService.removeBackground(listWithBackground) + await baseStore.handleSetCurrentList({list, forceUpdate: true}) + namespaceStore.setListInNamespaceById(list) + setList(list) + success({message: i18n.global.t('list.background.removeSuccess')}) + router.back() + } + + + async function loadListBackground(list: IList) { + const result = { + blurHash: '', + background: '', + } + + try { + const listService = new ListService() + const blurHashPromise = listService.getBlurHash(list.backgroundBlurHash).then(blurHash => { + if (blurHash) { + result.blurHash = blurHash + } + }) + const backgroundPromise = listService.loadBackground(list).then(background => { + if (background === undefined) { + throw new Error() + } + result.background = background + }) + + await Promise.all([ + blurHashPromise, + backgroundPromise, + ]) + } catch (e) { + console.error('Error getting background image for list', list.id, e) + } + + return result + } + return { isLoading: readonly(isLoading), lists: readonly(lists), @@ -171,43 +305,168 @@ export const useListStore = defineStore('list', () => { setList, setLists, - removeListById, toggleListFavorite, + + // crud + loadList, createList, updateList, deleteList, + + archiveList, + + //duplciate + isDuplicatingList, + duplicateList, + + isUploadingBackground, + uploadListBackground, + removeListBackground, + setUnsplashBackground, + loadListBackground, } }) -export function useList(listId: MaybeRef) { - const listService = shallowReactive(new ListService()) - const {loading: isLoading} = toRefs(listService) - const list: IList = reactive(new ListModel()) - const {t} = useI18n({useScope: 'global'}) - - watch( - () => unref(listId), - async (listId) => { - const loadedList = await listService.get(new ListModel({id: listId})) - Object.assign(list, loadedList) - }, - {immediate: true}, - ) - - const listStore = useListStore() - async function save() { - await listStore.updateList(list) - success({message: t('list.edit.success')}) - } - - return { - isLoading: readonly(isLoading), - list, - save, - } -} - // support hot reloading if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(useListStore, import.meta.hot)) +} + +export function useListBase(listId?: MaybeRef) { + const listStore = useListStore() + + const listService = shallowReactive(new ListService()) + const {loading: isLoading} = toRefs(listService) + const list = ref(new ListModel()) + const currentListId = computed(() => unref(listId)) + + // load list + watch( + currentListId, + async (listId, oldListId) => { + if (listId === oldListId) { + return + } + if (listId === undefined) { + list.value = new ListModel() + return + } + list.value = listStore.getListById(listId) || list.value + // FIXME: does this also make sense for a newly created list? + list.value = await listService.get(new ListModel({id: listId})) + }, + {immediate: true}, + ) + + return { + listStore, + listService, + isLoading, + currentListId, + list, + } +} + +export function useList(listId?: MaybeRef) { + const { + listStore, + listService, + isLoading, + list, + } = useListBase(listId) + + + const {t} = useI18n({useScope: 'global'}) + const router = useRouter() + const baseStore = useBaseStore() + const namespaceStore = useNamespaceStore() + + const errors = ref<{ [key: string]: string | boolean }>({}) + + async function createList(newList?: Partial) { + if (newList !== undefined) { + list.value = new ListModel(newList) + } + + if (list.value.title === '') { + errors.value.createList = true + return + } + errors.value.createList = false + + list.value = await listStore.createList(list.value) + await router.push({ + name: 'list.index', + params: { listId: list.value.id }, + }) + success({message: t('list.create.createdSuccess') }) + } + + async function saveList() { + await listStore.updateList(list.value) + success({message: t('list.edit.success')}) + await baseStore.handleSetCurrentList({list: list.value}) + router.back() + } + + async function deleteList() { + if (!list.value) { + return + } + + await listStore.deleteList(list.value) + success({message: t('list.delete.success')}) + router.push({name: 'home'}) + } + + async function removeListBackground() { + list.value = await listService.removeBackground(list.value) + await useBaseStore().handleSetCurrentList({list: list.value, forceUpdate: true}) + namespaceStore.setListInNamespaceById(list.value) + listStore.setList(list.value) + success({message: t('list.background.removeSuccess')}) + router.back() + } + + return { + isLoading: readonly(isLoading), + errors: readonly(errors), + list, + + createList, + saveList, + deleteList, + removeListBackground, + } +} + +export function useListBackground(listId?: MaybeRef) { + const { + isLoading, + list, + currentListId, + } = useListBase(listId) + + const blurHash = ref() + const background = ref() + + const listStore = useListStore() + + watch( + currentListId, + async (id) => { + if (id === undefined || isLoading) { + return + } + const result = await listStore.loadListBackground(list.value) + blurHash.value = result.blurHash + background.value = result.background + }, + {immediate: true}, + ) + + return { + blurHash, + background, + } } \ No newline at end of file diff --git a/src/stores/namespaces.ts b/src/stores/namespaces.ts index 3d5070f96..bf44f645c 100644 --- a/src/stores/namespaces.ts +++ b/src/stores/namespaces.ts @@ -4,7 +4,7 @@ import {defineStore, acceptHMRUpdate} from 'pinia' import type {INamespace} from '@/modelTypes/INamespace' import type {IList} from '@/modelTypes/IList' -import {NAMESPACE_ID} from "@/constants/namespaces" +import {NAMESPACE_ID} from '@/constants/namespaces' import {setModuleLoading} from '@/stores/helper' import {useListStore} from '@/stores/lists' diff --git a/src/views/list/NewList.vue b/src/views/list/NewList.vue index 45bd39309..0ed0fdbbc 100644 --- a/src/views/list/NewList.vue +++ b/src/views/list/NewList.vue @@ -1,13 +1,17 @@ \ No newline at end of file diff --git a/src/views/list/settings/archive.vue b/src/views/list/settings/archive.vue index ce8ed9e71..8eb6c5a31 100644 --- a/src/views/list/settings/archive.vue +++ b/src/views/list/settings/archive.vue @@ -1,7 +1,7 @@