Compare commits

..

3 Commits

14 changed files with 352 additions and 375 deletions

View File

@ -53,8 +53,8 @@
"@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.39.0",
"@sentry/vue": "7.39.0",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
@ -66,7 +66,7 @@
"codemirror": "5.65.12",
"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",
@ -82,7 +82,7 @@
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "1.1.1",
"ufo": "1.1.0",
"vue": "3.2.47",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.2",
@ -105,10 +105,10 @@
"@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.14.2",
"@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.53.0",
"@typescript-eslint/parser": "5.53.0",
"@vitejs/plugin-legacy": "4.0.1",
"@vitejs/plugin-vue": "4.0.0",
"@vue/eslint-config-typescript": "11.0.2",
@ -119,7 +119,7 @@
"caniuse-lite": "1.0.30001458",
"csstype": "3.1.1",
"cypress": "12.7.0",
"esbuild": "0.17.11",
"esbuild": "0.17.10",
"eslint": "8.35.0",
"eslint-plugin-vue": "9.9.0",
"happy-dom": "8.9.0",
@ -129,7 +129,7 @@
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "3.0.1",
"postcss-preset-env": "8.0.1",
"rollup": "3.18.0",
"rollup": "3.17.3",
"rollup-plugin-visualizer": "5.9.0",
"sass": "1.58.3",
"start-server-and-test": "2.0.0",
@ -138,7 +138,7 @@
"vite-plugin-inject-preload": "1.3.0",
"vite-plugin-pwa": "0.14.4",
"vite-svg-loader": "4.0.0",
"vitest": "0.29.2",
"vitest": "0.29.1",
"vue-tsc": "1.2.0",
"wait-on": "7.0.1",
"workbox-cli": "6.5.4"

File diff suppressed because it is too large Load Diff

View File

@ -221,7 +221,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 +246,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 +342,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 {
@ -425,6 +418,7 @@ $vikunja-nav-selected-width: 0.4rem;
opacity: 1;
}
}
&:not(.dragging-disabled) .handle {
cursor: grab;
}
@ -433,7 +427,7 @@ $vikunja-nav-selected-width: 0.4rem;
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
font-weight: 600;
@ -488,24 +482,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" @click.stop.self="openTaskDetail">
<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,26 +114,27 @@
<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">
import {ref, watch, shallowReactive, toRef, type PropType, onMounted, onBeforeUnmount, computed} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
import TaskModel, { getHexColor } from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials//labels.vue'
import DeferTask from '@/components/tasks/partials//defer-task.vue'
import Labels from '@/components/tasks/partials/labels.vue'
import DeferTask from '@/components/tasks/partials/defer-task.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import User from '@/components/misc/user.vue'
@ -186,6 +184,7 @@ const props = defineProps({
const emit = defineEmits(['task-updated'])
const {t} = useI18n({useScope: 'global'})
const router = useRouter()
const taskService = shallowReactive(new TaskService())
const task = ref<ITask>(new TaskModel())
@ -275,6 +274,14 @@ function hideDeferDueDatePopup(e) {
showDefer.value = false
})
}
const taskLink = ref(null)
function openTaskDetail() {
const isTextSelected = window.getSelection().toString()
if (!isTextSelected) {
router.push(taskDetailRoute.value)
}
}
</script>
<style lang="scss" scoped>
@ -288,14 +295,18 @@ 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);
}
&:focus-within {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
a.tasktext {
box-shadow: none;
}
}
.tasktext,
&.tasktext {
white-space: nowrap;
@ -338,8 +349,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 +374,22 @@ function hideDeferDueDatePopup(e) {
}
}
.handle {
&:hover .favorite,
.favorite:focus {
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 +441,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

@ -405,7 +405,7 @@
"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."
"titleRequired": "Please provide a title for the filter."
},
"delete": {
"header": "Elimina questo filtro salvato",

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

@ -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

@ -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