feature/feat-useList-composable #2589
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<header
|
||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
||||
:class="{'has-background': hasBackground, 'menu-active': menuActive}"
|
||||
aria-label="main navigation"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
|
@ -21,7 +21,7 @@
|
|||
</BaseButton>
|
||||
|
||||
<list-settings-dropdown
|
||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
||||
v-if="canWriteCurrentList && currentList.id !== LIST_ID.FAVORITES"
|
||||
class="list-title-dropdown"
|
||||
:list="currentList"
|
||||
>
|
||||
|
@ -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'
|
||||
|
@ -105,7 +106,7 @@ import {useAuthStore} from '@/stores/auth'
|
|||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const background = computed(() => baseStore.background)
|
||||
const hasBackground = computed(() => Boolean(baseStore.currentList?.backgroundInformation))
|
||||
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
</BaseButton>
|
||||
<div
|
||||
class="app-container"
|
||||
:class="{'has-background': background || blurHash}"
|
||||
:style="{'background-image': blurHash && `url(${blurHash})`}"
|
||||
:class="{'has-background': backgroundUrl || blurHashUrl}"
|
||||
:style="{'background-image': blurHashUrl ? `url(${blurHashUrl})` : undefined}"
|
||||
>
|
||||
<div
|
||||
:class="{'is-visible': background}"
|
||||
:class="{'is-visible': backgroundUrl}"
|
||||
class="app-container-background background-fade-in d-print-none"
|
||||
:style="{'background-image': background && `url(${background})`}"></div>
|
||||
:style="{'background-image': backgroundUrl ? `url(${backgroundUrl})` : undefined}">
|
||||
</div>
|
||||
<navigation class="d-print-none"/>
|
||||
<main
|
||||
class="app-content"
|
||||
|
@ -72,13 +73,15 @@ import {useLabelStore} from '@/stores/labels'
|
|||
|
||||
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
||||
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
||||
import {useListBackground} from '@/stores/lists'
|
||||
|
||||
const {routeWithModal, currentModal, closeModal} = useRouteWithModal()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const background = computed(() => baseStore.background)
|
||||
const blurHash = computed(() => baseStore.blurHash)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
|
||||
const {backgroundUrl, blurHashUrl} = useListBackground(currentList)
|
||||
|
||||
function showKeyboardShortcuts() {
|
||||
baseStore.setKeyboardShortcutsActive(true)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[background ? 'has-background' : '', $route.name as string +'-view']"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
:class="[backgroundUrl ? 'has-background' : '', $route.name as string +'-view']"
|
||||
:style="{'background-image': backgroundUrl ? `url(${backgroundUrl})`: undefined}"
|
||||
class="link-share-container"
|
||||
>
|
||||
<div class="container has-text-centered link-share-view">
|
||||
|
@ -26,14 +26,18 @@
|
|||
import {computed} from 'vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListBackground} from '@/stores/lists'
|
||||
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
import PoweredByLink from './PoweredByLink.vue'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const background = computed(() => baseStore.background)
|
||||
const logoVisible = computed(() => baseStore.logoVisible)
|
||||
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
|
||||
// TODO: use blurhash here aswell
|
||||
const{backgroundUrl} = useListBackground(currentList)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -132,8 +132,6 @@ watch(
|
|||
loadedListId.value = 0
|
||||
const listFromStore = listStore.getListById(listData.id)
|
||||
if (listFromStore !== null) {
|
||||
baseStore.setBackground(null)
|
||||
baseStore.setBlurHash(null)
|
||||
baseStore.handleSetCurrentList({list: listFromStore})
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,13 @@
|
|||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
{{ $t('menu.delete') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div
|
||||
class="list-card"
|
||||
:class="{
|
||||
'has-light-text': background !== null,
|
||||
'has-background': blurHashUrl !== '' || background !== null
|
||||
'has-light-text': backgroundUrl !== null,
|
||||
'has-background': blurHashUrl !== '' || backgroundUrl !== null
|
||||
}"
|
||||
:style="{
|
||||
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined,
|
||||
|
@ -12,8 +12,8 @@
|
|||
>
|
||||
<div
|
||||
class="list-background background-fade-in"
|
||||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
:class="{'is-visible': backgroundUrl}"
|
||||
:style="{'background-image': backgroundUrl !== null ? `url(${backgroundUrl})` : undefined}"
|
||||
/>
|
||||
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
||||
|
||||
|
@ -45,8 +45,7 @@ import type {IList} from '@/modelTypes/IList'
|
|||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {useListBackground} from './useListBackground'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useListBackground, useListStore} from '@/stores/lists'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
|
@ -55,7 +54,7 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
const {backgroundUrl, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
|
||||
const listStore = useListStore()
|
||||
</script>
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import {ref, watch, type Ref} from 'vue'
|
||||
import ListService from '@/services/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
export function useListBackground(list: Ref<IList>) {
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
const blurHashUrl = ref('')
|
||||
|
||||
watch(
|
||||
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
|
||||
async ([listId, blurHash], oldValue) => {
|
||||
if (
|
||||
list.value === null ||
|
||||
!list.value.backgroundInformation ||
|
||||
backgroundLoading.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [oldListId, oldBlurHash] = oldValue || []
|
||||
if (
|
||||
oldValue !== undefined &&
|
||||
listId === oldListId && blurHash === oldBlurHash
|
||||
) {
|
||||
// list hasn't changed
|
||||
return
|
||||
}
|
||||
|
||||
backgroundLoading.value = true
|
||||
|
||||
try {
|
||||
const blurHashPromise = getBlobFromBlurHash(blurHash).then((blurHash) => {
|
||||
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
|
||||
})
|
||||
|
||||
const listService = new ListService()
|
||||
const backgroundPromise = listService.background(list.value).then((result) => {
|
||||
background.value = result
|
||||
})
|
||||
await Promise.all([blurHashPromise, backgroundPromise])
|
||||
} finally {
|
||||
backgroundLoading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
background,
|
||||
blurHashUrl,
|
||||
backgroundLoading,
|
||||
}
|
||||
}
|
|
@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const LIST_ID = {
|
||||
FAVORITES: -1,
|
||||
} as const
|
|
@ -0,0 +1,5 @@
|
|||
export const NAMESPACE_ID = {
|
||||
SHARED_LIST: -1,
|
||||
FAVORITES: -2,
|
||||
FILTERS: -3,
|
||||
} as const
|
|
@ -29,3 +29,14 @@ export async function getBlobFromBlurHash(blurHash: string): Promise<Blob | null
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getBlurHash(blurHashString: string) {
|
||||
if (!blurHashString) {
|
||||
return
|
||||
}
|
||||
const blurHashBlob = await getBlobFromBlurHash(blurHashString)
|
||||
if (!blurHashBlob) {
|
||||
return
|
||||
}
|
||||
return URL.createObjectURL(blurHashBlob)
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {LIST_ID} from '@/constants/lists'
|
||||
|
||||
export function getListTitle(l: IList) {
|
||||
if (l.id === -1) {
|
||||
if (l.id === LIST_ID.FAVORITES) {
|
||||
return i18n.global.t('list.pseudo.favorites.title')
|
||||
}
|
||||
return l.title
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||
|
||||
export const getNamespaceTitle = (n: INamespace) => {
|
||||
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
|
||||
|
|
|
@ -169,7 +169,6 @@
|
|||
"title": "List Title",
|
||||
"color": "Color",
|
||||
"lists": "Lists",
|
||||
"list": "List",
|
||||
konrad marked this conversation as resolved
Outdated
|
||||
"search": "Type to search for a list…",
|
||||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
|
@ -187,7 +186,8 @@
|
|||
"unarchive": "Un-Archive this list",
|
||||
"unarchiveText": "You will be able to create new tasks or edit it.",
|
||||
"archiveText": "You won't be able to edit this list or create new tasks until you un-archive it.",
|
||||
"success": "The list was successfully archived."
|
||||
"successArchived": "The list has been successfully archived.",
|
||||
"successUnarchived": "The list has been successfully unarchived."
|
||||
},
|
||||
"background": {
|
||||
"title": "Set list background",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -275,6 +275,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ namespaceId: Number(route.params.namespaceId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
|
@ -292,6 +293,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
|
@ -300,6 +302,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
|
@ -308,6 +311,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
|
@ -316,6 +320,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
|
@ -324,6 +329,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
@ -15,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<string, unknown>) {
|
||||
if (o instanceof Date) {
|
||||
return o.toISOString()
|
||||
|
@ -75,10 +79,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
this.http.interceptors.request.use((config) => {
|
||||
switch (config.method) {
|
||||
case 'post':
|
||||
if (this.useUpdateInterceptor()) {
|
||||
config.data = this.beforeUpdate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
break
|
||||
case 'put':
|
||||
if (this.useCreateInterceptor()) {
|
||||
|
@ -87,10 +89,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
}
|
||||
break
|
||||
case 'delete':
|
||||
if (this.useDeleteInterceptor()) {
|
||||
config.data = this.beforeDelete(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
break
|
||||
}
|
||||
return config
|
||||
|
@ -106,20 +105,6 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the update interceptor which processes a request payload into json
|
||||
*/
|
||||
useUpdateInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the delete interceptor which processes a request payload into json
|
||||
*/
|
||||
useDeleteInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
///////////////
|
||||
|
@ -127,10 +112,9 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
/**
|
||||
* Returns an object with all route parameters and their values.
|
||||
*/
|
||||
getRouteReplacements(route : string, parameters : Record<string, unknown> = {}) {
|
||||
private getRouteReplacements(route: string, parameters: {[key: string]: unknown} = {}) {
|
||||
const replace$$1: Record<string, unknown> = {}
|
||||
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]]
|
||||
|
@ -139,17 +123,10 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
return replace$$1
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the replacement pattern for url paths, can be overwritten by implementations.
|
||||
*/
|
||||
getRouteParameterPattern(): RegExp {
|
||||
return /{([^}]+)}/
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
|
||||
*/
|
||||
getReplacedRoute(path : string, pathparams : Record<string, unknown>) : string {
|
||||
getReplacedRoute<M extends {[key: string]: unknown}>(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),
|
||||
|
@ -158,8 +135,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
}
|
||||
|
||||
/**
|
||||
* setLoading is a method which sets the loading variable to true, after a timeout of 100ms.
|
||||
* It has the timeout to prevent the loading indicator from showing for only a blink of an eye in the
|
||||
* Sets the loading variable to true, after a timeout of 100ms.
|
||||
* The timeout prevents the loading indicator from showing for only a blink of an eye in the
|
||||
* case the api returns a response in < 100ms.
|
||||
* But because the timeout is created using setTimeout, it will still trigger even if the request is
|
||||
* already finished, so we return a method to call in that case.
|
||||
|
@ -185,36 +162,36 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* The modelFactory returns a model from an object.
|
||||
* This one here is the default one, usually the service definitions for a model will override this.
|
||||
*/
|
||||
modelFactory(data : Partial<Model>) {
|
||||
return data as Model
|
||||
modelFactory<DefaultModel extends {[key: string]: unknown} = Partial<Model>>(data: DefaultModel = {}) {
|
||||
return {...data} as DefaultModel
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get requests.
|
||||
*/
|
||||
modelGetFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelGetFactory<GetModel extends {[key: string]: unknown} = Partial<Model>>(data: GetModel = {}) {
|
||||
return this.modelFactory<GetModel>(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get all requests.
|
||||
*/
|
||||
modelGetAllFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelGetAllFactory<GetAllModel extends {[key: string]: unknown} = Partial<Model>>(data: GetAllModel = {}) {
|
||||
return this.modelFactory<GetAllModel>(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for create requests.
|
||||
*/
|
||||
modelCreateFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelCreateFactory<CreateModel extends {[key: string]: unknown} = Partial<Model>>(data: CreateModel = {}) {
|
||||
return this.modelFactory<CreateModel>(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for update requests.
|
||||
*/
|
||||
modelUpdateFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelUpdateFactory<UpdateModel extends {[key: string]: unknown} = Partial<Model>>(data: UpdateModel = {}) {
|
||||
return this.modelFactory<UpdateModel>(data)
|
||||
}
|
||||
|
||||
//////////////
|
||||
|
@ -224,28 +201,21 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
/**
|
||||
* Default preprocessor for get requests
|
||||
*/
|
||||
beforeGet(model : Model) {
|
||||
beforeGet(model: Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for create requests
|
||||
*/
|
||||
beforeCreate(model : Model) {
|
||||
beforeCreate(model: Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for update requests
|
||||
*/
|
||||
beforeUpdate(model : Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for delete requests
|
||||
*/
|
||||
beforeDelete(model : Model) {
|
||||
beforeUpdate(model: Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
|
@ -258,7 +228,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* @param model The model to use. The request path is built using the values from the model.
|
||||
* @param params Optional query parameters
|
||||
*/
|
||||
get(model : Model, params = {}) {
|
||||
get(model: Model, params = {}) {
|
||||
if (this.paths.get === '') {
|
||||
throw new Error('This model is not able to get data.')
|
||||
}
|
||||
|
@ -270,14 +240,14 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* 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 : Model = new AbstractModel({}), params: Record<string, unknown> = {}) {
|
||||
async getM(url : string, model: Model = this.modelGetFactory(), params: Record<string, unknown> = {}) {
|
||||
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<Model>(finalUrl, {params: prepareParams(params)})
|
||||
const result = this.modelGetFactory(response.data)
|
||||
result.maxRight = Number(response.headers['x-max-right']) as Right
|
||||
return result
|
||||
|
@ -293,7 +263,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
responseType: 'blob',
|
||||
data,
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
return URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -303,7 +273,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* @param params Optional query parameters
|
||||
* @param page The page to get
|
||||
*/
|
||||
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
|
||||
async getAll(model : Model = this.modelFactory(), params = {}, page = 1) {
|
||||
if (this.paths.getAll === '') {
|
||||
throw new Error('This model is not able to get data.')
|
||||
}
|
||||
|
@ -315,7 +285,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
|
||||
const response = await this.http.get<Model>(finalUrl, {params: prepareParams(params)})
|
||||
this.resultCount = Number(response.headers['x-pagination-result-count'])
|
||||
this.totalPages = Number(response.headers['x-pagination-total-pages'])
|
||||
|
||||
|
@ -326,7 +296,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
if (Array.isArray(response.data)) {
|
||||
return response.data.map(entry => this.modelGetAllFactory(entry))
|
||||
}
|
||||
return this.modelGetAllFactory(response.data)
|
||||
return [this.modelGetAllFactory(response.data)]
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
this seemed wrong. when wouldn't you receive an array here? this seemed wrong. when wouldn't you receive an array here?
konrad
commented
I think there really is no case where no array is returned. I think there really is no case where no array is returned.
dpschen
commented
Maybe it would be better to throw instead. Because this fucks up the return types. That's why I added the brackets. Maybe it would be better to throw instead. Because this fucks up the return types. That's why I added the brackets.
dpschen
commented
Fixed Fixed
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
@ -334,9 +304,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
|
||||
/**
|
||||
* Performs a put request to the url specified before
|
||||
* @returns {Promise<any | never>}
|
||||
*/
|
||||
async create(model : Model) {
|
||||
async create<M = Model>(model: M) {
|
||||
if (this.paths.create === '') {
|
||||
throw new Error('This model is not able to create data.')
|
||||
}
|
||||
|
@ -345,7 +314,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
const finalUrl = this.getReplacedRoute(this.paths.create, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.put(finalUrl, model)
|
||||
const response = await this.http.put<M>(finalUrl, model)
|
||||
const result = this.modelCreateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
result.maxRight = model.maxRight
|
||||
|
@ -360,11 +329,11 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* An abstract implementation to send post requests.
|
||||
* Services can use this to implement functions to do post requests other than using the update method.
|
||||
*/
|
||||
async post(url : string, model : Model) {
|
||||
async post<PostModel extends {[key: string]: unknown} = Model>(url : string, model: PostModel) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.post(url, model)
|
||||
const response = await this.http.post<PostModel>(url, model)
|
||||
const result = this.modelUpdateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
result.maxRight = model.maxRight
|
||||
|
@ -378,7 +347,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
/**
|
||||
* Performs a post request to the update url
|
||||
*/
|
||||
update(model : Model) {
|
||||
update<M = Model>(model: M) {
|
||||
if (this.paths.update === '') {
|
||||
throw new Error('This model is not able to update data.')
|
||||
}
|
||||
|
|
|
@ -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<IBackgroundImage> {
|
||||
constructor() {
|
||||
|
@ -11,7 +13,7 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
|
|||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IBackgroundImage>) {
|
||||
modelFactory(data) {
|
||||
return new BackgroundImageModel(data)
|
||||
}
|
||||
|
||||
|
@ -25,6 +27,6 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
|
|||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
return URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
}
|
|
@ -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<IList> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}/backgrounds/upload',
|
||||
|
@ -22,7 +21,7 @@ export default class BackgroundUploadService extends AbstractService {
|
|||
/**
|
||||
* Uploads a file to the server
|
||||
*/
|
||||
create(listId: IList['id'], file: IFile) {
|
||||
create(listId: IList['id'], file: File) {
|
||||
return this.uploadFile(
|
||||
this.getReplacedRoute(this.paths.create, {listId}),
|
||||
file,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import AbstractService from './abstractService'
|
||||
import ListModel from '@/models/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import TaskService from './task'
|
||||
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
|
||||
export default class ListService extends AbstractService<IList> {
|
||||
|
@ -34,29 +37,35 @@ export default class ListService extends AbstractService<IList> {
|
|||
return model
|
||||
}
|
||||
|
||||
beforeCreate(list) {
|
||||
beforeCreate(list: IList) {
|
||||
list.hexColor = colorFromHex(list.hexColor)
|
||||
return list
|
||||
}
|
||||
|
||||
async background(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||
if (list.backgroundInformation === null) {
|
||||
return ''
|
||||
async loadBackground(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||
if (list === null || !list.backgroundInformation) {
|
||||
return
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
unsure: maybe I should remove the setLoading here since the list shouldn't indicate a loading just for the background? unsure: maybe I should remove the setLoading here since the list shouldn't indicate a loading just for the background?
konrad
commented
I think that would make sense. I think that would make sense.
dpschen
commented
Fixed Fixed
|
||||
|
||||
try {
|
||||
const response = await this.http({
|
||||
url: `/lists/${list.id}/background`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
return URL.createObjectURL(new Blob([response.data]))
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
async removeBackground(list: Pick<IList, 'id'>) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.delete(`/lists/${list.id}/background`, list)
|
||||
const response = await this.http.delete<IList>(`/lists/${list.id}/background`)
|
||||
return response.data
|
||||
} finally {
|
||||
cancel()
|
||||
|
|
|
@ -100,7 +100,7 @@ export default class TaskService extends AbstractService<ITask> {
|
|||
})
|
||||
})
|
||||
|
||||
// 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 => {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
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'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
@ -21,8 +18,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
id: 0,
|
||||
isArchived: false,
|
||||
}))
|
||||
const background = ref('')
|
||||
const blurHash = ref('')
|
||||
|
||||
const hasTasks = ref(false)
|
||||
const keyboardShortcutsActive = ref(false)
|
||||
|
@ -62,14 +57,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
quickActionsActive.value = value
|
||||
}
|
||||
|
||||
function setBackground(newBackground: string) {
|
||||
background.value = newBackground
|
||||
}
|
||||
|
||||
function setBlurHash(newBlurHash: string) {
|
||||
blurHash.value = newBlurHash
|
||||
}
|
||||
|
||||
function setLogoVisible(visible: boolean) {
|
||||
logoVisible.value = visible
|
||||
}
|
||||
|
@ -78,43 +65,14 @@ export const useBaseStore = defineStore('base', () => {
|
|||
ready.value = value
|
||||
}
|
||||
|
||||
async function handleSetCurrentList(
|
||||
{list, forceUpdate = false}: {list: IList | null, forceUpdate?: boolean},
|
||||
function handleSetCurrentList(
|
||||
{list}: {list: IList | null},
|
||||
) {
|
||||
if (list === null) {
|
||||
setCurrentList({})
|
||||
setBackground('')
|
||||
setBlurHash('')
|
||||
return
|
||||
}
|
||||
|
||||
// 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 (blurHash) {
|
||||
setBlurHash(window.URL.createObjectURL(blurHash))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof list.backgroundInformation === 'undefined' ||
|
||||
list.backgroundInformation === null
|
||||
) {
|
||||
setBackground('')
|
||||
setBlurHash('')
|
||||
}
|
||||
|
||||
setCurrentList(list)
|
||||
}
|
||||
|
||||
|
@ -129,8 +87,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
loading: readonly(loading),
|
||||
ready: readonly(ready),
|
||||
currentList: readonly(currentList),
|
||||
background: readonly(background),
|
||||
blurHash: readonly(blurHash),
|
||||
hasTasks: readonly(hasTasks),
|
||||
keyboardShortcutsActive: readonly(keyboardShortcutsActive),
|
||||
quickActionsActive: readonly(quickActionsActive),
|
||||
|
@ -142,8 +98,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
setHasTasks,
|
||||
setKeyboardShortcutsActive,
|
||||
setQuickActionsActive,
|
||||
setBackground,
|
||||
setBlurHash,
|
||||
setLogoVisible,
|
||||
|
||||
handleSetCurrentList,
|
||||
|
|
|
@ -1,25 +1,38 @@
|
|||
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 ListService from '@/services/list'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {removeListFromHistory} from '@/modules/listHistory'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import {useNamespaceStore} from './namespaces'
|
||||
import {useRouter} from 'vue-router'
|
||||
import router from '@/router'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
|
||||
|
||||
import type {MaybeRef} from '@vueuse/core'
|
||||
import {LIST_ID} from '@/constants/lists'
|
||||
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||
|
||||
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 {setModuleLoading} from '@/stores/helper'
|
||||
import {useBaseStore} from './base'
|
||||
import {useNamespaceStore} from './namespaces'
|
||||
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import {success} from '@/message'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {i18n} from '@/i18n'
|
||||
import BackgroundUploadService from '@/services/backgroundUpload'
|
||||
import {getBlurHash} from '@/helpers/blurhash'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
||||
|
||||
const FavoriteListsNamespace = -2
|
||||
|
||||
export interface ListState {
|
||||
[id: IList['id']]: IList
|
||||
}
|
||||
|
@ -35,7 +48,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(() => {
|
||||
|
@ -53,7 +68,7 @@ export const useListStore = defineStore('list', () => {
|
|||
?.filter(value => value > 0)
|
||||
.map(id => lists.value[id])
|
||||
.filter(list => list.isArchived === includeArchived)
|
||||
|| []
|
||||
|| [] as IList[]
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -77,15 +92,10 @@ 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
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
if (list.id === LIST_ID.FAVORITES || list.isArchived) {
|
||||
return
|
||||
}
|
||||
return updateList({
|
||||
|
@ -94,6 +104,20 @@ export const useListStore = defineStore('list', () => {
|
|||
})
|
||||
}
|
||||
|
||||
async function loadList(listId: List['id']) {
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
remove 'this' remove 'this'
dpschen
commented
Was already fixed Was already fixed
|
||||
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()
|
||||
|
@ -113,6 +137,8 @@ export const useListStore = defineStore('list', () => {
|
|||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
|
||||
const oldList = cloneDeep(getListById.value(list.id) as IList)
|
||||
|
||||
try {
|
||||
await listService.update(list)
|
||||
setList(list)
|
||||
|
@ -120,24 +146,18 @@ export const useListStore = defineStore('list', () => {
|
|||
|
||||
// the returned list from listService.update is the same!
|
||||
// in order to not create a manipulation in pinia store we have to create a new copy
|
||||
const newList = {
|
||||
...list,
|
||||
namespaceId: FavoriteListsNamespace,
|
||||
}
|
||||
|
||||
namespaceStore.removeListFromNamespaceById(newList)
|
||||
const newList = {...list}
|
||||
if (list.isFavorite) {
|
||||
namespaceStore.addListToNamespace(newList)
|
||||
namespaceStore.addListToNamespace(newList, NAMESPACE_ID.FAVORITES)
|
||||
} else {
|
||||
namespaceStore.removeListFromNamespaceById(newList, NAMESPACE_ID.FAVORITES)
|
||||
}
|
||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
namespaceStore.removeFavoritesNamespaceIfEmpty()
|
||||
return newList
|
||||
} catch (e) {
|
||||
// Reset the list state to the initial one to avoid confusion for the user
|
||||
setList({
|
||||
...list,
|
||||
isFavorite: !list.isFavorite,
|
||||
})
|
||||
setList(oldList)
|
||||
throw e
|
||||
} finally {
|
||||
cancel()
|
||||
|
@ -150,7 +170,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
|
||||
|
@ -159,6 +180,124 @@ export const useListStore = defineStore('list', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function toggleArchiveList(list: IList) {
|
||||
try {
|
||||
const newList = await updateList({
|
||||
...list,
|
||||
isArchived: !list.isArchived,
|
||||
})
|
||||
baseStore.setCurrentList(newList)
|
||||
success({message: newList.isArchived
|
||||
? i18n.global.t('list.archive.successArchived')
|
||||
: i18n.global.t('list.archive.successUnarchived'),
|
||||
})
|
||||
} 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}})
|
||||
}
|
||||
|
||||
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
in case of the modal, where the modal has a view we should replace the view instead. maybe always? in case of the modal, where the modal has a view we should replace the view instead. maybe always?
konrad
commented
Doesn't the view in Doesn't the view in `list.index` do that?
dpschen
commented
Yes it does. But we still use Yes it does. But we still use `router.push()`. What I meant was using `router.replace()` instead, because you wouldn't want to go back to the route of the dialog.
konrad
commented
For routes like the task detail popup we should have the option to go back to the route with a popup. Maybe for settings as well. For routes like the task detail popup we should have the option to go back to the route with a popup. Maybe for settings as well.
dpschen
commented
Yes, maybe this should be configurable via a prop Yes, maybe this should be configurable via a prop
dpschen
commented
Keeping this as it is for now. This is something that should be tackled in the modal or even in the dialog branch later. Keeping this as it is for now. This is something that should be tackled in the modal or even in the dialog branch later.
|
||||
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
|
||||
}
|
||||
dpschen
commented
why not? why not?
konrad
commented
I think this was to prevent multiple clicks quickly after each other. The better solution would of course be disabling the button. I think this was to prevent multiple clicks quickly after each other. The better solution would of course be disabling the button.
dpschen
commented
Okay, will do that instead. Okay, will do that instead.
|
||||
|
||||
const list: IList = await backgroundService.update({
|
||||
id: backgroundId,
|
||||
listId,
|
||||
})
|
||||
dpschen marked this conversation as resolved
Outdated
dpschen
commented
fix: type should be returned by update. fix: type should be returned by update.
dpschen
commented
Fixed Fixed
|
||||
baseStore.handleSetCurrentList({list})
|
||||
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)
|
||||
|
||||
baseStore.handleSetCurrentList({list})
|
||||
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)
|
||||
baseStore.handleSetCurrentList({list})
|
||||
namespaceStore.setListInNamespaceById(list)
|
||||
setList(list)
|
||||
success({message: i18n.global.t('list.background.removeSuccess')})
|
||||
router.back()
|
||||
}
|
||||
|
||||
async function loadListBackground(
|
||||
list: IList,
|
||||
blurhashSetter: (blurhash: string) => void,
|
||||
backgroundSetter: (background: string) => void,
|
||||
) {
|
||||
if (
|
||||
list === null ||
|
||||
!list.backgroundInformation
|
||||
) {
|
||||
blurhashSetter('')
|
||||
backgroundSetter('')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const listService = new ListService()
|
||||
const blurHashPromise = getBlurHash(list.backgroundBlurHash).then(blurHash => {
|
||||
blurhashSetter(blurHash || '')
|
||||
})
|
||||
const backgroundPromise = listService.loadBackground(list).then(background => {
|
||||
if (background === undefined) {
|
||||
throw new Error()
|
||||
}
|
||||
backgroundSetter(background)
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
blurHashPromise,
|
||||
backgroundPromise,
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('Error getting background image for list', list.id, e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: readonly(isLoading),
|
||||
lists: readonly(lists),
|
||||
|
@ -169,43 +308,185 @@ export const useListStore = defineStore('list', () => {
|
|||
|
||||
setList,
|
||||
setLists,
|
||||
removeListById,
|
||||
toggleListFavorite,
|
||||
|
||||
// crud
|
||||
loadList,
|
||||
createList,
|
||||
updateList,
|
||||
deleteList,
|
||||
|
||||
toggleArchiveList,
|
||||
|
||||
//duplciate
|
||||
isDuplicatingList,
|
||||
duplicateList,
|
||||
|
||||
isUploadingBackground,
|
||||
uploadListBackground,
|
||||
removeListBackground,
|
||||
setUnsplashBackground,
|
||||
loadListBackground,
|
||||
}
|
||||
})
|
||||
|
||||
export function useList(listId: MaybeRef<IList['id']>) {
|
||||
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<IList['id']>) {
|
||||
const listStore = useListStore()
|
||||
|
||||
const listService = shallowReactive(new ListService())
|
||||
const {loading: isLoading} = toRefs(listService)
|
||||
const list = ref<IList>(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<IList['id']>) {
|
||||
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<IList>) {
|
||||
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')})
|
||||
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)
|
||||
useBaseStore().handleSetCurrentList({list: list.value})
|
||||
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(list: MaybeRef<IList>) {
|
||||
const listStore = useListStore()
|
||||
|
||||
const listVal = computed(() => unref(list))
|
||||
const backgroundUrl = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
const blurHashUrl = ref('')
|
||||
|
||||
watch(
|
||||
() => [listVal.value.id, listVal.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
|
||||
async ([listId, blurHash], oldValue) => {
|
||||
if (backgroundLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
dpschen
commented
I copied this from the old code, but I think that we shouldn't return in that case I copied this from the old code, but I think that we shouldn't return in that case
|
||||
const [oldListId, oldBlurHash] = oldValue || []
|
||||
if (
|
||||
oldValue !== undefined &&
|
||||
listId === oldListId && blurHash === oldBlurHash
|
||||
) {
|
||||
// list hasn't changed
|
||||
return
|
||||
}
|
||||
|
||||
backgroundLoading.value = true
|
||||
|
||||
await listStore.loadListBackground(
|
||||
listVal.value,
|
||||
(value) => {
|
||||
blurHashUrl.value = value
|
||||
},
|
||||
(value) => {
|
||||
backgroundUrl.value = value
|
||||
dpschen
commented
if the list or background has changed in between we shouldn't set this. if the list or background has changed in between we shouldn't set this.
konrad
commented
When could that be the case? When could that be the case?
dpschen
commented
If you click on one list that has a background and then on another that has none. The promise would still finish and then set the (wrong) background. If you click on one list that has a background and then on another that has none. The promise would still finish and then set the (wrong) background.
konrad
commented
Makes sense! Makes sense!
|
||||
},
|
||||
)
|
||||
|
||||
backgroundLoading.value = false
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
backgroundUrl,
|
||||
blurHashUrl,
|
||||
backgroundLoading,
|
||||
}
|
||||
}
|
|
@ -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', () => {
|
||||
|
@ -123,20 +127,20 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
|||
}
|
||||
}
|
||||
|
||||
function addListToNamespace(list: IList) {
|
||||
function addListToNamespace(list: IList, namespaceId?: INamespace['id']) {
|
||||
for (const n in namespaces.value) {
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
if (namespaces.value[n].id === (namespaceId ?? list.namespaceId)) {
|
||||
namespaces.value[n].lists.push(list)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeListFromNamespaceById(list: IList) {
|
||||
function removeListFromNamespaceById(list: IList, namespaceId?: INamespace['id']) {
|
||||
for (const n in namespaces.value) {
|
||||
// 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.
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
if (namespaces.value[n].id === (namespaceId ?? list.namespaceId)) {
|
||||
for (const l in namespaces.value[n].lists) {
|
||||
if (namespaces.value[n].lists[l].id === list.id) {
|
||||
namespaces.value[n].lists.splice(l, 1)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<create-edit :title="$t('list.create.header')" @create="createNewList()" :primary-disabled="list.title === ''">
|
||||
<create-edit
|
||||
:title="$t('list.create.header')"
|
||||
@create="createNewList()"
|
||||
:primary-disabled="list.title === ''"
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
|
||||
<div
|
||||
:class="{ 'is-loading': listService.loading }"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
class="control"
|
||||
>
|
||||
<input
|
||||
:class="{ disabled: listService.loading }"
|
||||
:class="{ disabled: isLoading }"
|
||||
@keyup.enter="createNewList()"
|
||||
@keyup.esc="$router.back()"
|
||||
class="input"
|
||||
|
@ -19,7 +23,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="showError && list.title === ''">
|
||||
<p class="help is-danger" v-if="errors.createList && list.title === ''">
|
||||
{{ $t('list.create.addTitleRequired') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
|
@ -32,43 +36,29 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, shallowReactive} from 'vue'
|
||||
import {watchEffect} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import ListModel from '@/models/list'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useList} from '@/stores/lists'
|
||||
|
||||
const {namespaceId} = defineProps<{
|
||||
namespaceId: INamespace['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
useTitle(() => t('list.create.header'))
|
||||
|
||||
const showError = ref(false)
|
||||
const list = reactive(new ListModel())
|
||||
const listService = shallowReactive(new ListService())
|
||||
const listStore = useListStore()
|
||||
const {isLoading, errors, list, createList} = useList()
|
||||
|
||||
async function createNewList() {
|
||||
if (list.title === '') {
|
||||
showError.value = true
|
||||
return
|
||||
}
|
||||
showError.value = false
|
||||
watchEffect(() => {
|
||||
list.value.namespaceId = namespaceId
|
||||
})
|
||||
|
||||
list.namespaceId = Number(route.params.namespaceId as string)
|
||||
const newList = await listStore.createList(list)
|
||||
await router.push({
|
||||
name: 'list.index',
|
||||
params: { listId: newList.id },
|
||||
})
|
||||
success({message: t('list.create.createdSuccess') })
|
||||
}
|
||||
const createNewList = () => createList()
|
||||
</script>
|
|
@ -1,12 +1,22 @@
|
|||
<template>
|
||||
<modal
|
||||
@close="$router.back()"
|
||||
@submit="archiveList()"
|
||||
@submit="listStore.toggleArchiveList(list)"
|
||||
>
|
||||
<template #header><span>{{ list.isArchived ? $t('list.archive.unarchive') : $t('list.archive.archive') }}</span></template>
|
||||
<template #header>
|
||||
<span>{{
|
||||
list.isArchived
|
||||
? $t('list.archive.unarchive')
|
||||
: $t('list.archive.archive')
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ list.isArchived ? $t('list.archive.unarchiveText') : $t('list.archive.archiveText') }}</p>
|
||||
<p>{{
|
||||
list.isArchived
|
||||
? $t('list.archive.unarchiveText')
|
||||
: $t('list.archive.archiveText')
|
||||
}}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -16,34 +26,21 @@ export default {name: 'list-setting-archive'}
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useList, useListStore} from '@/stores/lists'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const listStore = useListStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const {list} = useList(listId)
|
||||
|
||||
const list = computed(() => listStore.getListById(route.params.listId))
|
||||
useTitle(() => t('list.archive.title', {list: list.value.title}))
|
||||
|
||||
async function archiveList() {
|
||||
try {
|
||||
const newList = await listStore.updateList({
|
||||
...list.value,
|
||||
isArchived: !list.value.isArchived,
|
||||
})
|
||||
useBaseStore().setCurrentList(newList)
|
||||
success({message: t('list.archive.success')})
|
||||
} finally {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
>
|
||||
<div class="mb-4" v-if="uploadBackgroundEnabled">
|
||||
<input
|
||||
@change="uploadBackground"
|
||||
@change="listStore.uploadListBackground(listId, ($event.target as HTMLInputElement)?.files?.[0])"
|
||||
accept="image/*"
|
||||
class="is-hidden"
|
||||
ref="backgroundUploadInput"
|
||||
type="file"
|
||||
/>
|
||||
<x-button
|
||||
:loading="backgroundUploadService.loading"
|
||||
:loading="listStore.isUploadingBackground"
|
||||
@click="backgroundUploadInput?.click()"
|
||||
variant="primary"
|
||||
>
|
||||
|
@ -47,7 +47,7 @@
|
|||
<BaseButton
|
||||
v-if="backgroundThumbs[im.id]"
|
||||
class="image-search__image-button"
|
||||
@click="setBackground(im.id)"
|
||||
@click="listStore.setUnsplashBackground(listId, im.id)"
|
||||
>
|
||||
<img class="image-search__image" :src="backgroundThumbs[im.id]" alt="" />
|
||||
</BaseButton>
|
||||
|
@ -79,7 +79,7 @@
|
|||
:shadow="false"
|
||||
variant="tertiary"
|
||||
class="is-danger"
|
||||
@click.prevent.stop="removeBackground"
|
||||
@click.prevent.stop="listStore.removeListBackground(listId)"
|
||||
>
|
||||
{{ $t('list.background.remove') }}
|
||||
</x-button>
|
||||
|
@ -100,42 +100,40 @@ export default { name: 'list-setting-background' }
|
|||
<script setup lang="ts">
|
||||
import {ref, computed, shallowReactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
|
||||
import BackgroundUploadService from '@/services/backgroundUpload'
|
||||
import ListService from '@/services/list'
|
||||
import type BackgroundImageModel from '@/models/backgroundImage'
|
||||
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import {success} from '@/message'
|
||||
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {getBlurHash} from '@/helpers/blurhash'
|
||||
|
||||
const SEARCH_DEBOUNCE = 300
|
||||
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const baseStore = useBaseStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
useTitle(() => t('list.background.title'))
|
||||
|
||||
const backgroundService = shallowReactive(new BackgroundUnsplashService())
|
||||
const backgroundSearchTerm = ref('')
|
||||
const backgroundSearchResult = ref([])
|
||||
const backgroundSearchResult = ref<IBackgroundImage[]>([])
|
||||
const backgroundThumbs = ref<Record<string, string>>({})
|
||||
const backgroundBlurHashes = ref<Record<string, string>>({})
|
||||
const backgroundBlurHashes = ref<Record<string, string | undefined>>({})
|
||||
const currentPage = ref(1)
|
||||
|
||||
// We're using debounce to not search on every keypress but with a delay.
|
||||
|
@ -143,25 +141,23 @@ const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNC
|
|||
trailing: true,
|
||||
})
|
||||
|
||||
const backgroundUploadService = ref(new BackgroundUploadService())
|
||||
const listService = ref(new ListService())
|
||||
const listStore = useListStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
|
||||
const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload'))
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const hasBackground = computed(() => baseStore.background !== null)
|
||||
const hasBackground = computed(() => baseStore.currentList?.backgroundInformation !== undefined)
|
||||
|
||||
// Show the default collection of backgrounds
|
||||
newBackgroundSearch()
|
||||
|
||||
/**
|
||||
* Reset a few things before searching to not break loading more photos.
|
||||
*/
|
||||
function newBackgroundSearch() {
|
||||
if (!unsplashBackgroundEnabled.value) {
|
||||
return
|
||||
}
|
||||
// This is an extra method to reset a few things when searching to not break loading more photos.
|
||||
backgroundSearchResult.value = []
|
||||
backgroundThumbs.value = {}
|
||||
searchBackgrounds()
|
||||
|
@ -171,10 +167,9 @@ async function searchBackgrounds(page = 1) {
|
|||
currentPage.value = page
|
||||
const result = await backgroundService.getAll({}, {s: backgroundSearchTerm.value, p: page})
|
||||
backgroundSearchResult.value = backgroundSearchResult.value.concat(result)
|
||||
result.forEach((background: BackgroundImageModel) => {
|
||||
getBlobFromBlurHash(background.blurHash)
|
||||
.then((b) => {
|
||||
backgroundBlurHashes.value[background.id] = window.URL.createObjectURL(b)
|
||||
result.forEach((background: IBackgroundImage) => {
|
||||
getBlurHash(background.blurHash).then((b) => {
|
||||
backgroundBlurHashes.value[background.id] = b
|
||||
})
|
||||
|
||||
backgroundService.thumb(background).then(b => {
|
||||
|
@ -183,47 +178,7 @@ async function searchBackgrounds(page = 1) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
async function setBackground(backgroundId: string) {
|
||||
// Don't set a background if we're in the process of setting one
|
||||
if (backgroundService.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const list = await backgroundService.update({
|
||||
id: backgroundId,
|
||||
listId: route.params.listId,
|
||||
})
|
||||
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||
namespaceStore.setListInNamespaceById(list)
|
||||
listStore.setList(list)
|
||||
success({message: t('list.background.success')})
|
||||
}
|
||||
|
||||
const backgroundUploadInput = ref<HTMLInputElement | null>(null)
|
||||
async function uploadBackground() {
|
||||
if (backgroundUploadInput.value?.files?.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const list = await backgroundUploadService.value.create(
|
||||
route.params.listId,
|
||||
backgroundUploadInput.value?.files[0],
|
||||
)
|
||||
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||
namespaceStore.setListInNamespaceById(list)
|
||||
listStore.setList(list)
|
||||
success({message: t('list.background.success')})
|
||||
}
|
||||
|
||||
async function removeBackground() {
|
||||
const list = await listService.value.removeBackground(currentList.value)
|
||||
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||
namespaceStore.setListInNamespaceById(list)
|
||||
listStore.setList(list)
|
||||
success({message: t('list.background.removeSuccess')})
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -27,46 +27,37 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watchEffect} from 'vue'
|
||||
import {ref, watch} from 'vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {success} from '@/message'
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
||||
import {useList} from '@/stores/lists'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const props = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const listStore = useListStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const totalTasks = ref<number | null>(null)
|
||||
|
||||
const list = computed(() => listStore.getListById(route.params.listId))
|
||||
|
||||
watchEffect(
|
||||
() => {
|
||||
if (!route.params.listId) {
|
||||
watch(
|
||||
() => props.listId,
|
||||
async (currentListId) => {
|
||||
if (!currentListId) {
|
||||
return
|
||||
}
|
||||
|
||||
const taskCollectionService = new TaskCollectionService()
|
||||
taskCollectionService.getAll({listId: route.params.listId}).then(() => {
|
||||
await taskCollectionService.getAll({listId: currentListId})
|
||||
totalTasks.value = taskCollectionService.totalPages * taskCollectionService.resultCount
|
||||
})
|
||||
},
|
||||
{immediate: true},
|
||||
|
||||
)
|
||||
|
||||
const {list, deleteList} = useList(listId)
|
||||
useTitle(() => t('list.delete.title', {list: list?.value?.title}))
|
||||
|
||||
async function deleteList() {
|
||||
if (!list.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await listStore.deleteList(list.value)
|
||||
success({message: t('list.delete.success')})
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
:title="$t('list.duplicate.title')"
|
||||
primary-icon="paste"
|
||||
:primary-label="$t('list.duplicate.label')"
|
||||
@primary="duplicateList"
|
||||
:loading="listDuplicateService.loading"
|
||||
:primary-disabled="selectedNamespace === undefined"
|
||||
@primary="duplicateList(listId, selectedNamespace?.id)"
|
||||
:loading="isDuplicatingList"
|
||||
>
|
||||
<p>{{ $t('list.duplicate.text') }}</p>
|
||||
|
||||
|
@ -12,7 +13,7 @@
|
|||
:placeholder="$t('namespace.search')"
|
||||
@search="findNamespaces"
|
||||
:search-results="namespaces"
|
||||
@select="selectNamespace"
|
||||
@select="(namespace) => selectNamespace(namespace as INamespace)"
|
||||
label="title"
|
||||
:search-delay="10"
|
||||
/>
|
||||
|
@ -20,22 +21,23 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {ref, toRefs} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import ListDuplicateService from '@/services/listDuplicateService'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
import ListDuplicateModel from '@/models/listDuplicateModel'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useNamespaceSearch} from '@/composables/useNamespaceSearch'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const props = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
const {listId} = toRefs(props)
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => t('list.duplicate.title'))
|
||||
|
@ -46,30 +48,12 @@ const {
|
|||
} = useNamespaceSearch()
|
||||
|
||||
const selectedNamespace = ref<INamespace>()
|
||||
|
||||
function selectNamespace(namespace: INamespace) {
|
||||
selectedNamespace.value = namespace
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const listStore = useListStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const listDuplicateService = shallowReactive(new ListDuplicateService())
|
||||
|
||||
async function duplicateList() {
|
||||
const listDuplicate = new ListDuplicateModel({
|
||||
// FIXME: should be parameter
|
||||
listId: route.params.listId,
|
||||
namespaceId: selectedNamespace.value?.id,
|
||||
})
|
||||
|
||||
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}})
|
||||
}
|
||||
const {
|
||||
duplicateList,
|
||||
isDuplicatingList,
|
||||
} = useListStore()
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:title="$t('list.edit.header')"
|
||||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
@primary="saveList"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
||||
>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<input
|
||||
:class="{ 'disabled': isLoading}"
|
||||
:disabled="isLoading || undefined"
|
||||
@keyup.enter="save"
|
||||
@keyup.enter="saveList"
|
||||
class="input"
|
||||
id="title"
|
||||
:placeholder="$t('list.edit.titlePlaceholder')"
|
||||
|
@ -33,7 +33,7 @@
|
|||
<input
|
||||
:class="{ 'disabled': isLoading}"
|
||||
:disabled="isLoading || undefined"
|
||||
@keyup.enter="save"
|
||||
@keyup.enter="saveList"
|
||||
class="input"
|
||||
id="identifier"
|
||||
:placeholder="$t('list.edit.identifierPlaceholder')"
|
||||
|
@ -70,8 +70,6 @@ export default { name: 'list-setting-edit' }
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
|
@ -80,29 +78,16 @@ import CreateEdit from '@/components/misc/create-edit.vue'
|
|||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useList} from '@/stores/lists'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
type: Number as PropType<IList['id']>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const {list, save: saveList, isLoading} = useList(props.listId)
|
||||
const {list, saveList, isLoading} = useList(listId)
|
||||
|
||||
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
||||
|
||||
async function save() {
|
||||
await saveList()
|
||||
await useBaseStore().handleSetCurrentList({list})
|
||||
router.back()
|
||||
}
|
||||
useTitle(() => list.value.title ? t('list.edit.title', {list: list.value.title}) : '')
|
||||
</script>
|
||||
|
|
|
@ -18,7 +18,11 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<link-sharing :list-id="listId" v-if="linkSharingEnabled" class="mt-4"/>
|
||||
<link-sharing
|
||||
v-if="linkSharingEnabled"
|
||||
class="mt-4"
|
||||
:list-id="listId"
|
||||
/>
|
||||
</create-edit>
|
||||
</template>
|
||||
|
||||
|
@ -27,26 +31,28 @@ export default {name: 'list-setting-share'}
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed, watchEffect} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useTitle} from '@vueuse/core'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import ListModel from '@/models/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import LinkSharing from '@/components/sharing/linkSharing.vue'
|
||||
import userTeam from '@/components/sharing/userTeam.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import {useList} from '@/stores/lists'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const list = ref<IList>()
|
||||
const {list} = useList(listId)
|
||||
|
||||
const title = computed(() => list.value?.title
|
||||
? t('list.share.title', {list: list.value.title})
|
||||
: '',
|
||||
|
@ -54,21 +60,8 @@ const title = computed(() => list.value?.title
|
|||
useTitle(title)
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const linkSharingEnabled = computed(() => configStore.linkSharingEnabled)
|
||||
const userIsAdmin = computed(() => list?.value?.maxRight === RIGHTS.ADMIN)
|
||||
|
||||
async function loadList(listId: number) {
|
||||
const listService = new ListService()
|
||||
const newList = await listService.get(new ListModel({id: listId}))
|
||||
await useBaseStore().handleSetCurrentList({list: newList})
|
||||
list.value = newList
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const listId = computed(() => route.params.listId !== undefined
|
||||
? parseInt(route.params.listId as string)
|
||||
: undefined,
|
||||
)
|
||||
watchEffect(() => listId.value !== undefined && loadList(listId.value))
|
||||
const userIsAdmin = computed(() => list.value.owner.id === authStore.info?.id)
|
||||
</script>
|
||||
|
|
this entry is beeing overwritten below