forked from vikunja/frontend
Compare commits
71 Commits
65b90cbee0
...
0c6f1a4083
Author | SHA1 | Date | |
---|---|---|---|
|
0c6f1a4083 | ||
29eb42932a | |||
736e9051d8 | |||
4a4c401558 | |||
9198abe24d | |||
|
97c8970dd6 | ||
5303b6bc97 | |||
24a0a8f5eb | |||
|
d07ad495e2 | ||
8465afe421 | |||
d40729cbe7 | |||
fa0e46a399 | |||
b78481f9f6 | |||
cbc9cf6f7f | |||
62fd9a656e | |||
85269b4524 | |||
536d709961 | |||
59d6d7e786 | |||
ae86d0d42a | |||
9a20b7a853 | |||
5687b66ea5 | |||
1da411e1f6 | |||
e8a6d3f31b | |||
a25a795276 | |||
57f6abd99f | |||
84d205f90b | |||
de91e7c9ae | |||
2cf9c35acb | |||
db525db6eb | |||
88525ae7c8 | |||
957bfdc8f1 | |||
c52ae83b75 | |||
df40c4e475 | |||
3f41e9a3a6 | |||
1da510b5dd | |||
536db3fd46 | |||
cefa5250c5 | |||
f697640636 | |||
09b7595b68 | |||
6b7f73f724 | |||
|
d6b55c7570 | ||
|
3f4b08b8be | ||
791c61cabb | |||
e3dd4ef78a | |||
830d0887b9 | |||
e8db2c2b45 | |||
706a13242e | |||
13fab10584 | |||
4b0c8aa66b | |||
bfaf9401f4 | |||
13607124a6 | |||
9fc3d0a965 | |||
4d6286451e | |||
0479d17e69 | |||
5ca272959d | |||
c502f9b840 | |||
a3a313a21f | |||
c58d1ffd2e | |||
99dc5cf34f | |||
3604cb3ec7 | |||
aa01a92278 | |||
|
7b96397e3b | ||
b45a4e1aaf | |||
|
d3365d6add | ||
49cb2b9e6f | |||
d4ce10e79a | |||
345c5e3588 | |||
7ff84bcd29 | |||
d1633ef622 | |||
7e92bc63ac | |||
be076b65cf |
|
@ -51,7 +51,7 @@ LABEL maintainer="maintainers@vikunja.io"
|
|||
ENV VIKUNJA_HTTP_PORT 80
|
||||
ENV VIKUNJA_HTTP2_PORT 81
|
||||
ENV VIKUNJA_LOG_FORMAT main
|
||||
ENV VIKUNJA_API_URL http://localhost:3456/api/v1
|
||||
ENV VIKUNJA_API_URL /api/v1
|
||||
ENV VIKUNJA_SENTRY_ENABLED false
|
||||
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
||||
|
||||
|
@ -64,6 +64,7 @@ COPY --from=builder /build/dist ./
|
|||
# manage permissions
|
||||
RUN chmod 0755 /docker-entrypoint.d/*.sh /etc/nginx/templates && \
|
||||
chmod -R 0644 /etc/nginx/nginx.conf && \
|
||||
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates
|
||||
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
|
||||
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
||||
# unprivileged user
|
||||
USER nginx
|
||||
|
|
|
@ -51,6 +51,3 @@ pnpm run build
|
|||
pnpm run lint
|
||||
```
|
||||
|
||||
## Sponsors
|
||||
|
||||
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)
|
||||
|
|
|
@ -14,9 +14,9 @@ describe('List View List', () => {
|
|||
cy.visit('/lists/1')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/list')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'First List')
|
||||
cy.get('.list-title .dropdown')
|
||||
cy.get('.list-title-dropdown')
|
||||
.should('exist')
|
||||
cy.get('p')
|
||||
.contains('This list is currently empty.')
|
||||
|
@ -62,7 +62,7 @@ describe('List View List', () => {
|
|||
})
|
||||
cy.visit(`/lists/${lists[1].id}/`)
|
||||
|
||||
cy.get('.list-title .icon')
|
||||
cy.get('.list-title-wrapper .icon')
|
||||
.should('not.exist')
|
||||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('Lists', () => {
|
|||
.should('contain', 'Success')
|
||||
cy.url()
|
||||
.should('contain', '/lists/')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'New List')
|
||||
})
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('Lists', () => {
|
|||
const newListName = 'New list name'
|
||||
|
||||
cy.visit('/lists/1')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'First List')
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
|
@ -67,7 +67,7 @@ describe('Lists', () => {
|
|||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', newListName)
|
||||
.should('not.contain', lists[0].title)
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
|
||||
|
@ -104,9 +104,9 @@ describe('Lists', () => {
|
|||
it('Should archive a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
|
||||
cy.get('.list-title .dropdown')
|
||||
cy.get('.list-title-dropdown')
|
||||
.click()
|
||||
cy.get('.list-title .dropdown .dropdown-menu .dropdown-item')
|
||||
cy.get('.list-title-dropdown .dropdown-menu .dropdown-item')
|
||||
.contains('Archive')
|
||||
.click()
|
||||
cy.get('.modal-content')
|
||||
|
|
|
@ -11,7 +11,7 @@ export function createLists() {
|
|||
return lists
|
||||
}
|
||||
|
||||
export function prepareLists(setLists = () => {}) {
|
||||
export function prepareLists(setLists = (...args: any[]) => {}) {
|
||||
beforeEach(() => {
|
||||
const lists = createLists()
|
||||
setLists(lists)
|
||||
|
|
|
@ -2,9 +2,9 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|||
import {createLists} from '../list/prepareLists'
|
||||
|
||||
function logout() {
|
||||
cy.get('.navbar .user .username')
|
||||
cy.get('.navbar .username-dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.navbar .user .dropdown-menu .dropdown-item')
|
||||
cy.get('.navbar .dropdown-item')
|
||||
.contains('Logout')
|
||||
.click()
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('User Settings', () => {
|
|||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .user .username')
|
||||
cy.get('.navbar .username-dropdown-trigger .username')
|
||||
.should('contain', 'Lorem Ipsum')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
DEFAULT_CONF_FILE="etc/nginx/conf.d/default.conf"
|
||||
|
||||
if [ -f "/proc/net/if_inet6" ]; then
|
||||
echo "info: IPv6 available."
|
||||
exit 0
|
||||
if [ ! -f "/proc/net/if_inet6" ]; then
|
||||
echo "info: IPv6 is not available! Removing IPv6 listen configuration"
|
||||
find /etc/nginx/conf.d -name '*.conf' -type f | \
|
||||
while IFS= read -r CONFIG; do
|
||||
sed -r '/^\s*listen\s*\[::\]:.+$/d' "$CONFIG" > "$CONFIG.temp"
|
||||
if ! diff -U 5 "$CONFIG" "$CONFIG.temp" > "$CONFIG.diff"; then
|
||||
echo "info: Removing IPv6 lines from $CONFIG" | \
|
||||
cat - "$CONFIG.diff"
|
||||
echo "# IPv6 is disabled because /proc/net/if_inet6 was not found" | \
|
||||
cat - "$CONFIG.temp" > "$CONFIG"
|
||||
else
|
||||
echo "info: Skipping $CONFIG because it does not have IPv6 listen"
|
||||
fi
|
||||
rm -f "$CONFIG.temp" "$CONFIG.diff"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "info: IPv6 not available!"
|
||||
echo "info: Removing IPv6 lines from /$DEFAULT_CONF_FILE"
|
||||
sed -i 's/\(listen\s*\[::\].*\)$/#\1 # Disabled IPv6/' /${DEFAULT_CONF_FILE}
|
||||
|
|
67
package.json
67
package.json
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@7.26.3",
|
||||
"packageManager": "pnpm@7.27.0",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -45,28 +45,28 @@
|
|||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@infectoone/vue-ganttastic": "2.1.4",
|
||||
"@intlify/unplugin-vue-i18n": "0.8.1",
|
||||
"@kyvg/vue3-notification": "2.8.0",
|
||||
"@sentry/tracing": "7.36.0",
|
||||
"@sentry/vue": "7.36.0",
|
||||
"@intlify/unplugin-vue-i18n": "0.8.2",
|
||||
"@kyvg/vue3-notification": "2.9.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.12.0",
|
||||
"axios": "1.3.2",
|
||||
"axios": "1.3.3",
|
||||
"blurhash": "2.0.4",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.11",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "1.11.7",
|
||||
"dompurify": "2.4.3",
|
||||
"dompurify": "3.0.0",
|
||||
"easymde": "2.18.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
|
@ -75,10 +75,9 @@
|
|||
"focus-within": "3.0.2",
|
||||
"highlight.js": "11.7.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"klona": "2.0.6",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.2.12",
|
||||
"minimist": "1.2.7",
|
||||
"pinia": "2.0.30",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
|
@ -86,7 +85,7 @@
|
|||
"ufo": "1.0.1",
|
||||
"vue": "3.2.47",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.1",
|
||||
"vue-flatpickr-component": "11.0.2",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"workbox-precaching": "6.5.4",
|
||||
|
@ -95,10 +94,10 @@
|
|||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.3",
|
||||
"@cypress/vite-dev-server": "5.0.2",
|
||||
"@cypress/vue": "5.0.3",
|
||||
"@cypress/vue": "5.0.4",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@histoire/plugin-screenshot": "0.13.0",
|
||||
"@histoire/plugin-vue": "0.12.4",
|
||||
"@histoire/plugin-screenshot": "0.15.4",
|
||||
"@histoire/plugin-vue": "0.15.4",
|
||||
"@rushstack/eslint-patch": "1.2.0",
|
||||
"@types/codemirror": "5.60.7",
|
||||
"@types/dompurify": "2.4.0",
|
||||
|
@ -106,41 +105,41 @@
|
|||
"@types/focus-within": "1.0.1",
|
||||
"@types/lodash.debounce": "4.0.7",
|
||||
"@types/marked": "4.0.8",
|
||||
"@types/node": "18.11.19",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.50.0",
|
||||
"@typescript-eslint/parser": "5.50.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.2.10",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001449",
|
||||
"browserslist": "4.21.5",
|
||||
"caniuse-lite": "1.0.30001451",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "12.5.1",
|
||||
"esbuild": "0.17.5",
|
||||
"eslint": "8.33.0",
|
||||
"cypress": "12.6.0",
|
||||
"esbuild": "0.17.8",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"happy-dom": "8.2.0",
|
||||
"histoire": "0.12.4",
|
||||
"netlify-cli": "12.10.0",
|
||||
"happy-dom": "8.2.6",
|
||||
"histoire": "0.15.4",
|
||||
"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.14.0",
|
||||
"rollup": "3.15.0",
|
||||
"rollup-plugin-visualizer": "5.9.0",
|
||||
"sass": "1.58.0",
|
||||
"start-server-and-test": "1.15.3",
|
||||
"sass": "1.58.1",
|
||||
"start-server-and-test": "1.15.4",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "4.1.1",
|
||||
"vite-plugin-inject-preload": "1.2.0",
|
||||
"vite-plugin-pwa": "0.14.1",
|
||||
"vite-plugin-inject-preload": "1.3.0",
|
||||
"vite-plugin-pwa": "0.14.4",
|
||||
"vite-svg-loader": "4.0.0",
|
||||
"vitest": "0.28.4",
|
||||
"vue-tsc": "1.0.24",
|
||||
"vitest": "0.28.5",
|
||||
"vue-tsc": "1.1.2",
|
||||
"wait-on": "7.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
}
|
||||
|
|
1301
pnpm-lock.yaml
1301
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,7 @@
|
|||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"labels": ["dependencies"],
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:js-app"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
|
@ -20,6 +20,13 @@
|
|||
"@vueuse/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "histoire",
|
||||
"matchPackagePrefixes": [
|
||||
"@histoire/",
|
||||
"histoire"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"automerge": true,
|
||||
|
|
|
@ -2,95 +2,90 @@
|
|||
<header
|
||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
||||
aria-label="main navigation"
|
||||
class="navbar main-theme is-fixed-top d-print-none"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
<router-link :to="{name: 'home'}" class="logo-link">
|
||||
<Logo width="164" height="48"/>
|
||||
</router-link>
|
||||
<MenuButton class="menu-button"/>
|
||||
<div class="list-title" ref="listTitle" v-show="currentList.id">
|
||||
<template v-if="currentList.id">
|
||||
<h1
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
||||
</h1>
|
||||
|
||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="info-button">
|
||||
<MenuButton class="menu-button"/>
|
||||
|
||||
<div
|
||||
v-if="currentList.id"
|
||||
class="list-title-wrapper"
|
||||
>
|
||||
<h1 class="list-title">{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}</h1>
|
||||
|
||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="list-title-button">
|
||||
<icon icon="circle-info"/>
|
||||
</BaseButton>
|
||||
|
||||
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
|
||||
<list-settings-dropdown
|
||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
||||
class="list-title-dropdown"
|
||||
:list="currentList"
|
||||
>
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="list-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<BaseButton
|
||||
@click="openQuickActions"
|
||||
class="trigger-button pr-0"
|
||||
class="trigger-button"
|
||||
v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
</BaseButton>
|
||||
<notifications/>
|
||||
<div class="user">
|
||||
<dropdown class="is-right" ref="usernameDropdown">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<x-button
|
||||
<Notifications />
|
||||
<dropdown>
|
||||
<template #trigger="{toggleOpen, open}">
|
||||
<BaseButton
|
||||
class="username-dropdown-trigger"
|
||||
@click="toggleOpen()"
|
||||
@click="toggleOpen"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span class="icon is-small">
|
||||
<span class="icon is-small" :style="{
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||
}">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
</x-button>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<dropdown-item
|
||||
:to="{name: 'user.settings'}"
|
||||
>
|
||||
<dropdown-item :to="{name: 'user.settings'}">
|
||||
{{ $t('user.settings.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="imprintUrl"
|
||||
:href="imprintUrl"
|
||||
>
|
||||
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
||||
{{ $t('navigation.imprint') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="privacyPolicyUrl"
|
||||
:href="privacyPolicyUrl"
|
||||
>
|
||||
<dropdown-item v-if="privacyPolicyUrl" :href="privacyPolicyUrl">
|
||||
{{ $t('navigation.privacy') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="baseStore.setKeyboardShortcutsActive(true)"
|
||||
>
|
||||
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{name: 'about'}"
|
||||
>
|
||||
<dropdown-item :to="{name: 'about'}">
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="authStore.logout()"
|
||||
>
|
||||
<dropdown-item @click="authStore.logout()">
|
||||
{{ $t('user.auth.logout') }}
|
||||
</dropdown-item>
|
||||
</dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, nextTick} from 'vue'
|
||||
import {computed} from 'vue'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
|
||||
|
@ -120,182 +115,152 @@ const configStore = useConfigStore()
|
|||
const imprintUrl = computed(() => configStore.legal.imprintUrl)
|
||||
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
|
||||
|
||||
const usernameDropdown = ref()
|
||||
const listTitle = ref()
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (typeof usernameDropdown.value === 'undefined' || typeof listTitle.value === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const usernameWidth = usernameDropdown.value.$el.clientWidth
|
||||
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
|
||||
})
|
||||
|
||||
function openQuickActions() {
|
||||
baseStore.setQuickActionsActive(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$vikunja-nav-logo-full-width: 164px;
|
||||
$user-dropdown-width-mobile: 5rem;
|
||||
|
||||
$hamburger-menu-icon-spacing: 1rem;
|
||||
$hamburger-menu-icon-width: 28px;
|
||||
.navbar {
|
||||
--navbar-button-min-width: 40px;
|
||||
--navbar-gap-width: 1rem;
|
||||
--navbar-icon-size: 1.25rem;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--navbar-gap-width);
|
||||
|
||||
background: var(--site-background);
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding-left: 2rem;
|
||||
padding-right: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&.menu-active {
|
||||
@media screen and (max-width: $tablet) {
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: notifications should provide a slot for the icon instead, so that we can style it as we want
|
||||
:deep() {
|
||||
.trigger-button {
|
||||
color: var(--grey-400);
|
||||
font-size: var(--navbar-icon-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
display: none;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 2rem;
|
||||
margin-right: 1.5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
align-self: stretch;
|
||||
margin-right: auto;
|
||||
align-self: stretch;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin-left: $hamburger-menu-icon-spacing;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar.main-theme {
|
||||
background: var(--site-background);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
margin-left: 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
&.menu-active {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.user {
|
||||
width: $user-dropdown-width-mobile;
|
||||
|
||||
.username-dropdown-trigger {
|
||||
line-height: 1;
|
||||
padding: 0 0.25rem;
|
||||
height: 1rem;
|
||||
|
||||
.icon {
|
||||
width: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
// FIXME: notifications should provide a slot for the icon instead, so that we can style it as we want
|
||||
:deep() {
|
||||
.trigger-button {
|
||||
cursor: pointer;
|
||||
color: var(--grey-400);
|
||||
padding: .5rem;
|
||||
font-size: 1.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> * > .trigger-button {
|
||||
width: $navbar-icon-width;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
.list-title-wrapper {
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
// this makes the truncated text of the list title work
|
||||
// inside the flexbox parent
|
||||
min-width: 0;
|
||||
|
||||
.avatar {
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
height: 40px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.username-dropdown-trigger {
|
||||
background: none;
|
||||
|
||||
&:focus:not(:active), &:active {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding-inline: var(--navbar-gap-width);
|
||||
}
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
$edit-icon-width: 1rem;
|
||||
font-size: 1rem;
|
||||
// We need the following for overflowing ellipsis to work
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
--nav-username-width: 0;
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width} - #{$vikunja-nav-logo-full-width} - var(--nav-username-width));
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-dropdown {
|
||||
align-self: stretch;
|
||||
|
||||
.list-title-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-button {
|
||||
align-self: stretch;
|
||||
min-width: var(--navbar-button-min-width);
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--navbar-icon-size);
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
> * {
|
||||
min-width: var(--navbar-button-min-width);
|
||||
}
|
||||
}
|
||||
|
||||
.username-dropdown-trigger {
|
||||
padding-left: 1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-family: $vikunja-font;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width});
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.dropdown-trigger) {
|
||||
color: var(--grey-400);
|
||||
margin-left: .5rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-button {
|
||||
text-align: center;
|
||||
height: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
width: 2rem;
|
||||
margin-top: .25rem;
|
||||
padding: 0 .5rem;
|
||||
color: var(--grey-400);
|
||||
margin-left: .5rem;
|
||||
.avatar {
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
height: 40px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
</style>
|
|
@ -86,9 +86,6 @@ function showKeyboardShortcuts() {
|
|||
|
||||
const route = useRoute()
|
||||
|
||||
// hide menu on mobile
|
||||
watch(() => route.fullPath, () => window.innerWidth < 769 && baseStore.setMenuActive(false))
|
||||
|
||||
// FIXME: this is really error prone
|
||||
// Reset the current list highlight in menu if the current route is not list related.
|
||||
watch(() => route.name as string, (routeName) => {
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, onBeforeMount} from 'vue'
|
||||
import {ref, computed, onBeforeMount} from 'vue'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
import type {SortableEvent} from 'sortablejs'
|
||||
|
||||
|
@ -159,7 +159,6 @@ import Logo from '@/components/home/Logo.vue'
|
|||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import {useEventListener} from '@vueuse/core'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
@ -200,17 +199,8 @@ const namespaceListsCount = computed(() => {
|
|||
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
|
||||
})
|
||||
|
||||
|
||||
useEventListener('resize', resize)
|
||||
onMounted(() => resize())
|
||||
|
||||
const listStore = useListStore()
|
||||
|
||||
function resize() {
|
||||
// Hide the menu by default on mobile
|
||||
baseStore.setMenuActive(window.innerWidth >= 770)
|
||||
}
|
||||
|
||||
function toggleLists(namespaceId: INamespace['id']) {
|
||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:where(.button) {
|
||||
.button {
|
||||
transition: all $transition;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="dropdown" ref="dropdown">
|
||||
<slot name="trigger" :close="close" :toggleOpen="toggleOpen">
|
||||
<slot name="trigger" :close="close" :toggleOpen="toggleOpen" :open="open">
|
||||
<BaseButton class="dropdown-trigger is-flex" @click="toggleOpen">
|
||||
<icon :icon="triggerIcon" class="icon"/>
|
||||
</BaseButton>
|
||||
|
@ -56,7 +56,6 @@ onClickOutside(dropdown, (e: Event) => {
|
|||
.dropdown {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="notifications">
|
||||
<div class="is-flex is-justify-content-center">
|
||||
<BaseButton @click.stop="showNotifications = !showNotifications" class="trigger-button">
|
||||
<slot name="trigger" toggleOpen="() => showNotifications = !showNotifications" :has-unread-notifications="unreadNotifications > 0">
|
||||
<BaseButton class="trigger-button" @click.stop="showNotifications = !showNotifications">
|
||||
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
|
||||
<icon icon="bell"/>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
|
@ -141,7 +141,11 @@ function to(n, index) {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.notifications {
|
||||
width: $navbar-icon-width;
|
||||
display: flex;
|
||||
|
||||
.trigger-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
|
@ -156,9 +160,9 @@ function to(n, index) {
|
|||
}
|
||||
|
||||
.notifications-list {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
margin-top: 1rem;
|
||||
top: calc(100% + 1rem);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
|
|
47
src/composables/useMenuActive.ts
Normal file
47
src/composables/useMenuActive.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {ref, watch, readonly} from 'vue'
|
||||
import {useLocalStorage, useMediaQuery} from '@vueuse/core'
|
||||
|
||||
const BULMA_MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useMenuActive() {
|
||||
const isMobile = useMediaQuery(`(max-width: ${BULMA_MOBILE_BREAKPOINT}px)`)
|
||||
|
||||
const desktopPreference = useLocalStorage(
|
||||
'menuActiveDesktopPreference',
|
||||
true,
|
||||
// If we have two tabs open we want to be able to have the menu open in one window
|
||||
// and closed in the other. The last changed value will be the new preference
|
||||
{listenToStorageChanges: false},
|
||||
)
|
||||
|
||||
const menuActive = ref(false)
|
||||
|
||||
// set to prefered value
|
||||
watch(isMobile, (current) => {
|
||||
menuActive.value = current
|
||||
// On mobile we don't show the menu in an expanded state
|
||||
// because that would hide the main content
|
||||
? false
|
||||
: desktopPreference.value
|
||||
}, {immediate: true})
|
||||
|
||||
watch(menuActive, (current) => {
|
||||
if (!isMobile.value) {
|
||||
desktopPreference.value = current
|
||||
}
|
||||
})
|
||||
|
||||
function setMenuActive(newMenuActive: boolean) {
|
||||
menuActive.value = newMenuActive
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuActive.value = menuActive.value = !menuActive.value
|
||||
}
|
||||
|
||||
return {
|
||||
menuActive: readonly(menuActive),
|
||||
setMenuActive,
|
||||
toggleMenu,
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import {useRouter} from 'vue-router'
|
|||
import {useEventListener} from '@vueuse/core'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {MILLISECONDS_A_HOUR, SECONDS_A_HOUR} from '@/constants/date'
|
||||
import {MILLISECONDS_A_SECOND, SECONDS_A_HOUR} from '@/constants/date'
|
||||
|
||||
const SECONDS_TOKEN_VALID = 60 * SECONDS_A_HOUR
|
||||
|
||||
|
@ -24,11 +24,14 @@ export function useRenewTokenOnFocus() {
|
|||
return
|
||||
}
|
||||
|
||||
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - new Date().valueOf() / MILLISECONDS_A_HOUR
|
||||
const nowInSeconds = new Date().getTime() / MILLISECONDS_A_SECOND
|
||||
const expiresIn = userInfo.value !== null
|
||||
? userInfo.value.exp - nowInSeconds
|
||||
: 0
|
||||
|
||||
// If the token expiry is negative, it is already expired and we have no choice but to redirect
|
||||
// the user to the login page
|
||||
if (expiresIn < 0) {
|
||||
if (expiresIn <= 0) {
|
||||
await authStore.checkAuth()
|
||||
await router.push({name: 'user.login'})
|
||||
return
|
||||
|
|
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update available!",
|
||||
"available": "Es ist ein Update verfügbar!",
|
||||
"do": "Jetzt aktualisieren"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update available!",
|
||||
"available": "Es ist ein Update verfügbar!",
|
||||
"do": "Jetzt aktualisierä"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update available!",
|
||||
"available": "È disponibile un aggiornamento!",
|
||||
"do": "Aggiorna Adesso"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {readonly, ref} from 'vue'
|
||||
import { readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
@ -7,6 +7,8 @@ import ListModel from '@/models/list'
|
|||
import ListService from '../services/list'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
|
@ -23,7 +25,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
const blurHash = ref('')
|
||||
|
||||
const hasTasks = ref(false)
|
||||
const menuActive = ref(true)
|
||||
const keyboardShortcutsActive = ref(false)
|
||||
const quickActionsActive = ref(false)
|
||||
const logoVisible = ref(true)
|
||||
|
@ -53,14 +54,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
hasTasks.value = newHasTasks
|
||||
}
|
||||
|
||||
function setMenuActive(newMenuActive: boolean) {
|
||||
menuActive.value = newMenuActive
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuActive.value = !menuActive.value
|
||||
}
|
||||
|
||||
function setKeyboardShortcutsActive(value: boolean) {
|
||||
keyboardShortcutsActive.value = value
|
||||
}
|
||||
|
@ -139,25 +132,24 @@ export const useBaseStore = defineStore('base', () => {
|
|||
background: readonly(background),
|
||||
blurHash: readonly(blurHash),
|
||||
hasTasks: readonly(hasTasks),
|
||||
menuActive: readonly(menuActive),
|
||||
keyboardShortcutsActive: readonly(keyboardShortcutsActive),
|
||||
quickActionsActive: readonly(quickActionsActive),
|
||||
logoVisible: readonly(logoVisible),
|
||||
|
||||
setLoading,
|
||||
setReady,
|
||||
setCurrentList,
|
||||
setHasTasks,
|
||||
setMenuActive,
|
||||
toggleMenu,
|
||||
setKeyboardShortcutsActive,
|
||||
setQuickActionsActive,
|
||||
setBackground,
|
||||
setBlurHash,
|
||||
setLogoVisible,
|
||||
setReady,
|
||||
|
||||
handleSetCurrentList,
|
||||
loadApp,
|
||||
|
||||
...useMenuActive(),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {computed, readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import {findById, findIndexById} from '@/helpers/utils'
|
||||
import {i18n} from '@/i18n'
|
||||
|
@ -333,7 +333,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const bucketIndex = findIndexById(buckets.value, updatedBucketData.id)
|
||||
const oldBucket = cloneDeep(buckets.value[bucketIndex])
|
||||
const oldBucket = klona(buckets.value[bucketIndex])
|
||||
|
||||
const updatedBucket = {
|
||||
...oldBucket,
|
||||
|
|
|
@ -33,4 +33,3 @@ $switch-view-height: 2.69rem;
|
|||
|
||||
$navbar-height: 4rem;
|
||||
$navbar-width: 300px;
|
||||
$navbar-icon-width: 40px;
|
|
@ -256,8 +256,8 @@
|
|||
--card-border-color: var(--grey-200);
|
||||
--logo-text-color: hsl(180, 1%, 15%);
|
||||
|
||||
@media screen {
|
||||
&.dark {
|
||||
@media screen {
|
||||
// Light mode colours reversed for dark mode
|
||||
--grey-900-hsl: 210, 20%, 98%;
|
||||
--grey-900: hsl(var(--grey-900-hsl));
|
||||
|
|
|
@ -227,7 +227,7 @@
|
|||
import {computed, nextTick, ref, watch, type PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
import BucketModel from '@/models/bucket'
|
||||
|
@ -419,7 +419,7 @@ async function updateTaskPosition(e) {
|
|||
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
|
||||
taskUpdating.value[task.id] = true
|
||||
|
||||
const newTask = cloneDeep(task) // cloning the task to avoid pinia store manipulation
|
||||
const newTask = klona(task) // cloning the task to avoid pinia store manipulation
|
||||
newTask.bucketId = newBucket.id
|
||||
newTask.kanbanPosition = calculateItemPosition(
|
||||
taskBefore !== null ? taskBefore.kanbanPosition : null,
|
||||
|
@ -432,7 +432,7 @@ async function updateTaskPosition(e) {
|
|||
// Make sure the first and second task don't both get position 0 assigned
|
||||
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
||||
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
|
||||
const newTaskAfter = cloneDeep(taskAfter) // cloning the task to avoid pinia store manipulation
|
||||
const newTaskAfter = klona(taskAfter) // cloning the task to avoid pinia store manipulation
|
||||
newTaskAfter.bucketId = newBucket.id
|
||||
newTaskAfter.kanbanPosition = calculateItemPosition(
|
||||
0,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {computed, ref, shallowReactive, watch, type Ref} from 'vue'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import type {Filters} from '@/composables/useRouteFilters'
|
||||
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
|
||||
|
@ -64,7 +64,7 @@ export function useGanttTaskList<F extends Filters>(
|
|||
}
|
||||
|
||||
async function updateTask(task: ITaskPartialWithId) {
|
||||
const oldTask = cloneDeep(tasks.value.get(task.id))
|
||||
const oldTask = klona(tasks.value.get(task.id))
|
||||
|
||||
if (!oldTask) return
|
||||
|
||||
|
|
|
@ -449,7 +449,7 @@ import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type P
|
|||
import {useRouter, type RouteLocation} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
|
||||
|
@ -703,7 +703,7 @@ async function saveTask(args?: {
|
|||
undoCallback,
|
||||
} = {
|
||||
...{
|
||||
task: cloneDeep(task),
|
||||
task: klona(task),
|
||||
},
|
||||
...args,
|
||||
}
|
||||
|
|
|
@ -72,7 +72,17 @@ export default defineConfig(({mode}) => {
|
|||
plugins: [
|
||||
postcssEasings(),
|
||||
postcssEasingGradients(),
|
||||
postcssPresetEnv(),
|
||||
postcssPresetEnv({
|
||||
// Since postcss-preset-env v8.0.0 the 'enableClientSidePolyfills' option is disabled by default.
|
||||
// This is the list of features that require a client side library:
|
||||
// https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#plugins-that-need-client-library
|
||||
// Since we only use 'focus-within-pseudo-class' we have to force enable
|
||||
// that plugin now manually in order to keep the browser support as it was.
|
||||
// See also './src/polyfills.ts'
|
||||
features: {
|
||||
'focus-within-pseudo-class': true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user