Compare commits

...

40 Commits

Author SHA1 Message Date
renovate e948678e42 chore(deps): update dependency eslint to v8.28.0 2022-11-19 01:04:33 +00:00
Dominik Pschenitschni 5ccedc6f67 [skip ci] Updated translations via Crowdin 2022-11-19 00:12:18 +00:00
Dominik Pschenitschni 74ad98de68 fix: icon offset and color 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni 3282f55c34 chore: add TODO comment 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni d9984b28f7 feat: move link color location together 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni 4fc7b9c67e feat: group navigation styles further 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni ff9efe7889 feat: outdent navigation logo styles 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni 66be0e6ac4 feat: undent and order navigation css 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni da8df8b667 feat: move avatar class to where it is used (#2725)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2725
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-18 13:30:41 +00:00
Dominik Pschenitschni 42e9f306e8
feat: grid for list cards 2022-11-18 14:04:20 +01:00
Angelo Delicato 4b47478440
feat: change list-content style (#91)
Co-authored-by: thelicato <thelicato@users.noreply.github.com>
Reviewed-on: https://github.com/go-vikunja/frontend/pull/91
2022-11-17 17:35:06 +01:00
Dominik Pschenitschni b42e4cca59 feat: more horizontal space on mobile (#2722)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2722
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 16:17:18 +00:00
Dominik Pschenitschni 33d4efecc4 feat: move useAutoHeightTextarea to composable (#2723)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2723
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:39:34 +00:00
Dominik Pschenitschni 45ec1623d5 feat: remove edit-task from list view (#2721)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2721
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:35:18 +00:00
Dominik Pschenitschni 8ef309243d feat: improve loadTask logic (#2715)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2715
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:31:21 +00:00
Dominik Pschenitschni 3aaacf4533 fix: remove vuex leftover from setModuleLoading (#2716)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2716
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:02:26 +00:00
renovate 0350e37fbb fix(deps): update sentry-javascript monorepo to v7.20.0 (#2720)
Reviewed-on: vikunja/frontend#2720
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-17 12:13:40 +00:00
renovate 244c436202 fix(deps): update dependency pinia to v2.0.24 2022-11-17 08:04:15 +00:00
drone 18d0c8ba2c [skip ci] Updated translations via Crowdin 2022-11-17 00:12:14 +00:00
kolaente 3891d5b876
feat: only automatically redirect to provider if the url contains ?redirectToProvider=true and it's the only one
Resolves https://github.com/go-vikunja/frontend/issues/90
2022-11-16 16:37:00 +01:00
Dominik Pschenitschni 98b38af43c feat: disable fullscreen for EasyMDE side-by-side mode (#2710)
Fixes https://github.com/go-vikunja/frontend/issues/92
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2710
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-16 14:37:03 +00:00
konrad 77ff0aa256 feat: move transition in component (#2694)
Reviewed-on: vikunja/frontend#2694
Reviewed-by: konrad <k@knt.li>
2022-11-16 14:36:17 +00:00
renovate 2ab26ee7c5 chore(deps): update pnpm to v7.16.1 (#2717)
Reviewed-on: vikunja/frontend#2717
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-16 14:16:20 +00:00
renovate 58f38bcfc3 fix(deps): update font awesome to v6.2.1 (#2712)
Reviewed-on: vikunja/frontend#2712
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-16 09:04:05 +00:00
renovate bcb5190365 chore(deps): update dependency cypress to v11.1.0 2022-11-15 21:06:37 +00:00
renovate c99d09c83e chore(deps): update dependency typescript to v4.9.3 2022-11-15 19:04:55 +00:00
renovate f49ea9752d chore(deps): update dependency vite to v3.2.4 2022-11-15 14:04:41 +00:00
renovate a56683cdc2 chore(deps): update dependency @vue/test-utils to v2.2.3 (#2707)
Reviewed-on: vikunja/frontend#2707
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-15 09:17:14 +00:00
renovate 7f4af63003 chore(deps): update dependency esbuild to v0.15.14 (#2706)
Reviewed-on: vikunja/frontend#2706
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-15 08:36:58 +00:00
Dominik Pschenitschni 8c44ed83e6
feat: use transition component everywhere 2022-11-14 22:08:54 +01:00
renovate b388677eaf fix(deps): update dependency ufo to v1 2022-11-14 18:42:12 +00:00
renovate bd7430b405 chore(deps): update typescript-eslint monorepo to v5.43.0 2022-11-14 18:05:00 +00:00
renovate 4baed8fe79 chore(deps): update pnpm to v7.16.0 (#2703)
Reviewed-on: vikunja/frontend#2703
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 16:01:36 +00:00
renovate fdbe4e8314 chore(deps): update dependency vitest to v0.25.2 (#2702)
Reviewed-on: vikunja/frontend#2702
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 16:00:57 +00:00
renovate 4a7f839449 chore(deps): update dependency postcss-preset-env to v7.8.3 (#2701)
Reviewed-on: vikunja/frontend#2701
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 10:15:12 +00:00
renovate c359f4d4dd chore(deps): update dependency netlify-cli to v12.1.1 (#2699)
Reviewed-on: vikunja/frontend#2699
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 09:16:36 +00:00
renovate 79d6212e48 chore(deps): update dependency happy-dom to v7.7.0 2022-11-14 08:54:27 +00:00
renovate e541213872 chore(deps): update dependency caniuse-lite to v1.0.30001431 2022-11-14 00:05:39 +00:00
Dominik Pschenitschni 631a19fa92
feat: move transition in own component 2022-11-12 19:32:39 +01:00
Dominik Pschenitschni fba402fcd0
feat: reduce TaskDetailView selector specificity 2022-11-12 19:29:20 +01:00
62 changed files with 1650 additions and 1762 deletions

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -54,8 +54,8 @@
</p>
<modal
@close="() => showHowItWorks = false"
:enabled="showHowItWorks"
@close="() => showHowItWorks = false"
transition-name="fade"
:overflow="true"
variant="hint-modal"

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunct
uploadImage,
imageUploadFunction,
minHeight: '150px',
sideBySideFullscreen: false,
toolbar: [
{
name: 'heading-1',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
}>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -26,7 +26,10 @@
.task-view {
border-radius: $radius;
margin: 0 1rem;
@media screen and (min-width: $tablet) {
margin-inline: 1rem;
}
}
.kanban .tasks {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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