Compare commits

..

1 Commits

Author SHA1 Message Date
89a9f5a779 fix(deps): update dependency flexsearch to v0.7.31 2023-02-15 22:03:43 +00:00
41 changed files with 606 additions and 744 deletions

2
.nvmrc
View File

@ -1 +1 @@
18.14.2
v18

View File

@ -18,12 +18,6 @@
"javascriptreact",
"vue"
],
"volar.completion.preferredTagNameCase": "pascal",
// disable vetur in case it is installed
"vetur.validation.template": false,
// i18n ally
"i18n-ally.localesPaths": [
"src/i18n/lang"

View File

@ -28,20 +28,6 @@ server {
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
try_files $uri /index.html =404;
}
# Disable caching for sw
location = /sw.js {
autoindex off;
expires off;
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
}
# Disable caching for webmanifest
location = /manifest.webmanifest {
autoindex off;
expires off;
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
}
# favicon.ico
location = /favicon.ico {

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@7.28.0",
"packageManager": "pnpm@7.27.0",
"keywords": [
"todo",
"productivity",
@ -53,24 +53,24 @@
"@infectoone/vue-ganttastic": "2.1.4",
"@intlify/unplugin-vue-i18n": "0.8.2",
"@kyvg/vue3-notification": "2.9.0",
"@sentry/tracing": "7.40.0",
"@sentry/vue": "7.40.0",
"@sentry/tracing": "7.37.2",
"@sentry/vue": "7.37.2",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
"@vueuse/core": "9.13.0",
"axios": "1.3.4",
"blurhash": "2.0.5",
"@vueuse/core": "9.12.0",
"axios": "1.3.3",
"blurhash": "2.0.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.12",
"codemirror": "5.65.11",
"date-fns": "2.29.3",
"dayjs": "1.11.7",
"dompurify": "3.0.1",
"dompurify": "3.0.0",
"easymde": "2.18.0",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.21",
"flexsearch": "0.7.31",
"floating-vue": "2.0.0-beta.20",
"focus-within": "3.0.2",
"highlight.js": "11.7.0",
@ -78,11 +78,11 @@
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
"marked": "4.2.12",
"pinia": "2.0.32",
"pinia": "2.0.30",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "1.1.1",
"ufo": "1.0.1",
"vue": "3.2.47",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.2",
@ -93,11 +93,11 @@
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.3",
"@cypress/vite-dev-server": "5.0.4",
"@cypress/vite-dev-server": "5.0.2",
"@cypress/vue": "5.0.4",
"@faker-js/faker": "7.6.0",
"@histoire/plugin-screenshot": "0.15.8",
"@histoire/plugin-vue": "0.15.8",
"@histoire/plugin-screenshot": "0.15.3",
"@histoire/plugin-vue": "0.15.3",
"@rushstack/eslint-patch": "1.2.0",
"@types/codemirror": "5.60.7",
"@types/dompurify": "2.4.0",
@ -105,41 +105,41 @@
"@types/focus-within": "1.0.1",
"@types/lodash.debounce": "4.0.7",
"@types/marked": "4.0.8",
"@types/node": "18.14.6",
"@types/node": "18.13.0",
"@types/postcss-preset-env": "7.7.0",
"@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.54.0",
"@typescript-eslint/eslint-plugin": "5.52.0",
"@typescript-eslint/parser": "5.52.0",
"@vitejs/plugin-legacy": "4.0.1",
"@vitejs/plugin-vue": "4.0.0",
"@vue/eslint-config-typescript": "11.0.2",
"@vue/test-utils": "2.3.0",
"@vue/test-utils": "2.2.10",
"@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.13",
"browserslist": "4.21.5",
"caniuse-lite": "1.0.30001458",
"caniuse-lite": "1.0.30001451",
"csstype": "3.1.1",
"cypress": "12.7.0",
"esbuild": "0.17.11",
"eslint": "8.35.0",
"cypress": "12.5.1",
"esbuild": "0.17.8",
"eslint": "8.34.0",
"eslint-plugin-vue": "9.9.0",
"happy-dom": "8.9.0",
"histoire": "0.15.8",
"netlify-cli": "13.0.0",
"happy-dom": "8.2.6",
"histoire": "0.15.3",
"netlify-cli": "12.12.0",
"postcss": "8.4.21",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "3.0.1",
"postcss-preset-env": "8.0.1",
"rollup": "3.18.0",
"rollup": "3.15.0",
"rollup-plugin-visualizer": "5.9.0",
"sass": "1.58.3",
"start-server-and-test": "2.0.0",
"sass": "1.58.1",
"start-server-and-test": "1.15.4",
"typescript": "4.9.5",
"vite": "4.1.4",
"vite": "4.1.1",
"vite-plugin-inject-preload": "1.3.0",
"vite-plugin-pwa": "0.14.4",
"vite-svg-loader": "4.0.0",
"vitest": "0.29.2",
"vue-tsc": "1.2.0",
"vitest": "0.28.5",
"vue-tsc": "1.1.0",
"wait-on": "7.0.1",
"workbox-cli": "6.5.4"
}

File diff suppressed because it is too large Load Diff

View File

@ -122,7 +122,6 @@
<span class="list-menu-title">{{ getListTitle(l) }}</span>
</BaseButton>
<BaseButton
v-if="l.id > 0"
class="favorite"
:class="{'is-favorite': l.isFavorite}"
@click="listStore.toggleListFavorite(l)"
@ -221,7 +220,7 @@ function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
// This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
@ -246,8 +245,8 @@ async function saveListPosition(e: SortableEvent) {
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
// To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
const list = listsActive[newIndex]
const listBefore = listsActive[newIndex - 1] ?? null
const listAfter = listsActive[newIndex + 1] ?? null
@ -342,20 +341,13 @@ $vikunja-nav-selected-width: 0.4rem;
}
.menu-list-dropdown {
opacity: 1;
opacity: 0;
transition: $transition;
}
@media(hover: hover) and (pointer: fine) {
.menu-list-dropdown {
opacity: 0;
}
&:hover .menu-list-dropdown {
opacity: 1;
}
&:hover .menu-list-dropdown {
opacity: 1;
}
}
.menu-item-icon {
@ -419,21 +411,18 @@ $vikunja-nav-selected-width: 0.4rem;
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
&:hover .handle {
opacity: 1;
}
}
&:not(.dragging-disabled) .handle {
cursor: grab;
}
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
font-weight: 600;
@ -488,24 +477,17 @@ $vikunja-nav-selected-width: 0.4rem;
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 1;
opacity: 0;
&:hover,
&.is-favorite {
color: var(--warning);
opacity: 1;
}
}
@media(hover: hover) and (pointer: fine) {
.list-menu .favorite {
opacity: 0;
}
.list-menu:hover .favorite,
.favorite.is-favorite {
opacity: 1;
}
.favorite.is-favorite,
.list-menu:hover .favorite {
opacity: 1;
}
.list-menu-title {

View File

@ -7,7 +7,7 @@
@change="(event: Event) => updateData((event.target as HTMLInputElement).checked)"
type="checkbox"
/>
<label :for="checkBoxId" class="check" @click.prevent="check">
<label :for="checkBoxId" class="check">
<svg height="18px" viewBox="0 0 18 18" width="18px">
<path
d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
@ -56,11 +56,6 @@ function updateData(newChecked: boolean) {
emit('update:modelValue', newChecked)
emit('change', newChecked)
}
function check() {
checked.value = !checked.value
updateData(checked.value)
}
</script>

View File

@ -147,7 +147,7 @@ const listStore = useListStore()
top: var(--list-card-padding);
right: var(--list-card-padding);
transition: opacity $transition, color $transition;
opacity: 1;
opacity: 0;
&:hover {
color: var(--warning);
@ -160,14 +160,8 @@ const listStore = useListStore()
}
}
@media(hover: hover) and (pointer: fine) {
.list-card .favorite {
opacity: 0;
}
.list-card:hover .favorite {
opacity: 1;
}
.list-card:hover .favorite {
opacity: 1;
}
.background-fade-in {
@ -179,4 +173,4 @@ const listStore = useListStore()
opacity: 1;
}
}
</style>
</style>

View File

@ -428,7 +428,7 @@ function searchTeams() {
teamService.getAll({}, { s: t }),
)
const teamsResult = await Promise.all(teamSearchPromises)
foundTeams.value = teamsResult.flat().map((team) => {
foundTeams.value = teamsResult.flatMap((team) => {
team.title = team.name
return team
})
@ -458,13 +458,6 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
params: { id: (item as DoAction<ITask>).id },
})
break
case ACTION_TYPE.TEAM:
closeQuickActions()
await router.push({
name: 'teams.edit',
params: { id: (item as DoAction<ITeam>).id },
})
break
case ACTION_TYPE.CMD:
query.value = ''
selectedCmd.value = item as DoAction<Command>

View File

@ -1,22 +1,19 @@
<template>
<router-link
:to="taskDetailRoute"
:class="{'is-loading': taskService.loading}"
class="task loader-container"
>
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@change="markAsDone"
v-model="task.done"
/>
<ColorBubble
v-if="showListColor && listColor !== '' && currentList.id !== task.listId"
:color="listColor"
class="mr-1"
/>
<div
<router-link
:to="taskDetailRoute"
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}"
class="tasktext"
>
@ -96,7 +93,7 @@
</span>
<checklist-summary :task="task"/>
</div>
</router-link>
<progress
class="progress is-small"
@ -117,14 +114,14 @@
<BaseButton
:class="{'is-favorite': task.isFavorite}"
@click.prevent="toggleFavorite"
@click="toggleFavorite"
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot />
</router-link>
</div>
</template>
<script setup lang="ts">
@ -288,11 +285,7 @@ function hideDeferDueDatePopup(e) {
border-radius: $radius;
border: 2px solid transparent;
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
background-color: var(--grey-100);
}
@ -338,8 +331,17 @@ function hideDeferDueDatePopup(e) {
}
a {
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
}
}
.favorite {
opacity: 1;
opacity: 0;
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
@ -354,26 +356,21 @@ function hideDeferDueDatePopup(e) {
}
}
.handle {
&:hover .favorite {
opacity: 1;
}
.handle {
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
@media(hover: hover) and (pointer: fine) {
& .favorite,
& .handle {
opacity: 0;
}
&:hover .favorite,
&:hover .handle {
opacity: 1;
}
&:hover .handle {
opacity: 1;
}
:deep(.fancycheckbox) {
height: 18px;
padding-top: 0;
@ -425,4 +422,4 @@ function hideDeferDueDatePopup(e) {
margin-bottom: 0;
}
}
</style>
</style>

View File

@ -5,22 +5,6 @@ import TaskCollectionService from '@/services/taskCollection'
import type {ITask} from '@/modelTypes/ITask'
import {error} from '@/message'
export type Order = 'asc' | 'desc' | 'none'
export interface SortBy {
id?: Order
index?: Order
done?: Order
title?: Order
priority?: Order
due_date?: Order
start_date?: Order
end_date?: Order
percent_done?: Order
created?: Order
updated?: Order
}
// FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({
sort_by: ['position', 'id'],
@ -31,7 +15,7 @@ export const getDefaultParams = () => ({
filter_concat: 'and',
})
const SORT_BY_DEFAULT: SortBy = {
const SORT_BY_DEFAULT = {
id: 'desc',
}
@ -60,7 +44,7 @@ const SORT_BY_DEFAULT: SortBy = {
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function useTaskList(listId, sortByDefault: SortBy = SORT_BY_DEFAULT) {
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
const params = ref({...getDefaultParams()})
const search = ref('')

View File

@ -113,8 +113,8 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
window.API_URL = oldUrl
throw e
})
.then(success => {
if (success) {
.then(r => {
if (typeof r !== 'undefined') {
localStorage.setItem('API_URL', window.API_URL)
return window.API_URL
}

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Nový uložený filtr",
"description": "Uložený filtr je virtuální seznam, který se počítá ze sady filtrů pokaždé, když je přístupný. Jakmile bude vytvořen, objeví se ve speciálním prostoru.",
"action": "Vytvořit uložený filtr",
"titleRequired": "Please provide a title for the filter."
"action": "Vytvořit uložený filtr"
},
"delete": {
"header": "Smazat tento uložený filtr",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Nyt Gemt Filter",
"description": "Et gemt filter er en virtuel liste, som beregnes ud fra et sæt filtre, hver gang det er tilgået. Når den er oprettet, vises den i et særligt navneområde.",
"action": "Opret nyt gemt filter",
"titleRequired": "Please provide a title for the filter."
"action": "Opret nyt gemt filter"
},
"delete": {
"header": "Slet dette gemte filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Neuer gespeicherter Filter",
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint diese in einem speziellen Namespace.",
"action": "Neuen gespeicherten Filter erstellen",
"titleRequired": "Bitte gib den Titel für den Filter an."
"action": "Neuen gespeicherten Filter erstellen"
},
"delete": {
"header": "Diesen gespeicherten Filter löschen",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Neuer gespeicherter Filter",
"description": "En gspeicherete Filter isch e virtuelli Liste, welche vomene Satz a Filter zemmegsetzt wird, sobald me uf sie zuegriift. Wenn sie mal erstellt worde isch, erhaltet si ihren eigene Namensruum.",
"action": "Neue gspeicherete Filter erstelle",
"titleRequired": "Bitte gib den Titel für den Filter an."
"action": "Neue gspeicherete Filter erstelle"
},
"delete": {
"header": "De g'speicheret Filter chüble",

View File

@ -405,8 +405,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Nouveau filtre enregistré",
"description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir dun ensemble de filtres à chaque fois quon y accède. Une fois créé, il apparaît dans un espace de noms spécial.",
"action": "Créer un nouveau filtre enregistré",
"titleRequired": "Please provide a title for the filter."
"action": "Créer un nouveau filtre enregistré"
},
"delete": {
"header": "Supprimer ce filtre enregistré",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Nuovo Filtro Salvato",
"description": "Un filtro salvato è una lista virtuale che viene calcolata da un insieme di filtri di volta in volta. Una volta creato, apparirà in un namespace speciale.",
"action": "Crea nuovo filtro salvato",
"titleRequired": "È necessario un titolo per il filtro."
"action": "Crea nuovo filtro salvato"
},
"delete": {
"header": "Elimina questo filtro salvato",
@ -912,7 +911,7 @@
}
},
"update": {
"available": "È disponibile un aggiornamento!",
"available": "There is an update available!",
"do": "Aggiorna Adesso"
},
"menu": {

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Nytt lagret filter",
"description": "Et lagret filter er en virtuell liste som beregnes fra et sett med filtre hver gang det åpnes. Når du er opprettet, vil det vises i et eget navneområde.",
"action": "Opprett nytt filter",
"titleRequired": "Please provide a title for the filter."
"action": "Opprett nytt filter"
},
"delete": {
"header": "Slett dette lagrede filteret",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Nowy filtr stały",
"description": "Filtr stały to wirtualna lista, która jest kalkulowana na podstawie zestawu filtrów przy każdym wejściu w nią. Po utworzeniu pojawi się w specjalnej sekcji.",
"action": "Utwórz nowy filtr stały",
"titleRequired": "Please provide a title for the filter."
"action": "Utwórz nowy filtr stały"
},
"delete": {
"header": "Usuń ten filtr stały",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Novo filtro salvo",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Novo Filtro Memorizado",
"description": "Um filtro memorizado é uma lista virtual que é compilada a partir de um conjunto de filtros de cada vez que é acedido. Uma vez criado, irá aparecer num espaço especial.",
"action": "Criar novo filtro memorizado",
"titleRequired": "Por favor, insere um título para o filtro."
"action": "Criar novo filtro memorizado"
},
"delete": {
"header": "Eliminar este filtro memorizado",

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Создать сохранённый фильтр",
"description": "Сохранённый фильтр — это виртуальный список, построенный из набора фильтров. При создании отображается в специальном пространстве имён.",
"action": "Создать новый сохранённый фильтр",
"titleRequired": "Укажите название фильтра."
"action": "Создать новый сохранённый фильтр"
},
"delete": {
"header": "Удалить этот сохранённый фильтр",

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -404,8 +404,7 @@
"create": {
"title": "Bộ lọc đã lưu mới",
"description": "Bộ lọc sẵn là một danh sách ảo được chọn từ một tập hợp các bộ lọc. Sau khi được tạo, nó sẽ xuất hiện trong một không gian làm việc đặc biệt.",
"action": "Tạo thêm bộ lọc sẵn",
"titleRequired": "Please provide a title for the filter."
"action": "Tạo thêm bộ lọc sẵn"
},
"delete": {
"header": "Xóa bộ lọc sẵn này",

View File

@ -404,8 +404,7 @@
"create": {
"title": "新保存的过滤器",
"description": "保存的过滤器是一个虚拟列表,在每次访问时从一组过滤器中计算出来。 创建后,它将出现在一个特殊的命名空间里。",
"action": "创建新保存的过滤器",
"titleRequired": "Please provide a title for the filter."
"action": "创建新保存的过滤器"
},
"delete": {
"header": "删除此保存的过滤器",

View File

@ -404,8 +404,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",

View File

@ -126,12 +126,6 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
/**
* Returns an object with all route parameters and their values.
* @example
* getRouteReplacements(
* '/tasks/{taskId}/assignees/{userId}',
* { taskId: 7, userId: 2 },
* )
* // { "{taskId}": 7, "{userId}": 2 }
*/
getRouteReplacements(route : string, parameters : Record<string, unknown> = {}) {
const replace$$1: Record<string, unknown> = {}
@ -154,8 +148,6 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
/**
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
* @example
* getReplacedRoute('/lists/{listId}/tasks', { listId: 3 }) === '/lists/1/tasks'
*/
getReplacedRoute(path : string, pathparams : Record<string, unknown>) : string {
const replacements = this.getRouteReplacements(path, pathparams)
@ -311,7 +303,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): Promise<Model[]> {
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
if (this.paths.getAll === '') {
throw new Error('This model is not able to get data.')
}
@ -331,7 +323,10 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
return []
}
return response.data.map(entry => this.modelGetAllFactory(entry))
if (Array.isArray(response.data)) {
return response.data.map(entry => this.modelGetAllFactory(entry))
}
return this.modelGetAllFactory(response.data)
} finally {
cancel()
}

View File

@ -2,7 +2,6 @@ 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 {useDebounceFn} from '@vueuse/core'
import type {IList} from '@/modelTypes/IList'
import type {ISavedFilter} from '@/modelTypes/ISavedFilter'
@ -134,38 +133,14 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
router.push({name: 'namespaces.index'})
}
const titleValid = ref(true)
const validateTitleField = useDebounceFn(() => {
titleValid.value = filter.value.title !== ''
}, 100)
async function createFilterWithValidation() {
if (!titleValid.value) {
return
}
return createFilter()
}
async function saveFilterWithValidation() {
if (!titleValid.value) {
return
}
return saveFilter()
}
return {
createFilter,
createFilterWithValidation,
saveFilter,
saveFilterWithValidation,
deleteFilter,
filter,
filters,
filterService,
titleValid,
validateTitleField,
}
}

View File

@ -6,7 +6,6 @@ import {HTTPFactory} from '@/helpers/fetcher'
import {objectToCamelCase} from '@/helpers/case'
import type {IProvider} from '@/types/IProvider'
import type {MIGRATORS} from '@/views/migrate/migrators'
export interface ConfigState {
version: string,
@ -15,10 +14,10 @@ export interface ConfigState {
linkSharingEnabled: boolean,
maxFileSize: string,
registrationEnabled: boolean,
availableMigrators: Array<keyof typeof MIGRATORS>,
availableMigrators: [],
taskAttachmentsEnabled: boolean,
totpEnabled: boolean,
enabledBackgroundProviders: Array<'unsplash' | 'upload'>,
enabledBackgroundProviders: [],
legal: {
imprintUrl: string,
privacyPolicyUrl: string,
@ -79,12 +78,11 @@ export const useConfigStore = defineStore('config', () => {
function setConfig(config: ConfigState) {
Object.assign(state, config)
}
async function update(): Promise<boolean> {
async function update() {
const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info')
setConfig(objectToCamelCase(config))
const success = !!config
return success
return config
}
return {

View File

@ -3,27 +3,25 @@
:title="$t('filters.edit.title')"
primary-icon=""
:primary-label="$t('misc.save')"
@primary="saveFilterWithValidation"
@primary="saveFilter"
:tertiary="$t('misc.delete')"
@tertiary="$router.push({ name: 'filter.settings.delete', params: { id: listId } })"
>
<form @submit.prevent="saveFilterWithValidation()">
<form @submit.prevent="saveFilter()">
<div class="field">
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
<div class="control">
<input
v-model="filter.title"
:class="{ 'disabled': filterService.loading, 'is-danger': !titleValid }"
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading || undefined"
@keyup.enter="saveFilter"
class="input"
id="Title"
id="title"
:placeholder="$t('filters.attributes.titlePlaceholder')"
type="text"
v-focus
@focusout="validateTitleField"
/>
v-model="filter.title"/>
</div>
<p class="help is-danger" v-if="!titleValid">{{ $t('filters.create.titleRequired') }}</p>
</div>
<div class="field">
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
@ -67,11 +65,9 @@ import type {IList} from '@/modelTypes/IList'
const props = defineProps<{ listId: IList['id'] }>()
const {
saveFilterWithValidation,
saveFilter,
filter,
filters,
filterService,
titleValid,
validateTitleField,
} = useSavedFilter(toRef(props, 'listId'))
</script>

View File

@ -12,17 +12,15 @@
<div class="control">
<input
v-model="filter.title"
:class="{ 'disabled': filterService.loading, 'is-danger': !titleValid }"
:class="{ 'disabled': filterService.loading}"
:disabled="filterService.loading || undefined"
class="input"
id="Title"
:placeholder="$t('filters.attributes.titlePlaceholder')"
type="text"
v-focus
@focusout="validateTitleField"
/>
</div>
<p class="help is-danger" v-if="!titleValid">{{ $t('filters.create.titleRequired') }}</p>
</div>
<div class="field">
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
@ -53,8 +51,8 @@
<template #footer>
<x-button
:loading="filterService.loading"
:disabled="filterService.loading || !titleValid"
@click="createFilterWithValidation()"
:disabled="filterService.loading"
@click="createFilter()"
class="is-fullwidth"
>
{{ $t('filters.create.action') }}
@ -73,9 +71,7 @@ import {useSavedFilter} from '@/services/savedFilter'
const {
filter,
filters,
createFilterWithValidation,
createFilter,
filterService,
titleValid,
validateTitleField,
} = useSavedFilter()
</script>

View File

@ -196,7 +196,7 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup.vue'
import {useTaskList, SortBy} from '@/composables/useTaskList'
import {useTaskList} from '@/composables/useTaskList'
import type {ITask} from '@/modelTypes/ITask'
const ACTIVE_COLUMNS_DEFAULT = {
@ -222,6 +222,21 @@ const props = defineProps({
},
})
type Order = 'asc' | 'desc' | 'none'
interface SortBy {
index: Order
done?: Order
title?: Order
priority?: Order
due_date?: Order
start_date?: Order
end_date?: Order
percent_done?: Order
created?: Order
updated?: Order
}
const SORT_BY_DEFAULT: SortBy = {
index: 'desc',
}
@ -229,7 +244,7 @@ const SORT_BY_DEFAULT: SortBy = {
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
const taskList = useTaskList(toRef(props, 'listId'), sortBy.value)
const taskList = useTaskList(toRef(props, 'listId'))
const {
loading,

View File

@ -16,7 +16,7 @@ interface IMigratorRecord {
[key: Migrator['id']]: Migrator
}
export const MIGRATORS = {
export const MIGRATORS: IMigratorRecord = {
wunderlist: {
id: 'wunderlist',
name: 'Wunderlist',
@ -49,4 +49,4 @@ export const MIGRATORS = {
icon: tickTickIcon as string,
isFileMigrator: true,
},
} as const satisfies IMigratorRecord
} as const