forked from vikunja/frontend
Compare commits
40 Commits
fd1d01164f
...
e948678e42
Author | SHA1 | Date |
---|---|---|
renovate | e948678e42 | |
Dominik Pschenitschni | 5ccedc6f67 | |
Dominik Pschenitschni | 74ad98de68 | |
Dominik Pschenitschni | 3282f55c34 | |
Dominik Pschenitschni | d9984b28f7 | |
Dominik Pschenitschni | 4fc7b9c67e | |
Dominik Pschenitschni | ff9efe7889 | |
Dominik Pschenitschni | 66be0e6ac4 | |
Dominik Pschenitschni | da8df8b667 | |
Dominik Pschenitschni | 42e9f306e8 | |
Angelo Delicato | 4b47478440 | |
Dominik Pschenitschni | b42e4cca59 | |
Dominik Pschenitschni | 33d4efecc4 | |
Dominik Pschenitschni | 45ec1623d5 | |
Dominik Pschenitschni | 8ef309243d | |
Dominik Pschenitschni | 3aaacf4533 | |
renovate | 0350e37fbb | |
renovate | 244c436202 | |
drone | 18d0c8ba2c | |
kolaente | 3891d5b876 | |
Dominik Pschenitschni | 98b38af43c | |
konrad | 77ff0aa256 | |
renovate | 2ab26ee7c5 | |
renovate | 58f38bcfc3 | |
renovate | bcb5190365 | |
renovate | c99d09c83e | |
renovate | f49ea9752d | |
renovate | a56683cdc2 | |
renovate | 7f4af63003 | |
Dominik Pschenitschni | 8c44ed83e6 | |
renovate | b388677eaf | |
renovate | bd7430b405 | |
renovate | 4baed8fe79 | |
renovate | fdbe4e8314 | |
renovate | 4a7f839449 | |
renovate | c359f4d4dd | |
renovate | 79d6212e48 | |
renovate | e541213872 | |
Dominik Pschenitschni | 631a19fa92 | |
Dominik Pschenitschni | fba402fcd0 |
|
@ -45,7 +45,7 @@ describe('List History', () => {
|
|||
|
||||
cy.get('body')
|
||||
.should('contain', 'Last viewed')
|
||||
cy.get('.list-cards-wrapper-2-rows')
|
||||
cy.get('[data-cy="listCardGrid"]')
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('contain', lists[1].title)
|
||||
.should('contain', lists[2].title)
|
||||
|
|
|
@ -78,7 +78,7 @@ describe('List View List', () => {
|
|||
|
||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||
cy.get('.tasks-container .tasks .color-bubble')
|
||||
cy.get('.tasks .color-bubble')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
|
@ -90,9 +90,9 @@ describe('List View List', () => {
|
|||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks-container .tasks')
|
||||
cy.get('.tasks')
|
||||
.should('contain', tasks[1].title)
|
||||
cy.get('.tasks-container .tasks')
|
||||
cy.get('.tasks')
|
||||
.should('not.contain', tasks[99].title)
|
||||
|
||||
cy.get('.card-content .pagination .pagination-link')
|
||||
|
@ -101,9 +101,9 @@ describe('List View List', () => {
|
|||
|
||||
cy.url()
|
||||
.should('contain', '?page=2')
|
||||
cy.get('.tasks-container .tasks')
|
||||
cy.get('.tasks')
|
||||
.should('contain', tasks[99].title)
|
||||
cy.get('.tasks-container .tasks')
|
||||
cy.get('.tasks')
|
||||
.should('not.contain', tasks[1].title)
|
||||
})
|
||||
})
|
|
@ -52,7 +52,7 @@ describe('Lists', () => {
|
|||
cy.get('.list-title h1')
|
||||
.should('contain', 'First List')
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
||||
.contains('Edit')
|
||||
|
@ -80,7 +80,7 @@ describe('Lists', () => {
|
|||
it('Should remove a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
|
||||
.contains('Delete')
|
||||
|
|
42
package.json
42
package.json
|
@ -18,15 +18,15 @@
|
|||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.2",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@infectoone/vue-ganttastic": "2.1.2",
|
||||
"@kyvg/vue3-notification": "2.6.1",
|
||||
"@sentry/tracing": "7.19.0",
|
||||
"@sentry/vue": "7.19.0",
|
||||
"@sentry/tracing": "7.20.0",
|
||||
"@sentry/vue": "7.20.0",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
|
@ -51,11 +51,11 @@
|
|||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.2.2",
|
||||
"minimist": "1.2.7",
|
||||
"pinia": "2.0.23",
|
||||
"pinia": "2.0.24",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.0",
|
||||
"ufo": "0.8.6",
|
||||
"ufo": "1.0.0",
|
||||
"vue": "3.2.45",
|
||||
"vue-advanced-cropper": "2.8.6",
|
||||
"vue-flatpickr-component": "11.0.1",
|
||||
|
@ -77,38 +77,38 @@
|
|||
"@types/marked": "4.0.7",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.1",
|
||||
"@typescript-eslint/parser": "5.42.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"@vitejs/plugin-legacy": "2.3.1",
|
||||
"@vitejs/plugin-vue": "3.2.0",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
"@vue/test-utils": "2.2.2",
|
||||
"@vue/test-utils": "2.2.3",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001430",
|
||||
"caniuse-lite": "1.0.30001431",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "11.0.1",
|
||||
"esbuild": "0.15.13",
|
||||
"eslint": "8.27.0",
|
||||
"cypress": "11.1.0",
|
||||
"esbuild": "0.15.14",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-plugin-vue": "9.7.0",
|
||||
"express": "4.18.2",
|
||||
"happy-dom": "7.6.6",
|
||||
"netlify-cli": "12.1.0",
|
||||
"happy-dom": "7.7.0",
|
||||
"netlify-cli": "12.1.1",
|
||||
"postcss": "8.4.19",
|
||||
"postcss-preset-env": "7.8.2",
|
||||
"postcss-preset-env": "7.8.3",
|
||||
"rollup": "3.3.0",
|
||||
"rollup-plugin-visualizer": "5.8.3",
|
||||
"sass": "1.56.1",
|
||||
"typescript": "4.8.4",
|
||||
"vite": "3.2.3",
|
||||
"typescript": "4.9.3",
|
||||
"vite": "3.2.4",
|
||||
"vite-plugin-pwa": "0.13.3",
|
||||
"vite-svg-loader": "3.6.0",
|
||||
"vitest": "0.25.1",
|
||||
"vitest": "0.25.2",
|
||||
"vue-tsc": "1.0.9",
|
||||
"wait-on": "6.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "pnpm@7.15.0"
|
||||
"packageManager": "pnpm@7.16.1"
|
||||
}
|
||||
|
|
813
pnpm-lock.yaml
813
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -54,8 +54,8 @@
|
|||
</p>
|
||||
|
||||
<modal
|
||||
@close="() => showHowItWorks = false"
|
||||
:enabled="showHowItWorks"
|
||||
@close="() => showHowItWorks = false"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</router-view>
|
||||
|
||||
<modal
|
||||
v-if="currentModal"
|
||||
:enabled="Boolean(currentModal)"
|
||||
@close="closeModal()"
|
||||
variant="scrolling"
|
||||
class="task-detail-view-modal"
|
||||
|
@ -159,27 +159,20 @@ labelStore.loadAllLabels()
|
|||
.app-content {
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
|
||||
@media screen {
|
||||
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
|
||||
}
|
||||
padding: 1.5rem 0.5rem 1rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin-left: 0;
|
||||
padding-top: 1.5rem;
|
||||
min-height: calc(100vh - 4rem);
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
&.is-menu-enabled {
|
||||
&.is-menu-enabled {
|
||||
@media screen and (min-width: $tablet) {
|
||||
margin-left: $navbar-width;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,6 +230,4 @@ labelStore.loadAllLabels()
|
|||
.content-auth.z-unset {
|
||||
z-index: unset;
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
<ul class="menu-list">
|
||||
<li>
|
||||
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
||||
<span class="icon">
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="calendar"/>
|
||||
</span>
|
||||
{{ $t('navigation.overview') }}
|
||||
|
@ -15,7 +15,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
|
||||
<span class="icon">
|
||||
<span class="menu-item-icon icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
{{ $t('navigation.upcoming') }}
|
||||
|
@ -23,7 +23,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
|
||||
<span class="icon">
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="layer-group"/>
|
||||
</span>
|
||||
{{ $t('namespace.title') }}
|
||||
|
@ -31,7 +31,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
|
||||
<span class="icon">
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="tags"/>
|
||||
</span>
|
||||
{{ $t('label.title') }}
|
||||
|
@ -39,7 +39,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
|
||||
<span class="icon">
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
{{ $t('team.title') }}
|
||||
|
@ -63,7 +63,7 @@
|
|||
/>
|
||||
<span class="name">{{ namespaceTitles[nk] }}</span>
|
||||
<div
|
||||
class="icon is-small toggle-lists-icon pl-2"
|
||||
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
|
||||
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
||||
>
|
||||
<icon icon="chevron-down"/>
|
||||
|
@ -72,7 +72,7 @@
|
|||
({{ namespaceListsCount[nk] }})
|
||||
</span>
|
||||
</BaseButton>
|
||||
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
|
||||
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
|
||||
</div>
|
||||
<!--
|
||||
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
||||
|
@ -111,11 +111,11 @@
|
|||
class="list-menu-link"
|
||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
||||
>
|
||||
<span class="icon handle">
|
||||
<span class="icon menu-item-icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<ColorBubble
|
||||
v-if="l.hexColor !== ''"
|
||||
v-if="l.hexColor !== ''"
|
||||
:color="l.hexColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
@ -128,7 +128,13 @@
|
|||
>
|
||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
</BaseButton>
|
||||
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
|
||||
<list-settings-dropdown class="menu-list-dropdown" :list="l" v-if="l.id > 0">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
<span class="list-setting-spacer" v-else></span>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -280,6 +286,18 @@ $vikunja-nav-background: var(--site-background);
|
|||
$vikunja-nav-color: var(--grey-700);
|
||||
$vikunja-nav-selected-width: 0.4rem;
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
|
||||
padding-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.namespace-container {
|
||||
background: $vikunja-nav-background;
|
||||
color: $vikunja-nav-color;
|
||||
|
@ -303,248 +321,226 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
transform: translateX(0);
|
||||
transition: transform $transition-duration ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
.menu-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-weight: bold;
|
||||
font-family: $vikunja-font;
|
||||
color: $vikunja-nav-color;
|
||||
font-weight: 500;
|
||||
min-height: 2.5rem;
|
||||
padding-top: 0;
|
||||
padding-left: $navbar-padding;
|
||||
// these are general menu styles
|
||||
// should be in own components
|
||||
.menu {
|
||||
.menu-label,
|
||||
.menu-list .list-menu-link,
|
||||
.menu-list a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-label,
|
||||
.menu-list .list-menu-link,
|
||||
.menu-list a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
.list-menu-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-bubble {
|
||||
height: 12px;
|
||||
flex: 0 0 12px;
|
||||
}
|
||||
|
||||
}
|
||||
.favorite {
|
||||
margin-left: .25rem;
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 0;
|
||||
|
||||
&:hover,
|
||||
&.is-favorite {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.favorite.is-favorite,
|
||||
.list-menu:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
.color-bubble {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.is-archived {
|
||||
min-width: 85px;
|
||||
}
|
||||
}
|
||||
|
||||
.namespace-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: $vikunja-nav-color;
|
||||
padding: 0 .25rem;
|
||||
|
||||
.menu-label {
|
||||
margin-bottom: 0;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--grey-500);
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dropdown-trigger) {
|
||||
padding: .5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-lists-icon {
|
||||
svg {
|
||||
transition: all $transition;
|
||||
transform: rotate(90deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active svg {
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .toggle-lists-icon svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not(.has-menu) .toggle-lists-icon {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-label,
|
||||
.nsettings,
|
||||
.menu-list .list-menu-link,
|
||||
.menu-list a {
|
||||
color: $vikunja-nav-color;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
li {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
:deep(.dropdown-trigger) {
|
||||
opacity: 0;
|
||||
padding: .5rem;
|
||||
cursor: pointer;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
&:hover :deep(.dropdown-trigger) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform $transition-duration;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: var(--grey-200);
|
||||
|
||||
* {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.list-menu-link, li > a {
|
||||
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border-left: $vikunja-nav-selected-width solid transparent;
|
||||
|
||||
.icon {
|
||||
height: 1rem;
|
||||
vertical-align: middle;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
&.handle {
|
||||
opacity: 0;
|
||||
transition: opacity $transition;
|
||||
margin-right: .25rem;
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .icon.handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: var(--primary);
|
||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
||||
|
||||
.icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
|
||||
padding-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.namespaces-lists {
|
||||
padding-top: math.div($navbar-padding, 2);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--grey-400) !important;
|
||||
.color-bubble {
|
||||
height: 12px;
|
||||
flex: 0 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
margin-top: math.div($navbar-padding, 2);
|
||||
.menu-list {
|
||||
li {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.menu-list {
|
||||
li {
|
||||
font-weight: 500;
|
||||
font-family: $vikunja-font;
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.list-menu-link, li > a {
|
||||
padding-left: 2rem;
|
||||
display: inline-block;
|
||||
.menu-list-dropdown {
|
||||
opacity: 0;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
&:hover .menu-list-dropdown {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.menu-list-dropdown-trigger {
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform $transition-duration;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: var(--grey-200);
|
||||
|
||||
* {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.list-menu-link,
|
||||
li > a {
|
||||
color: $vikunja-nav-color;
|
||||
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
border-radius: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border-left: $vikunja-nav-selected-width solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: var(--primary);
|
||||
border-left: $vikunja-nav-selected-width solid var(--primary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 1rem;
|
||||
vertical-align: middle;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
&.router-link-exact-active .icon:not(.handle) {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.handle {
|
||||
opacity: 0;
|
||||
transition: opacity $transition;
|
||||
margin-right: .25rem;
|
||||
cursor: grab;
|
||||
}
|
||||
&:hover .handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
margin-top: math.div($navbar-padding, 2);
|
||||
|
||||
.menu-list {
|
||||
li {
|
||||
font-weight: 500;
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
|
||||
.list-menu-link,
|
||||
li > a {
|
||||
padding-left: 2rem;
|
||||
display: inline-block;
|
||||
|
||||
.icon {
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.namespaces-lists {
|
||||
padding-top: math.div($navbar-padding, 2);
|
||||
|
||||
.menu-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-weight: bold;
|
||||
font-family: $vikunja-font;
|
||||
color: $vikunja-nav-color;
|
||||
font-weight: 500;
|
||||
min-height: 2.5rem;
|
||||
padding-top: 0;
|
||||
padding-left: $navbar-padding;
|
||||
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--grey-500);
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.favorite {
|
||||
margin-left: .25rem;
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 0;
|
||||
|
||||
&:hover,
|
||||
&.is-favorite {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.favorite.is-favorite,
|
||||
.list-menu:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.list-menu-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-bubble {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.is-archived {
|
||||
min-width: 85px;
|
||||
}
|
||||
}
|
||||
|
||||
.namespace-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: $vikunja-nav-color;
|
||||
padding: 0 .25rem;
|
||||
|
||||
.toggle-lists-icon {
|
||||
svg {
|
||||
transition: all $transition;
|
||||
transform: rotate(90deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active svg {
|
||||
transform: rotate(0deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .toggle-lists-icon svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not(.has-menu) .toggle-lists-icon {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
||||
</BaseButton>
|
||||
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||
|
||||
<BaseButton
|
||||
|
@ -84,7 +84,7 @@
|
|||
{{ $t('misc.confirm') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -94,6 +94,7 @@ import flatPickr from 'vue-flatpickr-component'
|
|||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
|
||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
|
|
|
@ -9,6 +9,7 @@ export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunct
|
|||
uploadImage,
|
||||
imageUploadFunction,
|
||||
minHeight: '150px',
|
||||
sideBySideFullscreen: false,
|
||||
toolbar: [
|
||||
{
|
||||
name: 'heading-1',
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
|
||||
<BaseButton
|
||||
class="search-result-button is-fullwidth"
|
||||
|
@ -78,8 +78,7 @@
|
|||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</CustomTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -90,6 +89,7 @@ import {useI18n} from 'vue-i18n'
|
|||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
function elementInResults(elem: string | any, label: string, query: string): boolean {
|
||||
// Don't make create available if we have an exact match in our search results.
|
||||
|
|
|
@ -44,11 +44,11 @@
|
|||
</div>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
|
||||
{{ $t('list.archived') }}
|
||||
</Message>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
|
||||
<slot v-if="loadedListId"/>
|
||||
</div>
|
||||
|
@ -60,6 +60,7 @@ import {useRoute} from 'vue-router'
|
|||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ListService from '@/services/list'
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<dropdown>
|
||||
<template #trigger="triggerProps">
|
||||
<slot name="trigger" v-bind="triggerProps">
|
||||
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="isSavedFilter(list)">
|
||||
<dropdown-item
|
||||
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
|
||||
|
@ -78,6 +86,7 @@
|
|||
<script setup lang="ts">
|
||||
import {ref, computed, watchEffect, type PropType} from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Subscription from '@/components/misc/subscription.vue'
|
||||
|
@ -115,4 +124,4 @@ function setSubscriptionInStore(sub: ISubscription) {
|
|||
listStore.setList(updatedList)
|
||||
namespaceStore.setListInNamespaceById(updatedList)
|
||||
}
|
||||
</script>
|
||||
</script>
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<div
|
||||
class="list-card"
|
||||
:class="{
|
||||
'has-light-text': background !== null,
|
||||
'has-background': blurHashUrl !== '' || background !== null
|
||||
}"
|
||||
:style="{
|
||||
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined,
|
||||
'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="list-background background-fade-in"
|
||||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
/>
|
||||
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
||||
|
||||
<div class="list-title" aria-hidden="true">{{ list.title }}</div>
|
||||
<BaseButton
|
||||
class="list-button"
|
||||
:aria-label="list.title"
|
||||
:title="list.description"
|
||||
:to="{
|
||||
name: 'list.index',
|
||||
params: { listId: list.id}
|
||||
}"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="!list.isArchived"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': list.isFavorite}"
|
||||
@click.prevent.stop="listStore.toggleListFavorite(list)"
|
||||
>
|
||||
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {toRef, type PropType} from 'vue'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {useListBackground} from './useListBackground'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object as PropType<IList>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
|
||||
const listStore = useListStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-card {
|
||||
--list-card-padding: 1rem;
|
||||
background: var(--white);
|
||||
padding: var(--list-card-padding);
|
||||
border-radius: $radius;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow $transition;
|
||||
position: relative;
|
||||
overflow: hidden; // hide background
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
box-shadow: var(--shadow-xs) !important;
|
||||
}
|
||||
|
||||
> * {
|
||||
// so the elements are on top of the background
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.has-background,
|
||||
.list-background {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.list-background,
|
||||
.list-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.is-archived {
|
||||
font-size: .75rem;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
align-self: flex-end;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
line-height: var(--title-line-height);
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
max-height: calc(100% - (var(--list-card-padding) + 1rem)); // padding & height of the "is archived" badge
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.has-light-text .list-title {
|
||||
color: var(--grey-100);
|
||||
}
|
||||
|
||||
.has-background .list-title {
|
||||
text-shadow:
|
||||
0 0 10px var(--black),
|
||||
1px 1px 5px var(--grey-700),
|
||||
-1px -1px 5px var(--grey-700);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.favorite {
|
||||
position: absolute;
|
||||
top: var(--list-card-padding);
|
||||
right: var(--list-card-padding);
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.is-favorite {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.list-card:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.background-fade-in {
|
||||
opacity: 0;
|
||||
transition: opacity $transition;
|
||||
transition-delay: $transition-duration * 2; // To fake an appearing background
|
||||
|
||||
&.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<ul class="list-grid">
|
||||
<li
|
||||
v-for="(item, index) in filteredLists"
|
||||
:key="`list_${item.id}_${index}`"
|
||||
class="list-grid-item"
|
||||
>
|
||||
<ListCard :list="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, type PropType} from 'vue'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import ListCard from './ListCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
lists: {
|
||||
type: Array as PropType<IList[]>,
|
||||
default: () => [],
|
||||
},
|
||||
showArchived: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
itemLimit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const filteredLists = computed(() => {
|
||||
return props.showArchived
|
||||
? props.lists
|
||||
: props.lists.filter(l => !l.isArchived)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$list-height: 150px;
|
||||
$list-spacing: 1rem;
|
||||
|
||||
.list-grid {
|
||||
margin: 0; // reset li
|
||||
list-style-type: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--list-columns), 1fr);
|
||||
grid-auto-rows: $list-height;
|
||||
gap: $list-spacing;
|
||||
|
||||
@media screen and (min-width: $mobile) {
|
||||
--list-rows: 4;
|
||||
--list-columns: 1;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $mobile) and (max-width: $tablet) {
|
||||
--list-columns: 2;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: $widescreen) {
|
||||
--list-columns: 3;
|
||||
--list-rows: 3;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
--list-columns: 5;
|
||||
--list-rows: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.list-grid-item {
|
||||
display: grid;
|
||||
margin-top: 0; // remove padding coming form .content li + li
|
||||
}
|
||||
</style>
|
|
@ -14,11 +14,11 @@
|
|||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
<modal
|
||||
@close="() => modalOpen = false"
|
||||
:enabled="modalOpen"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="() => modalOpen = false"
|
||||
>
|
||||
<filters
|
||||
:has-title="true"
|
||||
|
|
|
@ -1,222 +0,0 @@
|
|||
<template>
|
||||
<router-link
|
||||
:class="{
|
||||
'has-light-text': !colorIsDark(list.hexColor) || background !== null,
|
||||
'has-background': blurHashUrl !== '' || background !== null,
|
||||
}"
|
||||
:style="{
|
||||
'background-color': list.hexColor,
|
||||
'background-image': blurHashUrl !== null ? `url(${blurHashUrl})` : false,
|
||||
}"
|
||||
:to="{ name: 'list.index', params: { listId: list.id} }"
|
||||
class="list-card"
|
||||
v-if="list !== null && (showArchived ? true : !list.isArchived)"
|
||||
>
|
||||
<div
|
||||
class="list-background background-fade-in"
|
||||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
/>
|
||||
<div class="list-content">
|
||||
<span class="is-archived" v-if="list.isArchived">
|
||||
{{ $t('namespace.archived') }}
|
||||
</span>
|
||||
<BaseButton
|
||||
v-else
|
||||
:class="{'is-favorite': list.isFavorite}"
|
||||
@click.stop="listStore.toggleListFavorite(list)"
|
||||
class="favorite"
|
||||
>
|
||||
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
</BaseButton>
|
||||
|
||||
<div class="title">{{ list.title }}</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {type PropType, ref, watch} from 'vue'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
const blurHashUrl = ref('')
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object as PropType<IList>,
|
||||
required: true,
|
||||
},
|
||||
showArchived: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
})
|
||||
|
||||
watch(props.list, loadBackground, {immediate: true})
|
||||
|
||||
async function loadBackground() {
|
||||
if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const blurHash = await getBlobFromBlurHash(props.list.backgroundBlurHash)
|
||||
if (blurHash) {
|
||||
blurHashUrl.value = window.URL.createObjectURL(blurHash)
|
||||
}
|
||||
|
||||
backgroundLoading.value = true
|
||||
|
||||
const listService = new ListService()
|
||||
try {
|
||||
background.value = await listService.background(props.list)
|
||||
} finally {
|
||||
backgroundLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const listStore = useListStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-card {
|
||||
cursor: pointer;
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
height: $list-height;
|
||||
background: var(--white);
|
||||
margin: 0 $list-spacing $list-spacing 0;
|
||||
border-radius: $radius;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow $transition;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.has-light-text .title {
|
||||
color: var(--grey-100) !important;
|
||||
}
|
||||
|
||||
&.has-background,
|
||||
.list-background {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&.has-background .title {
|
||||
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.list-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus:not(:active) {
|
||||
box-shadow: var(--shadow-xs) !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
|
||||
$lists-per-row: 3;
|
||||
& {
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
}
|
||||
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
$lists-per-row: 2;
|
||||
& {
|
||||
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||
}
|
||||
|
||||
&:nth-child(#{$lists-per-row}n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
$lists-per-row: 1;
|
||||
& {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-content {
|
||||
display: flex;
|
||||
align-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
padding: 1rem;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
|
||||
.is-archived {
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.favorite {
|
||||
margin-left: auto;
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 0;
|
||||
display: block;
|
||||
|
||||
&:hover,
|
||||
&.is-favorite {
|
||||
color: var(--warning);
|
||||
}
|
||||
}
|
||||
|
||||
.favorite.is-favorite,
|
||||
&:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
align-self: flex-end;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: 400;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,55 @@
|
|||
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,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<transition :name="name">
|
||||
<slot />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
name: 'flash-background' | 'fade' | 'width' | 'modal'
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$flash-background-duration: 750ms;
|
||||
|
||||
.flash-background-enter-from,
|
||||
.flash-background-enter-active {
|
||||
animation: flash-background $flash-background-duration ease 1;
|
||||
}
|
||||
|
||||
@keyframes flash-background {
|
||||
0% {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
100% {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes flash-background {
|
||||
0% {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity $transition-duration;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.width-enter-active,
|
||||
.width-leave-active {
|
||||
transition: width $transition-duration;
|
||||
}
|
||||
|
||||
.width-enter-from,
|
||||
.width-leave-to {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.modal-enter,
|
||||
.modal-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
|
@ -6,13 +6,13 @@
|
|||
</BaseButton>
|
||||
</slot>
|
||||
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<div class="dropdown-menu" v-if="open">
|
||||
<div class="dropdown-content">
|
||||
<slot :close="close"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -21,6 +21,7 @@ import {ref, type PropType} from 'vue'
|
|||
import {onClickOutside} from '@vueuse/core'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- FIXME: transition should not be included in the modal -->
|
||||
<transition :name="transitionName">
|
||||
<CustomTransition :name="transitionName" appear>
|
||||
<section
|
||||
v-if="enabled"
|
||||
class="modal-mask"
|
||||
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
|
@ -70,6 +70,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import {ref, useAttrs, watchEffect} from 'vue'
|
||||
import {useScrollLock} from '@vueuse/core'
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</card>
|
||||
</no-auth-wrapper>
|
||||
</section>
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<section class="vikunja-loading" v-if="showLoading">
|
||||
<Logo class="logo"/>
|
||||
<p>
|
||||
|
@ -37,7 +37,7 @@
|
|||
{{ $t('ready.loading') }}
|
||||
</p>
|
||||
</section>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -47,6 +47,7 @@ import {useRouter, useRoute} from 'vue-router'
|
|||
import Logo from '@/assets/logo.svg?component'
|
||||
import ApiConfig from '@/components/misc/api-config.vue'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||
|
||||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<dropdown>
|
||||
<template #trigger="triggerProps">
|
||||
<slot name="trigger" v-bind="triggerProps">
|
||||
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="namespace.isArchived">
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
||||
|
@ -56,6 +64,7 @@
|
|||
<script setup lang="ts">
|
||||
import {ref, onMounted, type PropType} from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Subscription from '@/components/misc/subscription.vue'
|
||||
|
@ -85,3 +94,9 @@ function setSubscriptionInStore(sub: ISubscription) {
|
|||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dropdown-trigger {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
<span class="head">{{ $t('notification.title') }}</span>
|
||||
<div
|
||||
|
@ -42,7 +42,7 @@
|
|||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -52,6 +52,7 @@ import {useRouter} from 'vue-router'
|
|||
|
||||
import NotificationService from '@/services/notification'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import User from '@/components/misc/user.vue'
|
||||
import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<modal v-if="active" @close="closeQuickActions" :overflow="isNewTaskCommand">
|
||||
<modal :enabled="active" @close="closeQuickActions" :overflow="isNewTaskCommand">
|
||||
<div class="card quick-actions">
|
||||
<div class="action-input" :class="{'has-active-cmd': selectedCmd !== null}">
|
||||
<div class="active-cmd tag" v-if="selectedCmd !== null">
|
||||
|
|
|
@ -169,21 +169,19 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
@close="showDeleteModal = false"
|
||||
@submit="remove(listId)"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{ $t('list.share.links.remove') }}</span>
|
||||
</template>
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="remove(listId)"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{ $t('list.share.links.remove') }}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.links.removeText') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.links.removeText') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -297,6 +295,4 @@ function getShareLink(hash: string, view: ListView = LIST_VIEWS.LIST) {
|
|||
.sharables-list:not(.card-content) {
|
||||
overflow-y: auto
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -113,22 +113,20 @@
|
|||
{{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }}
|
||||
</nothing>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteSharable()"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{
|
||||
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
|
||||
}}</span>
|
||||
</template>
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteSharable()"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{
|
||||
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
|
||||
}}</span>
|
||||
</template>
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -381,8 +379,4 @@ async function find(query: string) {
|
|||
return typeof sharables.value.find(s => s.id === m.id) === 'undefined'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@include modal-transition();
|
||||
</style>
|
||||
</script>
|
|
@ -3,7 +3,7 @@
|
|||
@submit.prevent="createTask"
|
||||
class="add-new-task"
|
||||
>
|
||||
<transition name="width">
|
||||
<CustomTransition name="width">
|
||||
<input
|
||||
v-if="newTaskFieldActive"
|
||||
v-model="newTaskTitle"
|
||||
|
@ -13,7 +13,7 @@
|
|||
ref="newTaskTitleField"
|
||||
type="text"
|
||||
/>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
<x-button @click="showCreateTaskOrCreate" :shadow="false" icon="plus">
|
||||
{{ $t('task.new') }}
|
||||
</x-button>
|
||||
|
@ -24,6 +24,8 @@
|
|||
import {nextTick, ref} from 'vue'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create-task', title: string): Promise<ITask>
|
||||
}>()
|
||||
|
|
|
@ -41,9 +41,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, unref, watch} from 'vue'
|
||||
import {computed, ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {debouncedWatch, type MaybeRef, tryOnMounted, useWindowSize} from '@vueuse/core'
|
||||
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
@ -53,74 +52,7 @@ import TaskRelationModel from '@/models/taskRelation'
|
|||
import {RELATION_KIND} from '@/types/IRelationKind'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
function useAutoHeightTextarea(value: MaybeRef<string>) {
|
||||
const textarea = ref<HTMLInputElement>()
|
||||
const minHeight = ref(0)
|
||||
|
||||
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
|
||||
function resize(textareaEl: HTMLInputElement | undefined) {
|
||||
if (!textareaEl) return
|
||||
|
||||
let empty
|
||||
|
||||
// the value here is the attribute value
|
||||
if (!textareaEl.value && textareaEl.placeholder) {
|
||||
empty = true
|
||||
textareaEl.value = textareaEl.placeholder
|
||||
}
|
||||
|
||||
const cs = getComputedStyle(textareaEl)
|
||||
|
||||
textareaEl.style.minHeight = ''
|
||||
textareaEl.style.height = '0'
|
||||
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
|
||||
const height = textareaEl.scrollHeight + offset + 'px'
|
||||
|
||||
textareaEl.style.height = height
|
||||
|
||||
// calculate min-height for the first time
|
||||
if (!minHeight.value) {
|
||||
minHeight.value = parseFloat(height)
|
||||
}
|
||||
|
||||
textareaEl.style.minHeight = minHeight.value.toString()
|
||||
|
||||
|
||||
if (empty) {
|
||||
textareaEl.value = ''
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tryOnMounted(() => {
|
||||
if (textarea.value) {
|
||||
// we don't want scrollbars
|
||||
textarea.value.style.overflowY = 'hidden'
|
||||
}
|
||||
})
|
||||
|
||||
const {width: windowWidth} = useWindowSize()
|
||||
|
||||
debouncedWatch(
|
||||
windowWidth,
|
||||
() => resize(textarea.value),
|
||||
{debounce: 200},
|
||||
)
|
||||
|
||||
// It is not possible to get notified of a change of the value attribute of a textarea without workarounds (setTimeout)
|
||||
// So instead we watch the value that we bound to it.
|
||||
watch(
|
||||
() => [textarea.value, unref(value)],
|
||||
() => resize(textarea.value),
|
||||
{
|
||||
immediate: true, // calculate initial size
|
||||
flush: 'post', // resize after value change is rendered to DOM
|
||||
},
|
||||
)
|
||||
|
||||
return textarea
|
||||
}
|
||||
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
|
||||
|
||||
const props = defineProps({
|
||||
defaultPosition: {
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
<template>
|
||||
<card
|
||||
class="taskedit"
|
||||
:title="$t('list.list.editTask')"
|
||||
@close="$emit('close')"
|
||||
:has-close="true"
|
||||
>
|
||||
<form @submit.prevent="editTaskSubmit()">
|
||||
<div class="field">
|
||||
<label class="label" for="tasktext">{{ $t('task.attributes.title') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
:class="{ disabled: taskService.loading }"
|
||||
:disabled="taskService.loading || undefined"
|
||||
@change="editTaskSubmit()"
|
||||
class="input"
|
||||
id="tasktext"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="taskEditTask.title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="taskdescription">{{ $t('task.attributes.description') }}</label>
|
||||
<div class="control">
|
||||
<editor
|
||||
:preview-is-default="false"
|
||||
id="taskdescription"
|
||||
:placeholder="$t('task.description.placeholder')"
|
||||
v-if="editorActive"
|
||||
v-model="taskEditTask.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<strong>{{ $t('task.attributes.reminders') }}</strong>
|
||||
<reminders
|
||||
v-model="taskEditTask.reminderDates"
|
||||
@update:model-value="editTaskSubmit()"
|
||||
/>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.labels') }}</label>
|
||||
<div class="control">
|
||||
<edit-labels
|
||||
:task-id="taskEditTask.id"
|
||||
v-model="taskEditTask.labels"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.color') }}</label>
|
||||
<div class="control">
|
||||
<color-picker v-model="taskEditTask.hexColor" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
:loading="taskService.loading"
|
||||
class="is-fullwidth"
|
||||
@click="editTaskSubmit()"
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
|
||||
<router-link
|
||||
class="mt-2 has-text-centered is-block"
|
||||
:to="taskDetailRoute"
|
||||
>
|
||||
{{ $t('task.openDetail') }}
|
||||
</router-link>
|
||||
</form>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, computed, shallowReactive, watch, nextTick, type PropType} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import EditLabels from './partials/editLabels.vue'
|
||||
import Reminders from './partials/reminders.vue'
|
||||
import ColorPicker from '../input/colorPicker.vue'
|
||||
|
||||
import {success} from '@/message'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object as PropType<ITask | null>,
|
||||
},
|
||||
})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
|
||||
const editorActive = ref(false)
|
||||
let taskEditTask: ITask | undefined
|
||||
|
||||
|
||||
// FIXME: this initialization should not be necessary here
|
||||
function initTaskFields() {
|
||||
taskEditTask.dueDate =
|
||||
+new Date(props.task.dueDate) === 0 ? null : props.task.dueDate
|
||||
taskEditTask.startDate =
|
||||
+new Date(props.task.startDate) === 0
|
||||
? null
|
||||
: props.task.startDate
|
||||
taskEditTask.endDate =
|
||||
+new Date(props.task.endDate) === 0 ? null : props.task.endDate
|
||||
// This makes the editor trigger its mounted function again which makes it forget every input
|
||||
// it currently has in its textarea. This is a counter-hack to a hack inside of vue-easymde
|
||||
// which made it impossible to detect change from the outside. Therefore the component would
|
||||
// not update if new content from the outside was made available.
|
||||
// See https://github.com/NikulinIlya/vue-easymde/issues/3
|
||||
editorActive.value = false
|
||||
nextTick(() => (editorActive.value = true))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.task,
|
||||
() => {
|
||||
if (!taskEditTask) {
|
||||
taskEditTask = reactive(props.task)
|
||||
} else {
|
||||
Object.assign(taskEditTask, new TaskModel(props.task))
|
||||
}
|
||||
initTaskFields()
|
||||
},
|
||||
{immediate: true },
|
||||
)
|
||||
const taskDetailRoute = computed(() => {
|
||||
return {
|
||||
name: 'task.detail',
|
||||
params: { id: taskEditTask.id },
|
||||
state: { backdropView: router.currentRoute.value.fullPath },
|
||||
}
|
||||
})
|
||||
|
||||
async function editTaskSubmit() {
|
||||
const newTask = await taskService.update(taskEditTask)
|
||||
Object.assign(taskEditTask, newTask)
|
||||
initTaskFields()
|
||||
success({message: t('task.detail.updateSuccess')})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.priority-select {
|
||||
.select,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
ul.assingees {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: 0.5rem 0.5rem 0;
|
||||
|
||||
a {
|
||||
float: right;
|
||||
color: var(--danger);
|
||||
transition: all $transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -130,7 +130,7 @@
|
|||
|
||||
<!-- Delete modal -->
|
||||
<modal
|
||||
v-if="attachmentToDelete !== null"
|
||||
:enabled="attachmentToDelete !== null"
|
||||
@close="setAttachmentToDelete(null)"
|
||||
@submit="deleteAttachment()"
|
||||
>
|
||||
|
@ -148,7 +148,7 @@
|
|||
|
||||
<!-- Attachment image modal -->
|
||||
<modal
|
||||
v-if="attachmentImageBlobUrl !== null"
|
||||
:enabled="attachmentImageBlobUrl !== null"
|
||||
@close="attachmentImageBlobUrl = null"
|
||||
>
|
||||
<img :src="attachmentImageBlobUrl" alt=""/>
|
||||
|
@ -432,6 +432,4 @@ async function setCoverImage(attachment: IAttachment | null) {
|
|||
border-radius: 4px;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -49,14 +49,12 @@ const label = computed(() => {
|
|||
align-items: center;
|
||||
padding-left: .5rem;
|
||||
font-size: .9rem;
|
||||
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
transition: stroke-dashoffset 0.35s;
|
||||
margin-right: .25rem;
|
||||
|
||||
}
|
||||
|
||||
circle {
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
>
|
||||
· {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
class="is-inline-flex"
|
||||
v-if="
|
||||
|
@ -63,7 +63,7 @@
|
|||
>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
<editor
|
||||
:hasPreview="true"
|
||||
|
@ -94,15 +94,15 @@
|
|||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="form">
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
class="is-inline-flex"
|
||||
v-if="taskCommentService.loading && creating"
|
||||
class="is-inline-flex"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
{{ $t('task.comment.creating') }}
|
||||
</span>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
<div class="field">
|
||||
<editor
|
||||
:class="{
|
||||
|
@ -132,22 +132,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
v-if="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="() => deleteComment(commentToDelete)"
|
||||
>
|
||||
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="() => deleteComment(commentToDelete)"
|
||||
>
|
||||
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>
|
||||
{{ $t('task.comment.deleteText1') }}<br/>
|
||||
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<template #text>
|
||||
<p>
|
||||
{{ $t('task.comment.deleteText1') }}<br/>
|
||||
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -155,6 +153,7 @@
|
|||
import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
|
||||
import TaskCommentService from '@/services/taskComment'
|
||||
|
@ -348,9 +347,11 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
|||
}
|
||||
}
|
||||
|
||||
.image.is-avatar {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.media-content {
|
||||
width: calc(100% - 48px - 2rem);
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -5,7 +5,7 @@
|
|||
<icon icon="align-left"/>
|
||||
</span>
|
||||
{{ $t('task.attributes.description') }}
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<span class="is-small is-inline-flex" v-if="loading && saving">
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
{{ $t('misc.saving') }}
|
||||
|
@ -14,7 +14,7 @@
|
|||
<icon icon="check"/>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</h3>
|
||||
<editor
|
||||
:is-edit-enabled="canWrite"
|
||||
|
@ -33,6 +33,7 @@
|
|||
<script setup lang="ts">
|
||||
import {ref,computed, watch, type PropType} from 'vue'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
>
|
||||
{{ task.title.trim() }}
|
||||
</h1>
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
v-if="loading && saving"
|
||||
class="is-inline-flex is-align-items-center"
|
||||
|
@ -32,7 +32,7 @@
|
|||
<icon icon="check" class="mr-2"/>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -41,6 +41,7 @@ import {ref, computed, type PropType} from 'vue'
|
|||
import {useRouter} from 'vue-router'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
import Done from '@/components/misc/Done.vue'
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<ButtonLink @click="() => visible = true">{{ $t('task.quickAddMagic.what') }}</ButtonLink>
|
||||
</p>
|
||||
<modal
|
||||
@close="() => visible = false"
|
||||
:enabled="visible"
|
||||
@close="() => visible = false"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<template v-if="editEnabled && showCreate">
|
||||
<label class="label" key="label">
|
||||
{{ $t('task.relation.new') }}
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<span class="is-inline-flex" v-if="taskRelationService.loading">
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
{{ $t('misc.saving') }}
|
||||
|
@ -22,7 +22,7 @@
|
|||
<span class="has-text-success" v-else-if="!taskRelationService.loading && saved">
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</label>
|
||||
<div class="field" key="field-search">
|
||||
<Multiselect
|
||||
|
@ -133,7 +133,7 @@
|
|||
</p>
|
||||
|
||||
<modal
|
||||
v-if="relationToDelete !== undefined"
|
||||
:enabled="relationToDelete !== undefined"
|
||||
@close="relationToDelete = undefined"
|
||||
@submit="removeTaskRelation()"
|
||||
>
|
||||
|
@ -163,6 +163,7 @@ import {RELATION_KINDS, RELATION_KIND, type IRelationKind} from '@/types/IRelati
|
|||
import TaskRelationService from '@/services/taskRelation'
|
||||
import TaskRelationModel from '@/models/taskRelation'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
|
@ -442,6 +443,4 @@ async function toggleTaskDone(task: ITask) {
|
|||
padding: 0;
|
||||
height: 18px; // The exact height of the checkbox in the container
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -74,9 +74,9 @@
|
|||
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
|
||||
</time>
|
||||
</BaseButton>
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
|
||||
<priority-label :priority="task.priority" :done="task.done"/>
|
||||
|
||||
|
@ -140,6 +140,7 @@ import User from '@/components/misc/user.vue'
|
|||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import {ref, unref, watch} from 'vue'
|
||||
import {debouncedWatch, tryOnMounted, useWindowSize, type MaybeRef} from '@vueuse/core'
|
||||
|
||||
// TODO: also add related styles
|
||||
// OR: replace with vueuse function
|
||||
export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
||||
const textarea = ref<HTMLInputElement>()
|
||||
const minHeight = ref(0)
|
||||
|
||||
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
|
||||
function resize(textareaEl: HTMLInputElement | undefined) {
|
||||
if (!textareaEl) return
|
||||
|
||||
let empty
|
||||
|
||||
// the value here is the attribute value
|
||||
if (!textareaEl.value && textareaEl.placeholder) {
|
||||
empty = true
|
||||
textareaEl.value = textareaEl.placeholder
|
||||
}
|
||||
|
||||
const cs = getComputedStyle(textareaEl)
|
||||
|
||||
textareaEl.style.minHeight = ''
|
||||
textareaEl.style.height = '0'
|
||||
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
|
||||
const height = textareaEl.scrollHeight + offset + 'px'
|
||||
|
||||
textareaEl.style.height = height
|
||||
|
||||
// calculate min-height for the first time
|
||||
if (!minHeight.value) {
|
||||
minHeight.value = parseFloat(height)
|
||||
}
|
||||
|
||||
textareaEl.style.minHeight = minHeight.value.toString()
|
||||
|
||||
|
||||
if (empty) {
|
||||
textareaEl.value = ''
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tryOnMounted(() => {
|
||||
if (textarea.value) {
|
||||
// we don't want scrollbars
|
||||
textarea.value.style.overflowY = 'hidden'
|
||||
}
|
||||
})
|
||||
|
||||
const {width: windowWidth} = useWindowSize()
|
||||
|
||||
debouncedWatch(
|
||||
windowWidth,
|
||||
() => resize(textarea.value),
|
||||
{debounce: 200},
|
||||
)
|
||||
|
||||
// It is not possible to get notified of a change of the value attribute of a textarea without workarounds (setTimeout)
|
||||
// So instead we watch the value that we bound to it.
|
||||
watch(
|
||||
() => [textarea.value, unref(value)],
|
||||
() => resize(textarea.value),
|
||||
{
|
||||
immediate: true, // calculate initial size
|
||||
flush: 'post', // resize after value change is rendered to DOM
|
||||
},
|
||||
)
|
||||
|
||||
return textarea
|
||||
}
|
|
@ -172,7 +172,7 @@
|
|||
"list": {
|
||||
"title": "Liste",
|
||||
"add": "Hinzufügen",
|
||||
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
|
||||
"addPlaceholder": "Neue Aufgabe hinzufügen …",
|
||||
"empty": "Diese Liste ist derzeit leer.",
|
||||
"newTaskCta": "Eine neue Aufgabe erstellen.",
|
||||
"editTask": "Aufgabe bearbeiten"
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
"upload": "Upload",
|
||||
"uploadAvatar": "Upload Avatar",
|
||||
"statusUpdateSuccess": "Avatar status was updated successfully!",
|
||||
"setSuccess": "The avatar has been set successfully!"
|
||||
"setSuccess": "¡El avatar se ha establecido con éxito!"
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"title": "Quick Add Magic Mode",
|
||||
|
|
|
@ -39,8 +39,8 @@ export default class ListService extends AbstractService<IList> {
|
|||
return list
|
||||
}
|
||||
|
||||
async background(list) {
|
||||
if (list.background === null) {
|
||||
async background(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||
if (list.backgroundInformation === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ export default class ListService extends AbstractService<IList> {
|
|||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
async removeBackground(list) {
|
||||
async removeBackground(list: Pick<IList, 'id'>) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
|
|
|
@ -23,7 +23,8 @@ function redirectToProviderIfNothingElseIsEnabled() {
|
|||
auth.local.enabled === false &&
|
||||
auth.openidConnect.enabled &&
|
||||
auth.openidConnect.providers?.length === 1 &&
|
||||
(window.location.pathname.startsWith('/login') || window.location.pathname === '/') // Kinda hacky, but prevents an endless loop.
|
||||
(window.location.pathname.startsWith('/login') || window.location.pathname === '/') && // Kinda hacky, but prevents an endless loop.
|
||||
window.location.search.includes('redirectToProvider=true')
|
||||
) {
|
||||
redirectToProvider(auth.openidConnect.providers[0], auth.openidConnect.redirectUrl)
|
||||
}
|
||||
|
@ -285,7 +286,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
async function verifyEmail(): Promise<boolean> {
|
||||
const emailVerifyToken = localStorage.getItem('emailConfirmToken')
|
||||
if (emailVerifyToken) {
|
||||
const stopLoading = setModuleLoading(this, setIsLoading)
|
||||
const stopLoading = setModuleLoading(setIsLoading)
|
||||
try {
|
||||
await HTTPFactory().post('user/confirm', {token: emailVerifyToken})
|
||||
return true
|
||||
|
@ -308,7 +309,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
}) {
|
||||
const userSettingsService = new UserSettingsService()
|
||||
|
||||
const cancel = setModuleLoading(this, setIsLoadingGeneralSettings)
|
||||
const cancel = setModuleLoading(setIsLoadingGeneralSettings)
|
||||
try {
|
||||
saveLanguage(settings.language)
|
||||
await userSettingsService.update(settings)
|
||||
|
|
|
@ -1,23 +1,9 @@
|
|||
export interface LoadingState {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const LOADING_TIMEOUT = 100
|
||||
|
||||
export const setModuleLoading = <Store extends LoadingState>(store: Store, loadFunc : ((isLoading: boolean) => void) | null = null) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (loadFunc === null) {
|
||||
store.isLoading = true
|
||||
} else {
|
||||
loadFunc(true)
|
||||
}
|
||||
}, LOADING_TIMEOUT)
|
||||
export function setModuleLoading(loadFunc: (isLoading: boolean) => void) {
|
||||
const timeout = setTimeout(() => loadFunc(true), LOADING_TIMEOUT)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
if (loadFunc === null) {
|
||||
store.isLoading = false
|
||||
} else {
|
||||
loadFunc(false)
|
||||
}
|
||||
loadFunc(false)
|
||||
}
|
||||
}
|
|
@ -224,7 +224,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
}
|
||||
|
||||
async function loadBucketsForList({listId, params}: {listId: IList['id'], params}) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
|
||||
setBuckets([])
|
||||
|
@ -259,7 +259,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
return
|
||||
}
|
||||
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
setBucketLoading({bucketId: bucketId, loading: true})
|
||||
|
||||
const params = JSON.parse(JSON.stringify(ps))
|
||||
|
@ -302,7 +302,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
}
|
||||
|
||||
async function createBucket(bucket: IBucket) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const bucketService = new BucketService()
|
||||
try {
|
||||
|
@ -315,7 +315,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
}
|
||||
|
||||
async function deleteBucket({bucket, params}: {bucket: IBucket, params}) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const bucketService = new BucketService()
|
||||
try {
|
||||
|
@ -330,7 +330,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
}
|
||||
|
||||
async function updateBucket(updatedBucketData: Partial<IBucket>) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const bucketIndex = findIndexById(buckets.value, updatedBucketData.id)
|
||||
const oldBucket = cloneDeep(buckets.value[bucketIndex])
|
||||
|
|
|
@ -81,7 +81,7 @@ export const useLabelStore = defineStore('label', () => {
|
|||
return
|
||||
}
|
||||
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
try {
|
||||
const newLabels = await getAllLabels()
|
||||
|
@ -93,7 +93,7 @@ export const useLabelStore = defineStore('label', () => {
|
|||
}
|
||||
|
||||
async function deleteLabel(label: ILabel) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const labelService = new LabelService()
|
||||
|
||||
try {
|
||||
|
@ -107,7 +107,7 @@ export const useLabelStore = defineStore('label', () => {
|
|||
}
|
||||
|
||||
async function updateLabel(label: ILabel) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const labelService = new LabelService()
|
||||
|
||||
try {
|
||||
|
@ -121,7 +121,7 @@ export const useLabelStore = defineStore('label', () => {
|
|||
}
|
||||
|
||||
async function createLabel(label: ILabel) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const labelService = new LabelService()
|
||||
|
||||
try {
|
||||
|
|
|
@ -95,7 +95,7 @@ export const useListStore = defineStore('list', () => {
|
|||
}
|
||||
|
||||
async function createList(list: IList) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
|
||||
try {
|
||||
|
@ -110,7 +110,7 @@ export const useListStore = defineStore('list', () => {
|
|||
}
|
||||
|
||||
async function updateList(list: IList) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
|
||||
try {
|
||||
|
@ -145,7 +145,7 @@ export const useListStore = defineStore('list', () => {
|
|||
}
|
||||
|
||||
async function deleteList(list: IList) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
|
||||
try {
|
||||
|
|
|
@ -148,7 +148,7 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
|||
}
|
||||
|
||||
async function loadNamespaces() {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const namespaceService = new NamespaceService()
|
||||
try {
|
||||
|
@ -182,7 +182,7 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
|||
}
|
||||
|
||||
async function deleteNamespace(namespace: INamespace) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const namespaceService = new NamespaceService()
|
||||
|
||||
try {
|
||||
|
@ -195,7 +195,7 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
|||
}
|
||||
|
||||
async function createNamespace(namespace: INamespace) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const namespaceService = new NamespaceService()
|
||||
|
||||
try {
|
||||
|
|
|
@ -104,7 +104,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
async function loadTasks(params) {
|
||||
const taskService = new TaskService()
|
||||
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
try {
|
||||
tasks.value = await taskService.getAll({}, params)
|
||||
baseStore.setHasTasks(tasks.value.length > 0)
|
||||
|
@ -115,7 +115,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
}
|
||||
|
||||
async function update(task: ITask) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const taskService = new TaskService()
|
||||
try {
|
||||
|
@ -172,7 +172,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
user: IUser,
|
||||
taskId: ITask['id']
|
||||
}) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
try {
|
||||
const taskAssigneeService = new TaskAssigneeService()
|
||||
|
@ -370,7 +370,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
} :
|
||||
Partial<ITask>,
|
||||
) {
|
||||
const cancel = setModuleLoading(this, setIsLoading)
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const parsedTask = parseTaskText(title, getQuickAddMagicMode())
|
||||
|
||||
const foundListId = await findListId({
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
// since $tablet is defined by bulma we can just define it after importing the utilities
|
||||
$mobile: math.div($tablet, 2);
|
||||
|
||||
@import "mixins";
|
||||
|
||||
$family-sans-serif: 'Open Sans', Helvetica, Arial, sans-serif;
|
||||
$vikunja-font: 'Quicksand', sans-serif;
|
||||
|
||||
|
@ -34,8 +32,4 @@ $switch-view-height: 2.69rem;
|
|||
|
||||
$navbar-height: 4rem;
|
||||
$navbar-width: 300px;
|
||||
$navbar-icon-width: 40px;
|
||||
|
||||
$lists-per-row: 5;
|
||||
$list-height: 150px;
|
||||
$list-spacing: 1rem;
|
||||
$navbar-icon-width: 40px;
|
|
@ -1,17 +1,4 @@
|
|||
// FIXME: should be in TaskDetailView.vue
|
||||
.link-share-container:not(.has-background) .task-view {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// FIXME: should be a prop of TaskDetailView.vue
|
||||
.modal-container .task-view {
|
||||
border-radius: $radius;
|
||||
padding: 1rem;
|
||||
color: var(--text);
|
||||
background-color: var(--site-background) !important;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
border-radius: 0;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/* Transitions */
|
||||
@mixin modal-transition() {
|
||||
.modal-enter,
|
||||
.modal-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
|
@ -26,7 +26,10 @@
|
|||
|
||||
.task-view {
|
||||
border-radius: $radius;
|
||||
margin: 0 1rem;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban .tasks {
|
||||
|
|
|
@ -77,11 +77,6 @@ h6 {
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
// FIXME: this should be moved in a Avatar component
|
||||
.image.is-avatar {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
button.table {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
|
@ -40,17 +40,11 @@
|
|||
</template>
|
||||
<div v-if="listHistory.length > 0" class="is-max-width-desktop has-text-left mt-4">
|
||||
<h3>{{ $t('home.lastViewed') }}</h3>
|
||||
<div class="is-flex list-cards-wrapper-2-rows">
|
||||
<list-card
|
||||
v-for="(l, k) in listHistory"
|
||||
:key="`l${k}`"
|
||||
:list="l"
|
||||
/>
|
||||
</div>
|
||||
<ListCardGrid :lists="listHistory" v-cy="'listCardGrid'" />
|
||||
</div>
|
||||
<ShowTasks
|
||||
v-if="hasLists"
|
||||
class="mt-4"
|
||||
class="show-tasks"
|
||||
:key="showTasksKey"
|
||||
/>
|
||||
</div>
|
||||
|
@ -61,7 +55,7 @@ import {ref, computed} from 'vue'
|
|||
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import ShowTasks from '@/views/tasks/ShowTasks.vue'
|
||||
import ListCard from '@/components/list/partials/list-card.vue'
|
||||
import ListCardGrid from '@/components/list/partials/ListCardGrid.vue'
|
||||
import AddTask from '@/components/tasks/add-task.vue'
|
||||
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
|
@ -113,14 +107,8 @@ function updateTaskList() {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-cards-wrapper-2-rows {
|
||||
flex-wrap: wrap;
|
||||
max-height: calc(#{$list-height * 2} + #{$list-spacing * 2} - 4px);
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
max-height: calc(#{$list-height * 4} + #{$list-spacing * 4} - 4px);
|
||||
}
|
||||
<style scoped lang="scss">
|
||||
.show-tasks {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
|
@ -94,9 +94,9 @@
|
|||
</div>
|
||||
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteLabel(labelToDelete)"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('task.label.delete.header') }}</span></template>
|
||||
|
||||
|
|
|
@ -206,20 +206,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
v-if="showBucketDeleteModal"
|
||||
@close="showBucketDeleteModal = false"
|
||||
@submit="deleteBucket()"
|
||||
>
|
||||
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
||||
<modal
|
||||
:enabled="showBucketDeleteModal"
|
||||
@close="showBucketDeleteModal = false"
|
||||
@submit="deleteBucket()"
|
||||
>
|
||||
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
||||
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<template #text>
|
||||
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
||||
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
</ListWrapper>
|
||||
|
@ -752,7 +750,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
}
|
||||
|
||||
:deep(.dropdown-trigger) {
|
||||
cursor: pointer;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
|
@ -792,6 +789,4 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
.move-card-leave-active {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -53,15 +53,13 @@
|
|||
class="loader-container is-max-width-desktop list-view"
|
||||
>
|
||||
<card :padding="false" :has-content="false" class="has-overflow">
|
||||
<template
|
||||
<add-task
|
||||
v-if="!list.isArchived && canWrite"
|
||||
>
|
||||
<add-task
|
||||
@taskAdded="updateTaskList"
|
||||
ref="addTaskRef"
|
||||
:default-position="firstNewPosition"
|
||||
/>
|
||||
</template>
|
||||
class="list-view__add-task"
|
||||
ref="addTaskRef"
|
||||
:default-position="firstNewPosition"
|
||||
@taskAdded="updateTaskList"
|
||||
/>
|
||||
|
||||
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
||||
{{ $t('list.list.empty') }}
|
||||
|
@ -70,59 +68,42 @@
|
|||
</ButtonLink>
|
||||
</nothing>
|
||||
|
||||
<div class="tasks-container" :class="{ 'has-task-edit-open': isTaskEdit }">
|
||||
<div
|
||||
class="tasks mt-0"
|
||||
v-if="tasks && tasks.length > 0"
|
||||
>
|
||||
<draggable
|
||||
v-bind="DRAG_OPTIONS"
|
||||
v-model="tasks"
|
||||
group="tasks"
|
||||
@start="() => drag = true"
|
||||
@end="saveTaskPosition"
|
||||
handle=".handle"
|
||||
|
||||
<draggable
|
||||
v-if="tasks && tasks.length > 0"
|
||||
v-bind="DRAG_OPTIONS"
|
||||
v-model="tasks"
|
||||
group="tasks"
|
||||
@start="() => drag = true"
|
||||
@end="saveTaskPosition"
|
||||
handle=".handle"
|
||||
:disabled="!canWrite"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: {
|
||||
tasks: true,
|
||||
'dragging-disabled': !canWrite || isAlphabeticalSorting
|
||||
},
|
||||
type: 'transition-group'
|
||||
}"
|
||||
>
|
||||
<template #item="{element: t}">
|
||||
<single-task-in-list
|
||||
:show-list-color="false"
|
||||
:disabled="!canWrite"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: { 'dragging-disabled': !canWrite || isAlphabeticalSorting },
|
||||
type: 'transition-group'
|
||||
}"
|
||||
:can-mark-as-done="canWrite || isSavedFilter(list)"
|
||||
:the-task="t"
|
||||
@taskUpdated="updateTasks"
|
||||
>
|
||||
<template #item="{element: t}">
|
||||
<single-task-in-list
|
||||
:show-list-color="false"
|
||||
:disabled="!canWrite"
|
||||
:can-mark-as-done="canWrite || isSavedFilter(list)"
|
||||
:the-task="t"
|
||||
@taskUpdated="updateTasks"
|
||||
>
|
||||
<template v-if="canWrite">
|
||||
<span class="icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<BaseButton
|
||||
@click="editTask(t.id)"
|
||||
class="icon settings"
|
||||
v-if="!list.isArchived"
|
||||
>
|
||||
<icon icon="pencil-alt"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</single-task-in-list>
|
||||
<template v-if="canWrite">
|
||||
<span class="icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<EditTask
|
||||
v-if="isTaskEdit"
|
||||
class="taskedit mt-0"
|
||||
:title="$t('list.list.editTask')"
|
||||
@close="closeTaskEditPane()"
|
||||
:shadow="false"
|
||||
:task="taskEditTask"
|
||||
/>
|
||||
</div>
|
||||
</single-task-in-list>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<Pagination
|
||||
:total-pages="totalPages"
|
||||
|
@ -139,14 +120,12 @@ export default { name: 'List' }
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, toRef, nextTick, onMounted, type PropType, watch} from 'vue'
|
||||
import {ref, computed, toRef, nextTick, onMounted, type PropType} from 'vue'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
|
||||
import ListWrapper from '@/components/list/ListWrapper.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import ButtonLink from '@/components/misc/ButtonLink.vue'
|
||||
import EditTask from '@/components/tasks/edit-task.vue'
|
||||
import AddTask from '@/components/tasks/add-task.vue'
|
||||
import SingleTaskInList from '@/components/tasks/partials/singleTaskInList.vue'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||
|
@ -196,23 +175,9 @@ const showTaskSearch = ref(false)
|
|||
const drag = ref(false)
|
||||
const DRAG_OPTIONS = {
|
||||
animation: 100,
|
||||
ghostClass: 'ghost',
|
||||
ghostClass: 'task-ghost',
|
||||
} as const
|
||||
|
||||
|
||||
const taskEditTask = ref<ITask | null>(null)
|
||||
const isTaskEdit = ref(false)
|
||||
|
||||
function closeTaskEditPane() {
|
||||
isTaskEdit.value = false
|
||||
taskEditTask.value = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.listId,
|
||||
closeTaskEditPane,
|
||||
)
|
||||
|
||||
const {
|
||||
tasks,
|
||||
loading,
|
||||
|
@ -296,11 +261,6 @@ function updateTaskList(task: ITask) {
|
|||
baseStore.setHasTasks(true)
|
||||
}
|
||||
|
||||
function editTask(id: ITask['id']) {
|
||||
taskEditTask.value = {...tasks.value.find(t => t.id === Number(id))}
|
||||
isTaskEdit.value = true
|
||||
}
|
||||
|
||||
function updateTasks(updatedTask: ITask) {
|
||||
for (const t in tasks.value) {
|
||||
if (tasks.value[t].id === updatedTask.id) {
|
||||
|
@ -339,54 +299,21 @@ function prepareFiltersAndLoadTasks() {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tasks-container {
|
||||
display: flex;
|
||||
.tasks {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
&.has-task-edit-open {
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
flex-direction: row;
|
||||
|
||||
.tasks {
|
||||
width: 66%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tasks {
|
||||
width: 100%;
|
||||
padding: .5rem;
|
||||
|
||||
.ghost {
|
||||
border-radius: $radius;
|
||||
background: var(--grey-100);
|
||||
border: 2px dashed var(--grey-300);
|
||||
|
||||
* {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskedit {
|
||||
width: 33%;
|
||||
margin-right: 1rem;
|
||||
margin-left: .5rem;
|
||||
min-height: calc(100% - 1rem);
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.task-ghost {
|
||||
border-radius: $radius;
|
||||
background: var(--grey-100);
|
||||
border: 2px dashed var(--grey-300);
|
||||
|
||||
* {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-view .task-add {
|
||||
.list-view__add-task {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
:key="im.id"
|
||||
:style="{'background-image': `url(${backgroundBlurHashes[im.id]})`}"
|
||||
>
|
||||
<transition name="fade">
|
||||
<CustomTransition name="fade">
|
||||
<BaseButton
|
||||
v-if="backgroundThumbs[im.id]"
|
||||
class="image-search__image-button"
|
||||
|
@ -51,7 +51,7 @@
|
|||
>
|
||||
<img class="image-search__image" :src="backgroundThumbs[im.id]" alt="" />
|
||||
</BaseButton>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
|
||||
<BaseButton
|
||||
:href="`https://unsplash.com/@${im.info.author}`"
|
||||
|
@ -102,7 +102,9 @@ 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 {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
|
|
@ -15,28 +15,28 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="namespaces.length === 0">
|
||||
<p v-if="namespaces.length === 0" class="has-text-centered has-text-grey mt-4 is-italic">
|
||||
{{ $t('namespace.noneAvailable') }}
|
||||
<router-link :to="{name: 'namespace.create'}">
|
||||
<BaseButton :to="{name: 'namespace.create'}">
|
||||
{{ $t('namespace.create.title') }}.
|
||||
</router-link>
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
||||
<section :key="`n${n.id}`" class="namespace" v-for="n in namespaces">
|
||||
<x-button
|
||||
v-if="n.id > 0 && n.lists.length > 0"
|
||||
:to="{name: 'list.create', params: {namespaceId: n.id}}"
|
||||
class="is-pulled-right"
|
||||
variant="secondary"
|
||||
v-if="n.id > 0 && n.lists.length > 0"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('list.create.header') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
v-if="n.isArchived"
|
||||
:to="{name: 'namespace.settings.archive', params: {id: n.id}}"
|
||||
class="is-pulled-right mr-4"
|
||||
variant="secondary"
|
||||
v-if="n.isArchived"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('namespace.unarchive') }}
|
||||
|
@ -44,26 +44,22 @@
|
|||
|
||||
<h2 class="namespace-title">
|
||||
<span v-cy="'namespace-title'">{{ getNamespaceTitle(n) }}</span>
|
||||
<span class="is-archived" v-if="n.isArchived">
|
||||
<span v-if="n.isArchived" class="is-archived">
|
||||
{{ $t('namespace.archived') }}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="n.lists.length === 0">
|
||||
<p v-if="n.lists.length === 0" class="has-text-centered has-text-grey mt-4 is-italic">
|
||||
{{ $t('namespace.noLists') }}
|
||||
<router-link :to="{name: 'list.create', params: {namespaceId: n.id}}">
|
||||
<BaseButton :to="{name: 'list.create', params: {namespaceId: n.id}}">
|
||||
{{ $t('namespace.createList') }}
|
||||
</router-link>
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
||||
<div class="lists">
|
||||
<list-card
|
||||
v-for="l in n.lists"
|
||||
:key="`l${l.id}`"
|
||||
:list="l"
|
||||
:show-archived="showArchived"
|
||||
/>
|
||||
</div>
|
||||
<ListCardGrid v-else
|
||||
:lists="n.lists"
|
||||
:show-archived="showArchived"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -72,8 +68,9 @@
|
|||
import {computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import ListCard from '@/components/list/partials/list-card.vue'
|
||||
import ListCardGrid from '@/components/list/partials/ListCardGrid.vue'
|
||||
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
@ -89,11 +86,10 @@ const showArchived = useStorage('showArchived', false)
|
|||
|
||||
const loading = computed(() => namespaceStore.isLoading)
|
||||
const namespaces = computed(() => {
|
||||
return namespaceStore.namespaces.filter(n => showArchived.value ? true : !n.isArchived)
|
||||
// return namespaceStore.namespaces.filter(n => showArchived.value ? true : !n.isArchived).map(n => {
|
||||
// n.lists = n.lists.filter(l => !l.isArchived)
|
||||
// return n
|
||||
// })
|
||||
return namespaceStore.namespaces.filter(namespace => showArchived.value
|
||||
? true
|
||||
: !namespace.isArchived,
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -121,10 +117,8 @@ const namespaces = computed(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.namespace {
|
||||
& + & {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.namespace:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.namespace-title {
|
||||
|
@ -142,9 +136,4 @@ const namespaces = computed(() => {
|
|||
background: var(--white-translucent);
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.lists {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
</style>
|
|
@ -2,8 +2,7 @@
|
|||
<div
|
||||
class="loader-container task-view-container"
|
||||
:class="{
|
||||
'is-loading': taskService.loading,
|
||||
'visible': visible,
|
||||
'is-loading': taskService.loading || !visible,
|
||||
'is-modal': isModal,
|
||||
}"
|
||||
>
|
||||
|
@ -42,7 +41,7 @@
|
|||
v-model="task.assignees"
|
||||
/>
|
||||
</div>
|
||||
<transition name="flash-background" appear>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.priority">
|
||||
<!-- Priority -->
|
||||
<div class="detail-title">
|
||||
|
@ -55,8 +54,8 @@
|
|||
:ref="e => setFieldRef('priority', e)"
|
||||
v-model="task.priority"/>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.dueDate">
|
||||
<!-- Due Date -->
|
||||
<div class="detail-title">
|
||||
|
@ -81,8 +80,8 @@
|
|||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.percentDone">
|
||||
<!-- Progress -->
|
||||
<div class="detail-title">
|
||||
|
@ -95,8 +94,8 @@
|
|||
:ref="e => setFieldRef('percentDone', e)"
|
||||
v-model="task.percentDone"/>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.startDate">
|
||||
<!-- Start Date -->
|
||||
<div class="detail-title">
|
||||
|
@ -122,8 +121,8 @@
|
|||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.endDate">
|
||||
<!-- End Date -->
|
||||
<div class="detail-title">
|
||||
|
@ -148,8 +147,8 @@
|
|||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.reminders">
|
||||
<!-- Reminders -->
|
||||
<div class="detail-title">
|
||||
|
@ -163,8 +162,8 @@
|
|||
@update:model-value="saveTask"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.repeatAfter">
|
||||
<!-- Repeat after -->
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
|
@ -191,8 +190,8 @@
|
|||
}"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="flash-background" appear>
|
||||
</CustomTransition>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.color">
|
||||
<!-- Color -->
|
||||
<div class="detail-title">
|
||||
|
@ -206,7 +205,7 @@
|
|||
@update:model-value="saveTask"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
|
||||
<!-- Labels -->
|
||||
|
@ -431,9 +430,9 @@
|
|||
</div>
|
||||
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteTask()"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('task.detail.delete.header') }}</span></template>
|
||||
|
||||
|
@ -446,7 +445,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, reactive, toRef, shallowReactive, computed, watch, watchEffect, nextTick, type PropType} from 'vue'
|
||||
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type PropType} from 'vue'
|
||||
import {useRouter, type RouteLocation} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
|
@ -481,6 +480,7 @@ import RelatedTasks from '@/components/tasks/partials/relatedTasks.vue'
|
|||
import Reminders from '@/components/tasks/partials/reminders.vue'
|
||||
import RepeatAfter from '@/components/tasks/partials/repeatAfter.vue'
|
||||
import TaskSubscription from '@/components/misc/subscription.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import {uploadFile} from '@/helpers/attachments'
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
|
@ -590,13 +590,14 @@ async function scrollToHeading() {
|
|||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
|
||||
async function loadTask(taskId: ITask['id']) {
|
||||
if (taskId === undefined) {
|
||||
// load task
|
||||
watch(taskId, async (id) => {
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Object.assign(task, await taskService.get({id: taskId}))
|
||||
Object.assign(task, await taskService.get({id}))
|
||||
attachmentStore.set(task.attachments)
|
||||
taskColor.value = task.hexColor
|
||||
setActiveFields()
|
||||
|
@ -605,9 +606,7 @@ async function loadTask(taskId: ITask['id']) {
|
|||
scrollToHeading()
|
||||
visible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => taskId.value !== undefined && loadTask(taskId.value))
|
||||
}, {immediate: true})
|
||||
|
||||
type FieldType =
|
||||
| 'assignees'
|
||||
|
@ -799,244 +798,217 @@ async function setPercentDone(percentDone: number) {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$flash-background-duration: 750ms;
|
||||
|
||||
.task-view {
|
||||
padding: 1rem;
|
||||
background-color: var(--site-background);
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
padding-bottom: 0;
|
||||
.task-view-container {
|
||||
// simulate sass lighten($primary, 30) by increasing lightness 30% to 73%
|
||||
--primary-light: hsla(var(--primary-h), var(--primary-s), 73%, var(--primary-a));
|
||||
padding-bottom: 0;
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--grey-500);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
a {
|
||||
color: var(--grey-800);
|
||||
}
|
||||
.task-view {
|
||||
padding-top: 1rem;
|
||||
padding-inline: .5rem;
|
||||
background-color: var(--site-background);
|
||||
|
||||
@media screen and (min-width: $desktop) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 .button {
|
||||
vertical-align: middle;
|
||||
.is-modal .task-view {
|
||||
border-radius: $radius;
|
||||
padding: 1rem;
|
||||
color: var(--text);
|
||||
background-color: var(--site-background) !important;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
border-radius: 0;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon.is-grey {
|
||||
color: var(--grey-400);
|
||||
}
|
||||
.task-view * {
|
||||
transition: opacity 50ms ease;
|
||||
}
|
||||
|
||||
.is-loading .task-view * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.subtitle {
|
||||
color: var(--grey-500);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.date-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
a {
|
||||
color: var(--grey-800);
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
h3 .button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon.is-grey {
|
||||
color: var(--grey-400);
|
||||
}
|
||||
.date-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
vertical-align: middle;
|
||||
padding-left: .5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.datepicker) {
|
||||
width: 100%;
|
||||
:deep(.datepicker) {
|
||||
width: 100%;
|
||||
|
||||
.show {
|
||||
color: var(--text);
|
||||
padding: .25rem .5rem;
|
||||
transition: background-color $transition;
|
||||
border-radius: $radius;
|
||||
display: block;
|
||||
margin: .1rem 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
.show {
|
||||
color: var(--text);
|
||||
padding: .25rem .5rem;
|
||||
transition: background-color $transition;
|
||||
border-radius: $radius;
|
||||
display: block;
|
||||
margin: .1rem 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled .show:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
padding-bottom: 0.75rem;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
.detail-title {
|
||||
display: block;
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.none {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Break after the 2nd element
|
||||
.column:nth-child(2n) {
|
||||
page-break-after: always; // CSS 2.1 syntax
|
||||
break-after: always; // New syntax
|
||||
}
|
||||
|
||||
&.labels-list,
|
||||
.assignees {
|
||||
:deep(.multiselect) {
|
||||
.input-wrapper {
|
||||
&:not(:focus-within):not(:hover) {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.details),
|
||||
:deep(.heading) {
|
||||
.input:not(.has-defaults),
|
||||
.textarea,
|
||||
.select:not(.has-defaults) select {
|
||||
cursor: pointer;
|
||||
transition: all $transition-duration;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-light);
|
||||
opacity: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: var(--scheme-main);
|
||||
border-color: var(--border);
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
cursor: text;
|
||||
border-color: var(--link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select:not(.has-defaults):after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.select:not(.has-defaults):hover:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-bottom: 0;
|
||||
|
||||
table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@media screen and (min-width: $tablet) {
|
||||
position: sticky;
|
||||
top: $navbar-height + 1.5rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
margin-bottom: .5rem;
|
||||
justify-content: left;
|
||||
|
||||
&.has-light-text {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-modal .action-buttons {
|
||||
// we need same top margin for the modal close button
|
||||
@media screen and (min-width: $tablet) {
|
||||
top: 6.5rem;
|
||||
}
|
||||
// this is the moment when the fixed close button is outside the modal
|
||||
// => we can fill up the space again
|
||||
@media screen and (min-width: calc(#{$desktop} + 84px)) {
|
||||
top: 0;
|
||||
&:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-summary {
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
||||
|
||||
.task-view-container {
|
||||
padding-bottom: 1rem;
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.task-view * {
|
||||
opacity: 0;
|
||||
transition: opacity 50ms ease;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
opacity: 1;
|
||||
|
||||
.task-view * {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.visible:not(.is-loading) .task-view * {
|
||||
opacity: 1;
|
||||
}
|
||||
&.disabled .show:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.task-view-container {
|
||||
// simulate sass lighten($primary, 30) by increasing lightness 30% to 73%
|
||||
--primary-light: hsla(var(--primary-h), var(--primary-s), 73%, var(--primary-a));
|
||||
.details {
|
||||
padding-bottom: 0.75rem;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
.detail-title {
|
||||
display: block;
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.none {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Break after the 2nd element
|
||||
.column:nth-child(2n) {
|
||||
page-break-after: always; // CSS 2.1 syntax
|
||||
break-after: always; // New syntax
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.flash-background-enter-from,
|
||||
.flash-background-enter-active {
|
||||
animation: flash-background $flash-background-duration ease 1;
|
||||
}
|
||||
|
||||
@keyframes flash-background {
|
||||
0% {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
100% {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes flash-background {
|
||||
0% {
|
||||
background: transparent;
|
||||
.details.labels-list,
|
||||
.assignees {
|
||||
:deep(.multiselect) {
|
||||
.input-wrapper {
|
||||
&:not(:focus-within):not(:hover) {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
:deep(.details),
|
||||
:deep(.heading) {
|
||||
.input:not(.has-defaults),
|
||||
.textarea,
|
||||
.select:not(.has-defaults) select {
|
||||
cursor: pointer;
|
||||
transition: all $transition-duration;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-light);
|
||||
opacity: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: var(--scheme-main);
|
||||
border-color: var(--border);
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
cursor: text;
|
||||
border-color: var(--link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select:not(.has-defaults):after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.select:not(.has-defaults):hover:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-bottom: 0;
|
||||
|
||||
table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@media screen and (min-width: $tablet) {
|
||||
position: sticky;
|
||||
top: $navbar-height + 1.5rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
margin-bottom: .5rem;
|
||||
justify-content: left;
|
||||
|
||||
&.has-light-text {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-modal .action-buttons {
|
||||
// we need same top margin for the modal close button
|
||||
@media screen and (min-width: $tablet) {
|
||||
top: 6.5rem;
|
||||
}
|
||||
// this is the moment when the fixed close button is outside the modal
|
||||
// => we can fill up the space again
|
||||
@media screen and (min-width: calc(#{$desktop} + 84px)) {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-summary {
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
@media print {
|
||||
width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -149,35 +149,32 @@
|
|||
</modal>
|
||||
|
||||
<!-- Team delete modal -->
|
||||
<transition name="modal">
|
||||
<modal
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteTeam()"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('team.edit.delete.header') }}</span></template>
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteTeam()"
|
||||
>
|
||||
<template #header><span>{{ $t('team.edit.delete.header') }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('team.edit.delete.text1') }}<br/>
|
||||
{{ $t('team.edit.delete.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('team.edit.delete.text1') }}<br/>
|
||||
{{ $t('team.edit.delete.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<!-- User delete modal -->
|
||||
<transition name="modal">
|
||||
<modal
|
||||
@close="showUserDeleteModal = false"
|
||||
@submit="deleteMember()"
|
||||
v-if="showUserDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('team.edit.deleteUser.header') }}</span></template>
|
||||
<modal
|
||||
:enabled="showUserDeleteModal"
|
||||
@close="showUserDeleteModal = false"
|
||||
@submit="deleteMember()"
|
||||
>
|
||||
<template #header><span>{{ $t('team.edit.deleteUser.header') }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('team.edit.deleteUser.text1') }}<br/>
|
||||
{{ $t('team.edit.deleteUser.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<template #text>
|
||||
<p>{{ $t('team.edit.deleteUser.text1') }}<br/>
|
||||
{{ $t('team.edit.deleteUser.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -339,6 +336,4 @@ async function leave() {
|
|||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
Loading…
Reference in New Issue