Compare commits

...

34 Commits

Author SHA1 Message Date
kolaente 652d3c7384
chore: add archival note
continuous-integration/drone/push Build is passing Details
2024-02-07 15:03:24 +01:00
kolaente 447641c222 chore: apply lint fixes
continuous-integration/drone/push Build is passing Details
2024-02-07 12:23:09 +00:00
Dominik Pschenitschni 362be53a47 feat: use recommended vue-linting 2024-02-07 12:23:09 +00:00
renovate 46eabdfe6b fix(deps): update sentry-javascript monorepo to v7.100.1
continuous-integration/drone/push Build is passing Details
2024-02-07 11:53:00 +00:00
kolaente a0c5a464a5
feat(progress): less rounding
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-02-07 11:36:57 +01:00
kolaente e78ab476fc
chore(progress): cleanup unused css 2024-02-07 11:35:32 +01:00
kolaente aebb047d18
fix(progress): move customizations into progress bar component
continuous-integration/drone/pr Build is passing Details
2024-02-07 11:24:20 +01:00
Dominik Pschenitschni 7bb110b20e
feat: add ProgressBar component
continuous-integration/drone/pr Build is passing Details
2024-02-07 11:12:21 +01:00
renovate f148a43390 fix(deps): update dependency ufo to v1.4.0
continuous-integration/drone/push Build is passing Details
2024-02-07 10:08:12 +00:00
renovate aac70d3823 fix(deps): update dependency @kyvg/vue3-notification to v3.1.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-02-07 09:20:36 +00:00
kolaente 21126793ab
fix(test): make test assertion work again
continuous-integration/drone/push Build is passing Details
2024-02-06 23:13:38 +01:00
kolaente b057fb2784
fix(reminders): set reminder date on datepicker when editing a reminder
continuous-integration/drone/push Build is failing Details
Setting an actual reminder date (not a relative one) flowed only from the component to the outside when setting it. When editing it, the reminder date would not be populated, causing the datepicker date to stay at the current date.
2024-02-06 18:46:15 +01:00
kolaente 58c7da019d
fix(notifications): mark all notifications as read in ui directly when marking as read on the server
continuous-integration/drone/push Build is failing Details
This caused the notifications to stay on "unread" when marking them as read, making an unpleasant user experience
2024-02-06 18:34:42 +01:00
kolaente 70f48eaaca
fix(task): make sure the drag handle is shown as intended
continuous-integration/drone/push Build is failing Details
Due to a previous refactoring, the drag handle was always shown instead of only on hover. The css class was moved out of the task component, but its styles weren't

Related to #3934
2024-02-06 18:29:17 +01:00
kolaente 6cc75928d8
fix(task): remove default task color
continuous-integration/drone/push Build is failing Details
Previously, the task would use the default color. This was now removed, as this resulted in the default color not being visible on tasks.

Resolves https://github.com/go-vikunja/frontend/issues/135#issuecomment-1917576392
2024-02-06 18:18:44 +01:00
kolaente dc360d4a18
chore(editor): don't set editor content intitially
continuous-integration/drone/push Build is failing Details
2024-02-06 18:03:27 +01:00
kolaente 45ca0602f5
feat(editor): use primary color for currently selected node 2024-02-06 16:09:38 +01:00
kolaente 9d39ccf15c
fix(assignees): use correct amount of spacing in assignee selection
continuous-integration/drone/push Build is failing Details
2024-02-06 15:44:39 +01:00
kolaente 28e83325d7
fix(kanban): assignee spacing 2024-02-06 15:39:05 +01:00
kolaente aff48ddd9d
fix(kanban): bottom spacing of labels 2024-02-06 15:34:22 +01:00
kolaente 5b2a9a42c0
fix(gantt): correctly import languages from dayjs
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/error-in-gannt-with-spanish-language/1973/3
2024-02-05 21:57:21 +01:00
kolaente 45f5d522d1
docs: update readme
continuous-integration/drone/push Build is failing Details
Copied from https://github.com/go-vikunja/frontend/pull/146
2024-02-05 21:16:30 +01:00
renovate 4f27e4a477 fix(deps): update tiptap to v2.2.1 2024-02-05 10:37:28 +00:00
renovate d0dc86fd58 fix(deps): update dependency vue-i18n to v9.9.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-31 02:20:29 +00:00
renovate 0484923b8a fix(deps): update sentry-javascript monorepo to v7.99.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-30 17:18:57 +00:00
renovate 5f2fb01e90 fix(deps): update dependency floating-vue to v5.2.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-30 14:19:37 +00:00
renovate bd18524f36 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-30 00:19:43 +00:00
renovate 7375a87f2f fix(deps): update dependency @fortawesome/vue-fontawesome to v3.0.6
continuous-integration/drone/push Build is passing Details
2024-01-29 21:18:09 +00:00
renovate ccff276397 chore(deps): update pnpm to v8.15.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-29 20:19:25 +00:00
renovate 30b21fc11c fix(deps): update dependency floating-vue to v5.2.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-29 14:22:11 +00:00
renovate 7c98ddc20b fix(deps): update tiptap to v2.2.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-29 13:21:38 +00:00
renovate 6ba02a0f10 chore(deps): update pnpm to v8.15.0
continuous-integration/drone/push Build is passing Details
2024-01-29 07:32:55 +00:00
renovate 676d2b6215 chore(deps): update dependency @types/node to v20.11.10
continuous-integration/drone/push Build is passing Details
2024-01-29 07:32:26 +00:00
Frederick [Bot] 85e612451f chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-01-29 00:25:58 +00:00
161 changed files with 5120 additions and 3090 deletions

View File

@ -1,5 +1,5 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
'root': true,
@ -7,52 +7,54 @@ module.exports = {
'browser': true,
'es2022': true,
'node': true,
'vue/setup-compiler-macros': true,
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
],
'rules': {
'vue/html-quotes': [
'error',
'double',
],
'quotes': [
'error',
'single',
],
'comma-dangle': [
'error',
'always-multiline',
],
'semi': [
'error',
'never',
],
'quotes': ['error', 'single'],
'comma-dangle': ['error', 'always-multiline'],
'semi': ['error', 'never'],
// see https://segmentfault.com/q/1010000040813116/a-1020000041134455 (original in chinese)
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
'vue/v-on-event-hyphenation': ['warn', 'never', { 'autofix': true }],
'vue/multi-word-component-names': 'off',
'vue/multi-word-component-names': 0,
// disabled until we have support for reactivityTransform
// See https://github.com/vuejs/eslint-plugin-vue/issues/1948
// see also setting in `vite.config`
'vue/no-setup-props-destructure': 0,
// uncategorized rules:
'vue/component-api-style': ['error', ['script-setup']],
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-macros-order': 'error',
'vue/match-component-file-name': ['error', {
'extensions': ['.js', '.jsx', '.ts', '.tsx', '.vue'],
'shouldMatchCase': true,
}],
'vue/no-boolean-default': ['warn', 'default-false'],
'vue/match-component-import-name': 'error',
'vue/prefer-separate-static-class': 'warn',
'vue/padding-line-between-blocks': 'error',
'vue/next-tick-style': ['error', 'promise'],
'vue/block-lang': [
'error',
{ 'script': { 'lang': 'ts' } },
],
'vue/no-required-prop-with-default': ['error', { 'autofix': true }],
'vue/no-duplicate-attr-inheritance': 'error',
'vue/no-empty-component-block': 'error',
'vue/html-indent': ['error', 'tab'],
// vue3
'vue/no-ref-object-destructure': 'error',
},
'parser': 'vue-eslint-parser',
'parserOptions': {
'parser': '@typescript-eslint/parser',
'ecmaVersion': 2022,
'sourceType': 'module',
'ecmaVersion': 'latest',
},
'ignorePatterns': [
'*.test.*',
'cypress/*',
],
'globals': {
'defineProps': 'readonly',
},
}

View File

@ -1,3 +1,7 @@
# This repository was merged with the api and is now archived
You can find the new (old) code over on [vikunja/vikunja](https://kolaente.dev/vikunja/vikunja).
# Web frontend for Vikunja
> The todo app to organize your life.
@ -25,7 +29,7 @@ export DOCKER_BUILDKIT=1
docker build -t vikunja/frontend .
```
Refer to Refer [to multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for the different platform.
Refer to [multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for different platforms.
## Project setup

View File

@ -562,7 +562,7 @@ describe('Task', () => {
.click()
const today = new Date()
const day = today.toLocaleString('default', {day: '2-digit'})
const day = today.toLocaleString('default', {day: 'numeric'})
const month = today.toLocaleString('default', {month: 'short'})
const year = today.toLocaleString('default', {year: 'numeric'})
const date = `${day} ${month} ${year}, 12:00:00`
@ -605,7 +605,7 @@ describe('Task', () => {
.click()
const today = new Date()
const day = today.toLocaleString('default', {day: '2-digit'})
const day = today.toLocaleString('default', {day: 'numeric'})
const month = today.toLocaleString('default', {month: 'short'})
const year = today.toLocaleString('default', {year: 'numeric'})
const date = `${day} ${month} ${year}, 12:00:00`

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.14.3",
"packageManager": "pnpm@8.15.1",
"keywords": [
"todo",
"productivity",
@ -22,6 +22,7 @@
"gantt",
"kanban"
],
"type": "module",
"scripts": {
"serve": "vite",
"preview": "vite preview --port 4173",
@ -29,7 +30,8 @@
"build": "vite build && workbox copyLibraries dist/",
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
"build:dev": "vite build --mode development --outDir dist-dev/",
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
"lint": "eslint 'src/**/*.{js,ts,vue}'",
"lint:fix": "pnpm run lint --fix",
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
@ -48,46 +50,46 @@
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/vue-fontawesome": "3.0.5",
"@fortawesome/vue-fontawesome": "3.0.6",
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "2.0.0",
"@kyvg/vue3-notification": "3.1.3",
"@sentry/tracing": "7.98.0",
"@sentry/vue": "7.98.0",
"@tiptap/core": "2.1.16",
"@tiptap/extension-blockquote": "2.1.16",
"@tiptap/extension-bold": "2.1.16",
"@tiptap/extension-bullet-list": "2.1.16",
"@tiptap/extension-code": "2.1.16",
"@tiptap/extension-code-block-lowlight": "2.1.16",
"@tiptap/extension-document": "2.1.16",
"@tiptap/extension-dropcursor": "2.1.16",
"@tiptap/extension-gapcursor": "2.1.16",
"@tiptap/extension-hard-break": "2.1.16",
"@tiptap/extension-heading": "2.1.16",
"@tiptap/extension-history": "2.1.16",
"@tiptap/extension-horizontal-rule": "2.1.16",
"@tiptap/extension-image": "2.1.16",
"@tiptap/extension-italic": "2.1.16",
"@tiptap/extension-link": "2.1.16",
"@tiptap/extension-list-item": "2.1.16",
"@tiptap/extension-ordered-list": "2.1.16",
"@tiptap/extension-paragraph": "2.1.16",
"@tiptap/extension-placeholder": "2.1.16",
"@tiptap/extension-strike": "2.1.16",
"@tiptap/extension-table": "2.1.16",
"@tiptap/extension-table-cell": "2.1.16",
"@tiptap/extension-table-header": "2.1.16",
"@tiptap/extension-table-row": "2.1.16",
"@tiptap/extension-task-item": "2.1.16",
"@tiptap/extension-task-list": "2.1.16",
"@tiptap/extension-text": "2.1.16",
"@tiptap/extension-typography": "2.1.16",
"@tiptap/extension-underline": "2.1.16",
"@tiptap/pm": "2.1.16",
"@tiptap/suggestion": "2.1.16",
"@tiptap/vue-3": "2.1.16",
"@kyvg/vue3-notification": "3.1.4",
"@sentry/tracing": "7.100.1",
"@sentry/vue": "7.100.1",
"@tiptap/core": "2.2.1",
"@tiptap/extension-blockquote": "2.2.1",
"@tiptap/extension-bold": "2.2.1",
"@tiptap/extension-bullet-list": "2.2.1",
"@tiptap/extension-code": "2.2.1",
"@tiptap/extension-code-block-lowlight": "2.2.1",
"@tiptap/extension-document": "2.2.1",
"@tiptap/extension-dropcursor": "2.2.1",
"@tiptap/extension-gapcursor": "2.2.1",
"@tiptap/extension-hard-break": "2.2.1",
"@tiptap/extension-heading": "2.2.1",
"@tiptap/extension-history": "2.2.1",
"@tiptap/extension-horizontal-rule": "2.2.1",
"@tiptap/extension-image": "2.2.1",
"@tiptap/extension-italic": "2.2.1",
"@tiptap/extension-link": "2.2.1",
"@tiptap/extension-list-item": "2.2.1",
"@tiptap/extension-ordered-list": "2.2.1",
"@tiptap/extension-paragraph": "2.2.1",
"@tiptap/extension-placeholder": "2.2.1",
"@tiptap/extension-strike": "2.2.1",
"@tiptap/extension-table": "2.2.1",
"@tiptap/extension-table-cell": "2.2.1",
"@tiptap/extension-table-header": "2.2.1",
"@tiptap/extension-table-row": "2.2.1",
"@tiptap/extension-task-item": "2.2.1",
"@tiptap/extension-task-list": "2.2.1",
"@tiptap/extension-text": "2.2.1",
"@tiptap/extension-typography": "2.2.1",
"@tiptap/extension-underline": "2.2.1",
"@tiptap/pm": "2.2.1",
"@tiptap/suggestion": "2.2.1",
"@tiptap/vue-3": "2.2.1",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.7.2",
@ -102,7 +104,7 @@
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
"floating-vue": "5.2.0",
"floating-vue": "5.2.2",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
@ -112,11 +114,11 @@
"snake-case": "3.0.4",
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.3.2",
"ufo": "1.4.0",
"vue": "3.4.15",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.9.0",
"vue-i18n": "9.9.1",
"vue-router": "4.2.5",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
@ -136,11 +138,11 @@
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.11.8",
"@types/node": "20.11.10",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.7",
"@typescript-eslint/eslint-plugin": "6.19.1",
"@typescript-eslint/parser": "6.19.1",
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
"@vitejs/plugin-legacy": "5.3.0",
"@vitejs/plugin-vue": "5.0.3",
"@vue/eslint-config-typescript": "12.0.0",
@ -155,7 +157,7 @@
"esbuild": "0.20.0",
"eslint": "8.56.0",
"eslint-plugin-vue": "9.20.1",
"happy-dom": "13.3.1",
"happy-dom": "13.3.5",
"histoire": "0.17.9",
"postcss": "8.4.33",
"postcss-easing-gradients": "3.0.1",

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,23 @@
<template>
<ready>
<Ready>
<template v-if="authUser">
<TheNavigation/>
<content-auth/>
<TheNavigation />
<ContentAuth />
</template>
<content-link-share v-else-if="authLinkShare"/>
<no-auth-wrapper v-else>
<router-view/>
</no-auth-wrapper>
<ContentLinkShare v-else-if="authLinkShare" />
<NoAuthWrapper v-else>
<router-view />
</NoAuthWrapper>
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
<KeyboardShortcuts v-if="keyboardShortcutsActive" />
<Teleport to="body">
<AddToHomeScreen/>
<UpdateNotification/>
<Notification/>
<DemoMode/>
<AddToHomeScreen />
<UpdateNotification />
<Notification />
<DemoMode />
</Teleport>
</ready>
</Ready>
</template>
<script lang="ts" setup>

View File

@ -21,10 +21,16 @@ const state = reactive({
</script>
<template>
<Story :setup-app="setupApp" :layout="{ type: 'grid', width: '200px' }">
<Story
:setup-app="setupApp"
:layout="{ type: 'grid', width: '200px' }"
>
<Variant title="custom">
<template #controls>
<HstCheckbox v-model="state.disabled" title="Disabled" />
<HstCheckbox
v-model="state.disabled"
title="Disabled"
/>
</template>
<BaseButton :disabled="state.disabled">
Hello!

View File

@ -6,38 +6,39 @@
<template>
<div
v-if="disabled === true && (to !== undefined || href !== undefined)"
ref="button"
class="base-button"
:aria-disabled="disabled || undefined"
ref="button"
>
<slot/>
<slot />
</div>
<router-link
v-else-if="to !== undefined"
ref="button"
:to="to"
class="base-button"
ref="button"
>
<slot/>
<slot />
</router-link>
<a v-else-if="href !== undefined"
<a
v-else-if="href !== undefined"
ref="button"
class="base-button"
:href="href"
rel="noreferrer noopener nofollow"
target="_blank"
ref="button"
>
<slot/>
<slot />
</a>
<button
v-else
ref="button"
:type="type"
class="base-button base-button--type-button"
:disabled="disabled || undefined"
ref="button"
@click="(event: MouseEvent) => emit('click', event)"
>
<slot/>
<slot />
</button>
</template>

View File

@ -1,17 +1,26 @@
<template>
<div class="base-checkbox" v-cy="'checkbox'">
<div
v-cy="'checkbox'"
class="base-checkbox"
>
<input
type="checkbox"
:id="checkboxId"
type="checkbox"
class="is-sr-only"
:checked="modelValue"
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
:disabled="disabled || undefined"
/>
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
>
<slot name="label" :checkboxId="checkboxId">
<label :for="checkboxId" class="base-checkbox__label">
<slot/>
<slot
name="label"
:checkbox-id="checkboxId"
>
<label
:for="checkboxId"
class="base-checkbox__label"
>
<slot />
</label>
</slot>
</div>

View File

@ -1,27 +1,30 @@
<template>
<transition
name="expandable-slide"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<div
v-if="initialHeight"
class="expandable-initial-height"
:style="{ maxHeight: `${initialHeight}px` }"
:class="{ 'expandable-initial-height--expanded': open }"
>
<slot />
</div>
<div v-else-if="open" class="expandable">
<slot />
</div>
</transition>
<transition
name="expandable-slide"
@beforeEnter="beforeEnter"
@enter="enter"
@afterEnter="afterEnter"
@enterCancelled="enterCancelled"
@beforeLeave="beforeLeave"
@leave="leave"
@afterLeave="afterLeave"
@leaveCancelled="leaveCancelled"
>
<div
v-if="initialHeight"
class="expandable-initial-height"
:style="{ maxHeight: `${initialHeight}px` }"
:class="{ 'expandable-initial-height--expanded': open }"
>
<slot />
</div>
<div
v-else-if="open"
class="expandable"
>
<slot />
</div>
</transition>
</template>
<script setup lang="ts">

View File

@ -3,9 +3,9 @@ import datemathHelp from './datemathHelp.vue'
</script>
<template>
<Story>
<Variant title="Default">
<datemathHelp />
</Variant>
</Story>
<Story>
<Variant title="Default">
<datemathHelp />
</Variant>
</Story>
</template>

View File

@ -7,21 +7,29 @@
{{ $t('input.datemathHelp.intro') }}
</p>
<p>
<i18n-t keypath="input.datemathHelp.expression" scope="global">
<i18n-t
keypath="input.datemathHelp.expression"
scope="global"
>
<code>now</code>
<code>||</code>
</i18n-t>
</p>
<p>
<i18n-t keypath="input.datemathHelp.similar" scope="global">
<i18n-t
keypath="input.datemathHelp.similar"
scope="global"
>
<BaseButton
href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/"
target="_blank">
target="_blank"
>
Grafana
</BaseButton>
<BaseButton
href="https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math"
target="_blank">
target="_blank"
>
Elasticsearch
</BaseButton>
</i18n-t>
@ -35,76 +43,79 @@
<h3>{{ $t('input.datemathHelp.supportedUnits') }}</h3>
<table class="table">
<tbody>
<tr>
<td><code>s</code></td>
<td>{{ $t('input.datemathHelp.units.seconds') }}</td>
</tr>
<tr>
<td><code>m</code></td>
<td>{{ $t('input.datemathHelp.units.minutes') }}</td>
</tr>
<tr>
<td><code>h</code></td>
<td>{{ $t('input.datemathHelp.units.hours') }}</td>
</tr>
<tr>
<td><code>H</code></td>
<td>{{ $t('input.datemathHelp.units.hours') }}</td>
</tr>
<tr>
<td><code>d</code></td>
<td>{{ $t('input.datemathHelp.units.days') }}</td>
</tr>
<tr>
<td><code>w</code></td>
<td>{{ $t('input.datemathHelp.units.weeks') }}</td>
</tr>
<tr>
<td><code>M</code></td>
<td>{{ $t('input.datemathHelp.units.months') }}</td>
</tr>
<tr>
<td><code>y</code></td>
<td>{{ $t('input.datemathHelp.units.years') }}</td>
</tr>
<tr>
<td><code>s</code></td>
<td>{{ $t('input.datemathHelp.units.seconds') }}</td>
</tr>
<tr>
<td><code>m</code></td>
<td>{{ $t('input.datemathHelp.units.minutes') }}</td>
</tr>
<tr>
<td><code>h</code></td>
<td>{{ $t('input.datemathHelp.units.hours') }}</td>
</tr>
<tr>
<td><code>H</code></td>
<td>{{ $t('input.datemathHelp.units.hours') }}</td>
</tr>
<tr>
<td><code>d</code></td>
<td>{{ $t('input.datemathHelp.units.days') }}</td>
</tr>
<tr>
<td><code>w</code></td>
<td>{{ $t('input.datemathHelp.units.weeks') }}</td>
</tr>
<tr>
<td><code>M</code></td>
<td>{{ $t('input.datemathHelp.units.months') }}</td>
</tr>
<tr>
<td><code>y</code></td>
<td>{{ $t('input.datemathHelp.units.years') }}</td>
</tr>
</tbody>
</table>
<h3>{{ $t('input.datemathHelp.someExamples') }}</h3>
<table class="table">
<tbody>
<tr>
<td><code>now</code></td>
<td>{{ $t('input.datemathHelp.examples.now') }}</td>
</tr>
<tr>
<td><code>now+24h</code></td>
<td>{{ $t('input.datemathHelp.examples.in24h') }}</td>
</tr>
<tr>
<td><code>now/d</code></td>
<td>{{ $t('input.datemathHelp.examples.today') }}</td>
</tr>
<tr>
<td><code>now/w</code></td>
<td>{{ $t('input.datemathHelp.examples.beginningOfThisWeek') }}</td>
</tr>
<tr>
<td><code>now/w+1w</code></td>
<td>{{ $t('input.datemathHelp.examples.endOfThisWeek') }}</td>
</tr>
<tr>
<td><code>now+30d</code></td>
<td>{{ $t('input.datemathHelp.examples.in30Days') }}</td>
</tr>
<tr>
<td><code>{{ exampleDate }}||+1M/d</code></td>
<td>
<i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global">
<strong>{{ exampleDate }}</strong>
</i18n-t>
</td>
</tr>
<tr>
<td><code>now</code></td>
<td>{{ $t('input.datemathHelp.examples.now') }}</td>
</tr>
<tr>
<td><code>now+24h</code></td>
<td>{{ $t('input.datemathHelp.examples.in24h') }}</td>
</tr>
<tr>
<td><code>now/d</code></td>
<td>{{ $t('input.datemathHelp.examples.today') }}</td>
</tr>
<tr>
<td><code>now/w</code></td>
<td>{{ $t('input.datemathHelp.examples.beginningOfThisWeek') }}</td>
</tr>
<tr>
<td><code>now/w+1w</code></td>
<td>{{ $t('input.datemathHelp.examples.endOfThisWeek') }}</td>
</tr>
<tr>
<td><code>now+30d</code></td>
<td>{{ $t('input.datemathHelp.examples.in30Days') }}</td>
</tr>
<tr>
<td><code>{{ exampleDate }}||+1M/d</code></td>
<td>
<i18n-t
keypath="input.datemathHelp.examples.datePlusMonth"
scope="global"
>
<strong>{{ exampleDate }}</strong>
</i18n-t>
</td>
</tr>
</tbody>
</table>
</card>

View File

@ -1,20 +1,31 @@
<template>
<div class="datepicker-with-range-container">
<popup>
<Popup>
<template #trigger="{toggle}">
<slot name="trigger" :toggle="toggle" :buttonText="buttonText"></slot>
<slot
name="trigger"
:toggle="toggle"
:button-text="buttonText"
/>
</template>
<template #content="{isOpen}">
<div class="datepicker-with-range" :class="{'is-open': isOpen}">
<div
class="datepicker-with-range"
:class="{'is-open': isOpen}"
>
<div class="selections">
<BaseButton @click="setDateRange(null)" :class="{'is-active': customRangeActive}">
<BaseButton
:class="{'is-active': customRangeActive}"
@click="setDateRange(null)"
>
{{ $t('misc.custom') }}
</BaseButton>
<BaseButton
v-for="(value, text) in DATE_RANGES"
:key="text"
:class="{'is-active': from === value[0] && to === value[1]}"
@click="setDateRange(value)"
:class="{'is-active': from === value[0] && to === value[1]}">
>
{{ $t(`input.datepickerRange.ranges.${text}`) }}
</BaseButton>
</div>
@ -23,10 +34,18 @@
{{ $t('input.datepickerRange.from') }}
<div class="field has-addons">
<div class="control is-fullwidth">
<input class="input" type="text" v-model="from"/>
<input
v-model="from"
class="input"
type="text"
>
</div>
<div class="control">
<x-button icon="calendar" variant="secondary" data-toggle/>
<x-button
icon="calendar"
variant="secondary"
data-toggle
/>
</div>
</div>
</label>
@ -34,38 +53,49 @@
{{ $t('input.datepickerRange.to') }}
<div class="field has-addons">
<div class="control is-fullwidth">
<input class="input" type="text" v-model="to"/>
<input
v-model="to"
class="input"
type="text"
>
</div>
<div class="control">
<x-button icon="calendar" variant="secondary" data-toggle/>
<x-button
icon="calendar"
variant="secondary"
data-toggle
/>
</div>
</div>
</label>
<flat-pickr
:config="flatPickerConfig"
v-model="flatpickrRange"
:config="flatPickerConfig"
/>
<p>
{{ $t('input.datemathHelp.canuse') }}
<BaseButton class="has-text-primary" @click="showHowItWorks = true">
<BaseButton
class="has-text-primary"
@click="showHowItWorks = true"
>
{{ $t('input.datemathHelp.learnhow') }}
</BaseButton>
</p>
<modal
:enabled="showHowItWorks"
@close="() => showHowItWorks = false"
transition-name="fade"
:overflow="true"
variant="hint-modal"
@close="() => showHowItWorks = false"
>
<DatemathHelp/>
<DatemathHelp />
</modal>
</div>
</div>
</template>
</popup>
</Popup>
</div>
</template>
@ -83,15 +113,16 @@ import BaseButton from '@/components/base/BaseButton.vue'
import DatemathHelp from '@/components/date/datemathHelp.vue'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
required: false,
},
})
const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'})
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,

View File

@ -4,12 +4,18 @@
class="add-to-home-screen"
:class="{'has-update-available': hasUpdateAvailable}"
>
<icon icon="arrow-up-from-bracket" class="add-icon"/>
<icon
icon="arrow-up-from-bracket"
class="add-icon"
/>
<p>
{{ $t('home.addToHomeScreen') }}
</p>
<BaseButton @click="() => hideMessage = true" class="hide-button">
<icon icon="x"/>
<BaseButton
class="hide-button"
@click="() => hideMessage = true"
>
<icon icon="x" />
</BaseButton>
</div>
</template>

View File

@ -17,8 +17,11 @@ const enabled = computed(() => configStore.demoModeEnabled && !hide.value)
{{ $t('demo.title') }}
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
</p>
<BaseButton @click="() => hide = true" class="hide-button">
<icon icon="times"/>
<BaseButton
class="hide-button"
@click="() => hide = true"
>
<icon icon="times" />
</BaseButton>
</div>
</template>

View File

@ -15,8 +15,17 @@ const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
<template>
<div>
<Logo v-if="!CustomLogo" alt="Vikunja" class="logo" />
<img v-show="CustomLogo" :src="CustomLogo" alt="Vikunja" class="logo" />
<Logo
v-if="!CustomLogo"
alt="Vikunja"
class="logo"
/>
<img
v-show="CustomLogo"
:src="CustomLogo"
alt="Vikunja"
class="logo"
>
</div>
</template>

View File

@ -1,11 +1,11 @@
<template>
<BaseButton
class="menu-show-button"
@click="baseStore.toggleMenu()"
@shortkey="() => baseStore.toggleMenu()"
v-shortcut="'Mod+e'"
class="menu-show-button"
:title="$t('keyboardShortcuts.toggleMenu')"
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
@click="baseStore.toggleMenu()"
@shortkey="() => baseStore.toggleMenu()"
/>
</template>

View File

@ -1,7 +1,11 @@
<template>
<BaseButton class="menu-bottom-link" :href="poweredByUrl" target="_blank">
{{ $t('misc.poweredBy') }}
</BaseButton>
<BaseButton
class="menu-bottom-link"
:href="poweredByUrl"
target="_blank"
>
{{ $t('misc.poweredBy') }}
</BaseButton>
</template>
<script setup lang="ts">

View File

@ -2,10 +2,8 @@
<draggable
v-model="availableProjects"
animation="100"
ghostClass="ghost"
ghost-class="ghost"
group="projects"
@start="() => drag = true"
@end="saveProjectPosition"
handle=".handle"
tag="menu"
item-key="id"
@ -19,6 +17,8 @@
{ 'dragging-disabled': !canEditOrder }
],
}"
@start="() => drag = true"
@end="saveProjectPosition"
>
<template #item="{element: project}">
<ProjectsNavigationItem

View File

@ -6,10 +6,13 @@
<div>
<BaseButton
v-if="canCollapse && childProjects?.length > 0"
@click="childProjectsOpen = !childProjectsOpen"
class="collapse-project-button"
@click="childProjectsOpen = !childProjectsOpen"
>
<icon icon="chevron-down" :class="{ 'project-is-collapsed': !childProjectsOpen }"/>
<icon
icon="chevron-down"
:class="{ 'project-is-collapsed': !childProjectsOpen }"
/>
</BaseButton>
<BaseButton
:to="{ name: 'project.index', params: { projectId: project.id} }"
@ -19,21 +22,27 @@
<span
v-if="!canCollapse || childProjects?.length === 0"
class="collapse-project-button-placeholder"
></span>
<div class="color-bubble-handle-wrapper" :class="{'is-draggable': project.id > 0}">
/>
<div
class="color-bubble-handle-wrapper"
:class="{'is-draggable': project.id > 0}"
>
<ColorBubble
v-if="project.hexColor !== ''"
:color="project.hexColor"
/>
<span v-else-if="project.id < -1" class="saved-filter-icon icon menu-item-icon">
<icon icon="filter"/>
<span
v-else-if="project.id < -1"
class="saved-filter-icon icon menu-item-icon"
>
<icon icon="filter" />
</span>
<span
v-if="project.id > 0"
class="icon menu-item-icon handle lines-handle"
class="icon menu-item-icon handle"
:class="{'has-color-bubble': project.hexColor !== ''}"
>
<icon icon="grip-lines"/>
<icon icon="grip-lines" />
</span>
</div>
<span class="project-menu-title">{{ getProjectTitle(project) }}</span>
@ -44,7 +53,7 @@
:class="{'is-favorite': project.isFavorite}"
@click="projectStore.toggleProjectFavorite(project)"
>
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
</BaseButton>
<ProjectSettingsDropdown
class="menu-list-dropdown"
@ -52,8 +61,14 @@
:level="level"
>
<template #trigger="{toggleOpen}">
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
<BaseButton
class="menu-list-dropdown-trigger"
@click="toggleOpen"
>
<icon
icon="ellipsis-h"
class="icon"
/>
</BaseButton>
</template>
</ProjectSettingsDropdown>

View File

@ -1,66 +1,110 @@
<template>
<header :class="{ 'has-background': background, 'menu-active': menuActive }" aria-label="main navigation"
class="navbar d-print-none">
<router-link :to="{ name: 'home' }" class="logo-link">
<Logo width="164" height="48" />
<header
:class="{ 'has-background': background, 'menu-active': menuActive }"
aria-label="main navigation"
class="navbar d-print-none"
>
<router-link
:to="{ name: 'home' }"
class="logo-link"
>
<Logo
width="164"
height="48"
/>
</router-link>
<MenuButton class="menu-button" />
<div v-if="currentProject?.id" class="project-title-wrapper">
<div
v-if="currentProject?.id"
class="project-title-wrapper"
>
<h1 class="project-title">
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
</h1>
<BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
<BaseButton
:to="{ name: 'project.info', params: { projectId: currentProject.id } }"
class="project-title-button"
>
<icon icon="circle-info" />
</BaseButton>
<project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1"
class="project-title-dropdown" :project="currentProject">
<ProjectSettingsDropdown
v-if="canWriteCurrentProject && currentProject.id !== -1"
class="project-title-dropdown"
:project="currentProject"
>
<template #trigger="{ toggleOpen }">
<BaseButton class="project-title-button" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon" />
<BaseButton
class="project-title-button"
@click="toggleOpen"
>
<icon
icon="ellipsis-h"
class="icon"
/>
</BaseButton>
</template>
</project-settings-dropdown>
</ProjectSettingsDropdown>
</div>
<div class="navbar-end">
<OpenQuickActions/>
<OpenQuickActions />
<Notifications />
<dropdown>
<Dropdown>
<template #trigger="{ toggleOpen, open }">
<BaseButton class="username-dropdown-trigger" @click="toggleOpen" variant="secondary" :shadow="false">
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40" />
<BaseButton
class="username-dropdown-trigger"
variant="secondary"
:shadow="false"
@click="toggleOpen"
>
<img
:src="authStore.avatarUrl"
alt=""
class="avatar"
width="40"
height="40"
>
<span class="username">{{ authStore.userDisplayName }}</span>
<span class="icon is-small" :style="{
transform: open ? 'rotate(180deg)' : 'rotate(0)',
}">
<span
class="icon is-small"
:style="{
transform: open ? 'rotate(180deg)' : 'rotate(0)',
}"
>
<icon icon="chevron-down" />
</span>
</BaseButton>
</template>
<dropdown-item :to="{ name: 'user.settings' }">
<DropdownItem :to="{ name: 'user.settings' }">
{{ $t('user.settings.title') }}
</dropdown-item>
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
</DropdownItem>
<DropdownItem
v-if="imprintUrl"
:href="imprintUrl"
>
{{ $t('navigation.imprint') }}
</dropdown-item>
<dropdown-item v-if="privacyPolicyUrl" :href="privacyPolicyUrl">
</DropdownItem>
<DropdownItem
v-if="privacyPolicyUrl"
:href="privacyPolicyUrl"
>
{{ $t('navigation.privacy') }}
</dropdown-item>
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
</DropdownItem>
<DropdownItem @click="baseStore.setKeyboardShortcutsActive(true)">
{{ $t('keyboardShortcuts.title') }}
</dropdown-item>
<dropdown-item :to="{ name: 'about' }">
</DropdownItem>
<DropdownItem :to="{ name: 'about' }">
{{ $t('about.title') }}
</dropdown-item>
<dropdown-item @click="authStore.logout()">
</DropdownItem>
<DropdownItem @click="authStore.logout()">
{{ $t('user.auth.logout') }}
</dropdown-item>
</dropdown>
</DropdownItem>
</Dropdown>
</div>
</header>
</template>

View File

@ -1,11 +1,16 @@
<template>
<div class="update-notification" v-if="updateAvailable">
<p class="update-notification__message">{{ $t('update.available') }}</p>
<div
v-if="updateAvailable"
class="update-notification"
>
<p class="update-notification__message">
{{ $t('update.available') }}
</p>
<x-button
@click="refreshApp()"
:shadow="false"
:wrap="false"
>
@click="refreshApp()"
>
{{ $t('update.do') }}
</x-button>
</div>

View File

@ -2,10 +2,10 @@
<div class="content-auth">
<BaseButton
v-show="menuActive"
@click="baseStore.setMenuActive(false)"
class="menu-hide-button d-print-none"
@click="baseStore.setMenuActive(false)"
>
<icon icon="times"/>
<icon icon="times" />
</BaseButton>
<div
class="app-container"
@ -15,8 +15,9 @@
<div
:class="{'is-visible': background}"
class="app-container-background background-fade-in d-print-none"
:style="{'background-image': background && `url(${background})`}"></div>
<navigation class="d-print-none"/>
:style="{'background-image': background && `url(${background})`}"
/>
<Navigation class="d-print-none" />
<main
class="app-content"
:class="[
@ -26,33 +27,36 @@
>
<BaseButton
v-show="menuActive"
@click="baseStore.setMenuActive(false)"
class="mobile-overlay d-print-none"
@click="baseStore.setMenuActive(false)"
/>
<quick-actions/>
<QuickActions />
<router-view :route="routeWithModal" v-slot="{ Component }">
<router-view
v-slot="{ Component }"
:route="routeWithModal"
>
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
<component :is="Component"/>
<component :is="Component" />
</keep-alive>
</router-view>
<modal
:enabled="typeof currentModal !== 'undefined'"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"
@close="closeModal()"
>
<component :is="currentModal"/>
<component :is="currentModal" />
</modal>
<BaseButton
v-shortcut="'?'"
class="keyboard-shortcuts-button d-print-none"
@click="showKeyboardShortcuts()"
v-shortcut="'?'"
>
<icon icon="keyboard"/>
<icon icon="keyboard" />
</BaseButton>
</main>
</div>

View File

@ -6,16 +6,20 @@
>
<div class="container has-text-centered link-share-view">
<div class="column is-10 is-offset-1">
<Logo class="logo" v-if="logoVisible"/>
<Logo
v-if="logoVisible"
class="logo"
/>
<h1
:class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentProject?.title === '' ? '0': '1' }"
class="title">
class="title"
>
{{ currentProject?.title === '' ? $t('misc.loading') : currentProject?.title }}
</h1>
<div class="box has-text-left view">
<router-view/>
<PoweredByLink/>
<router-view />
<PoweredByLink />
</div>
</div>
</div>

View File

@ -1,46 +1,70 @@
<template>
<aside :class="{'is-active': baseStore.menuActive}" class="menu-container">
<aside
:class="{'is-active': baseStore.menuActive}"
class="menu-container"
>
<nav class="menu top-menu">
<router-link :to="{name: 'home'}" class="logo">
<Logo width="164" height="48"/>
<router-link
:to="{name: 'home'}"
class="logo"
>
<Logo
width="164"
height="48"
/>
</router-link>
<menu class="menu-list other-menu-items">
<li>
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
<router-link
v-shortcut="'g o'"
:to="{ name: 'home'}"
>
<span class="menu-item-icon icon">
<icon icon="calendar"/>
<icon icon="calendar" />
</span>
{{ $t('navigation.overview') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
<router-link
v-shortcut="'g u'"
:to="{ name: 'tasks.range'}"
>
<span class="menu-item-icon icon">
<icon :icon="['far', 'calendar-alt']"/>
<icon :icon="['far', 'calendar-alt']" />
</span>
{{ $t('navigation.upcoming') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'projects.index'}" v-shortcut="'g p'">
<router-link
v-shortcut="'g p'"
:to="{ name: 'projects.index'}"
>
<span class="menu-item-icon icon">
<icon icon="layer-group"/>
<icon icon="layer-group" />
</span>
{{ $t('project.projects') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
<router-link
v-shortcut="'g a'"
:to="{ name: 'labels.index'}"
>
<span class="menu-item-icon icon">
<icon icon="tags"/>
<icon icon="tags" />
</span>
{{ $t('label.title') }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
<router-link
v-shortcut="'g m'"
:to="{ name: 'teams.index'}"
>
<span class="menu-item-icon icon">
<icon icon="users"/>
<icon icon="users" />
</span>
{{ $t('team.title') }}
</router-link>
@ -53,7 +77,10 @@
variant="small"
/>
<template v-else>
<nav class="menu" v-if="favoriteProjects">
<nav
v-if="favoriteProjects"
class="menu"
>
<ProjectsNavigation
:model-value="favoriteProjects"
:can-edit-order="false"
@ -61,7 +88,10 @@
/>
</nav>
<nav class="menu" v-if="savedFilterProjects">
<nav
v-if="savedFilterProjects"
class="menu"
>
<ProjectsNavigation
:model-value="savedFilterProjects"
:can-edit-order="false"
@ -79,7 +109,7 @@
</nav>
</template>
<PoweredByLink/>
<PoweredByLink />
</aside>
</template>

View File

@ -6,19 +6,28 @@ import XButton from './button.vue'
<template>
<Story :layout="{ type: 'grid', width: '200px' }">
<Variant title="primary">
<XButton @click="logEvent('Click', $event)" variant="primary">
<XButton
variant="primary"
@click="logEvent('Click', $event)"
>
Order pizza!
</XButton>
</Variant>
<Variant title="secondary">
<XButton @click="logEvent('Click', $event)" variant="secondary">
<XButton
variant="secondary"
@click="logEvent('Click', $event)"
>
Order spaghetti!
</XButton>
</Variant>
<Variant title="tertiary">
<XButton @click="logEvent('Click', $event)" variant="tertiary">
<XButton
variant="tertiary"
@click="logEvent('Click', $event)"
>
Order tortellini!
</XButton>
</Variant>

View File

@ -1,36 +1,67 @@
<template>
<div class="color-picker-container">
<datalist :id="colorListID">
<option v-for="defaultColor in defaultColors" :key="defaultColor" :value="defaultColor" />
<option
v-for="defaultColor in defaultColors"
:key="defaultColor"
:value="defaultColor"
/>
</datalist>
<div class="picker">
<input
v-model="color"
class="picker__input"
type="color"
v-model="color"
:list="colorListID"
:class="{'is-empty': isEmpty}"
/>
<svg class="picker__pattern" v-show="isEmpty" viewBox="0 0 22 22" fill="fff">
<pattern id="checker" width="11" height="11" patternUnits="userSpaceOnUse" fill="FFF">
<rect fill="#cccccc" x="0" width="5.5" height="5.5" y="0"></rect>
<rect fill="#cccccc" x="5.5" width="5.5" height="5.5" y="5.5"></rect>
>
<svg
v-show="isEmpty"
class="picker__pattern"
viewBox="0 0 22 22"
fill="fff"
>
<pattern
id="checker"
width="11"
height="11"
patternUnits="userSpaceOnUse"
fill="FFF"
>
<rect
fill="#cccccc"
x="0"
width="5.5"
height="5.5"
y="0"
/>
<rect
fill="#cccccc"
x="5.5"
width="5.5"
height="5.5"
y="5.5"
/>
</pattern>
<rect width="22" height="22" fill="url(#checker)"></rect>
<rect
width="22"
height="22"
fill="url(#checker)"
/>
</svg>
</div>
<x-button
<XButton
v-if="!isEmpty"
:disabled="isEmpty"
@click="reset"
class="is-small ml-2"
:shadow="false"
variant="secondary"
@click="reset"
>
{{ $t('input.resetColor') }}
</x-button>
</XButton>
</div>
</template>
@ -39,6 +70,14 @@ import {computed, ref, watch} from 'vue'
import {createRandomID} from '@/helpers/randomId'
import XButton from '@/components/input/button.vue'
const {
modelValue,
} = defineProps<{
modelValue: string,
}>()
const emit = defineEmits(['update:modelValue'])
const DEFAULT_COLORS = [
'#1973ff',
'#7F23FF',
@ -53,14 +92,6 @@ const lastChangeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const defaultColors = ref(DEFAULT_COLORS)
const colorListID = ref(createRandomID())
const {
modelValue,
} = defineProps<{
modelValue: string,
}>()
const emit = defineEmits(['update:modelValue'])
watch(
() => modelValue,
(newValue) => {

View File

@ -1,5 +1,5 @@
<template>
<multiselect
<Multiselect
v-model="selectedProjects"
:search-results="foundProjects"
:loading="projectService.loading"

View File

@ -1,5 +1,5 @@
<template>
<multiselect
<Multiselect
v-model="selectedUsers"
:search-results="foundUsers"
:loading="userService.loading"

View File

@ -1,6 +1,6 @@
<template>
<BaseButton class="simple-button">
<slot/>
<slot />
</BaseButton>
</template>

View File

@ -18,7 +18,10 @@
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : undefined}"
/>
<span class="icon is-small" v-else>
<span
v-else
class="icon is-small"
>
<icon
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : undefined}"
@ -38,7 +41,7 @@ const BUTTON_TYPES_MAP = {
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
export default { name: 'x-button' }
export default { name: 'XButton' }
</script>
<script setup lang="ts">

View File

@ -1,22 +1,29 @@
<template>
<div class="datepicker">
<SimpleButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
<SimpleButton
class="show"
:disabled="disabled || undefined"
@click.stop="toggleDatePopup"
>
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
</SimpleButton>
<CustomTransition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
<div
v-if="show"
ref="datepickerPopup"
class="datepicker-popup"
>
<DatepickerInline
v-model="date"
@update:model-value="updateData"
@update:modelValue="updateData"
/>
<x-button
v-cy="'closeDatepicker'"
class="datepicker__close-button"
:shadow="false"
@click="close"
v-cy="'closeDatepicker'"
>
{{ $t('misc.confirm') }}
</x-button>
@ -56,7 +63,7 @@ const props = defineProps({
},
})
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
const emit = defineEmits(['update:modelValue', 'close', 'closeOnChange'])
const date = ref<Date | null>()
const show = ref(false)
@ -108,7 +115,7 @@ function close() {
emit('close', changed.value)
if (changed.value) {
changed.value = false
emit('close-on-change', changed.value)
emit('closeOnChange', changed.value)
}
}, 200)
}

View File

@ -4,7 +4,7 @@
class="datepicker__quick-select-date"
@click.stop="setDate('today')"
>
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
<span class="icon"><icon :icon="['far', 'calendar-alt']" /></span>
<span class="text">
<span>{{ $t('input.datepicker.today') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
@ -14,7 +14,7 @@
class="datepicker__quick-select-date"
@click.stop="setDate('tomorrow')"
>
<span class="icon"><icon :icon="['far', 'sun']"/></span>
<span class="icon"><icon :icon="['far', 'sun']" /></span>
<span class="text">
<span>{{ $t('input.datepicker.tomorrow') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
@ -24,7 +24,7 @@
class="datepicker__quick-select-date"
@click.stop="setDate('nextMonday')"
>
<span class="icon"><icon icon="coffee"/></span>
<span class="icon"><icon icon="coffee" /></span>
<span class="text">
<span>{{ $t('input.datepicker.nextMonday') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
@ -34,7 +34,7 @@
class="datepicker__quick-select-date"
@click.stop="setDate('thisWeekend')"
>
<span class="icon"><icon icon="cocktail"/></span>
<span class="icon"><icon icon="cocktail" /></span>
<span class="text">
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
@ -44,7 +44,7 @@
class="datepicker__quick-select-date"
@click.stop="setDate('laterThisWeek')"
>
<span class="icon"><icon icon="chess-knight"/></span>
<span class="icon"><icon icon="chess-knight" /></span>
<span class="text">
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
@ -54,7 +54,7 @@
class="datepicker__quick-select-date"
@click.stop="setDate('nextWeek')"
>
<span class="icon"><icon icon="forward"/></span>
<span class="icon"><icon icon="forward" /></span>
<span class="text">
<span>{{ $t('input.datepicker.nextWeek') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
@ -63,8 +63,8 @@
<div class="flatpickr-container">
<flat-pickr
:config="flatPickerConfig"
v-model="flatPickrDate"
:config="flatPickerConfig"
/>
</div>
</template>

View File

@ -2,26 +2,30 @@
<div class="items">
<template v-if="items.length">
<button
class="item"
:class="{ 'is-selected': index === selectedIndex }"
v-for="(item, index) in items"
:key="index"
class="item"
:class="{ 'is-selected': index === selectedIndex }"
@click="selectItem(index)"
>
<icon :icon="item.icon"/>
<icon :icon="item.icon" />
<div class="description">
<p>{{ item.title }}</p>
<p>{{ item.description }}</p>
</div>
</button>
</template>
<div class="item" v-else>
<div
v-else
class="item"
>
No result
</div>
</div>
</template>
<script>
<script lang="ts">
/* eslint-disable vue/component-api-style */
export default {
props: {
items: {

View File

@ -2,35 +2,35 @@
<div class="editor-toolbar">
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
v-tooltip="$t('input.editor.heading1')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-header']"/>
<icon :icon="['fa', 'fa-header']" />
<span class="icon__lower-text">1</span>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
v-tooltip="$t('input.editor.heading2')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-header']"/>
<icon :icon="['fa', 'fa-header']" />
<span class="icon__lower-text">2</span>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
v-tooltip="$t('input.editor.heading3')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-header']"/>
<icon :icon="['fa', 'fa-header']" />
<span class="icon__lower-text">3</span>
</span>
</BaseButton>
@ -38,167 +38,167 @@
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
v-tooltip="$t('input.editor.bold')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('bold') }"
@click="editor.chain().focus().toggleBold().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-bold']"/>
<icon :icon="['fa', 'fa-bold']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
v-tooltip="$t('input.editor.italic')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('italic') }"
@click="editor.chain().focus().toggleItalic().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-italic']"/>
<icon :icon="['fa', 'fa-italic']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': editor.isActive('underline') }"
v-tooltip="$t('input.editor.underline')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('underline') }"
@click="editor.chain().focus().toggleUnderline().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-underline']"/>
<icon :icon="['fa', 'fa-underline']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
v-tooltip="$t('input.editor.strikethrough')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('strike') }"
@click="editor.chain().focus().toggleStrike().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-strikethrough']"/>
<icon :icon="['fa', 'fa-strikethrough']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
v-tooltip="$t('input.editor.code')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('codeBlock') }"
@click="editor.chain().focus().toggleCodeBlock().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-code']"/>
<icon :icon="['fa', 'fa-code']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
v-tooltip="$t('input.editor.quote')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('blockquote') }"
@click="editor.chain().focus().toggleBlockquote().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-quote-right']"/>
<icon :icon="['fa', 'fa-quote-right']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
v-tooltip="$t('input.editor.bulletList')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('bulletList') }"
@click="editor.chain().focus().toggleBulletList().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ul']"/>
<icon :icon="['fa', 'fa-list-ul']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
v-tooltip="$t('input.editor.orderedList')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('orderedList') }"
@click="editor.chain().focus().toggleOrderedList().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ol']"/>
<icon :icon="['fa', 'fa-list-ol']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleTaskList().run()"
:class="{ 'is-active': editor.isActive('taskList') }"
v-tooltip="$t('input.editor.taskList')"
>
<span class="icon">
<icon icon="fa-list-check"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="openImagePicker"
v-tooltip="$t('input.editor.image')"
>
<span class="icon">
<icon icon="fa-image"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="setLink"
:class="{ 'is-active': editor.isActive('taskList') }"
@click="editor.chain().focus().toggleTaskList().run()"
>
<span class="icon">
<icon icon="fa-list-check" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
v-tooltip="$t('input.editor.image')"
class="editor-toolbar__button"
@click="openImagePicker"
>
<span class="icon">
<icon icon="fa-image" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
v-tooltip="$t('input.editor.link')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('link') }"
title="set link"
v-tooltip="$t('input.editor.link')"
@click="setLink"
>
<span class="icon">
<icon :icon="['fa', 'fa-link']"/>
<icon :icon="['fa', 'fa-link']" />
</span>
</BaseButton>
<BaseButton
v-tooltip="$t('input.editor.text')"
class="editor-toolbar__button"
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
title="paragraph"
v-tooltip="$t('input.editor.text')"
@click="editor.chain().focus().setParagraph().run()"
>
<span class="icon">
<icon :icon="['fa', 'fa-paragraph']"/>
<icon :icon="['fa', 'fa-paragraph']" />
</span>
</BaseButton>
<BaseButton
v-tooltip="$t('input.editor.horizontalRule')"
class="editor-toolbar__button"
@click="editor.chain().focus().setHorizontalRule().run()"
v-tooltip="$t('input.editor.horizontalRule')"
>
<span class="icon">
<icon :icon="['fa', 'fa-ruler-horizontal']"/>
<icon :icon="['fa', 'fa-ruler-horizontal']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
v-tooltip="$t('input.editor.undo')"
class="editor-toolbar__button"
@click="editor.chain().focus().undo().run()"
v-tooltip="$t('input.editor.undo')"
>
<span class="icon">
<icon :icon="['fa', 'fa-undo']"/>
<icon :icon="['fa', 'fa-undo']" />
</span>
</BaseButton>
<BaseButton
v-tooltip="$t('input.editor.redo')"
class="editor-toolbar__button"
@click="editor.chain().focus().redo().run()"
v-tooltip="$t('input.editor.redo')"
>
<span class="icon">
<icon :icon="['fa', 'fa-redo']"/>
<icon :icon="['fa', 'fa-redo']" />
</span>
</BaseButton>
</div>
@ -206,16 +206,19 @@
<div class="editor-toolbar__segment">
<!-- table -->
<BaseButton
class="editor-toolbar__button"
@click="toggleTableMode"
:class="{ 'is-active': editor.isActive('table') }"
v-tooltip="$t('input.editor.table.title')"
class="editor-toolbar__button"
:class="{ 'is-active': editor.isActive('table') }"
@click="toggleTableMode"
>
<span class="icon">
<icon :icon="['fa', 'fa-table']"/>
<icon :icon="['fa', 'fa-table']" />
</span>
</BaseButton>
<div v-if="tableMode" class="editor-toolbar__table-buttons">
<div
v-if="tableMode"
class="editor-toolbar__table-buttons"
>
<BaseButton
class="editor-toolbar__button"
@click="
@ -230,99 +233,99 @@
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addColumnBefore().run()"
:disabled="!editor.can().addColumnBefore"
@click="editor.chain().focus().addColumnBefore().run()"
>
{{ $t('input.editor.table.addColumnBefore') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addColumnAfter().run()"
:disabled="!editor.can().addColumnAfter"
@click="editor.chain().focus().addColumnAfter().run()"
>
{{ $t('input.editor.table.addColumnAfter') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteColumn().run()"
:disabled="!editor.can().deleteColumn"
@click="editor.chain().focus().deleteColumn().run()"
>
{{ $t('input.editor.table.deleteColumn') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addRowBefore().run()"
:disabled="!editor.can().addRowBefore"
@click="editor.chain().focus().addRowBefore().run()"
>
{{ $t('input.editor.table.addRowBefore') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addRowAfter().run()"
:disabled="!editor.can().addRowAfter"
@click="editor.chain().focus().addRowAfter().run()"
>
{{ $t('input.editor.table.addRowAfter') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteRow().run()"
:disabled="!editor.can().deleteRow"
@click="editor.chain().focus().deleteRow().run()"
>
{{ $t('input.editor.table.deleteRow') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteTable().run()"
:disabled="!editor.can().deleteTable"
@click="editor.chain().focus().deleteTable().run()"
>
{{ $t('input.editor.table.deleteTable') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().mergeCells().run()"
:disabled="!editor.can().mergeCells"
@click="editor.chain().focus().mergeCells().run()"
>
{{ $t('input.editor.table.mergeCells') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().splitCell().run()"
:disabled="!editor.can().splitCell"
@click="editor.chain().focus().splitCell().run()"
>
{{ $t('input.editor.table.splitCell') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderColumn().run()"
:disabled="!editor.can().toggleHeaderColumn"
@click="editor.chain().focus().toggleHeaderColumn().run()"
>
{{ $t('input.editor.table.toggleHeaderColumn') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderRow().run()"
:disabled="!editor.can().toggleHeaderRow"
@click="editor.chain().focus().toggleHeaderRow().run()"
>
{{ $t('input.editor.table.toggleHeaderRow') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderCell().run()"
:disabled="!editor.can().toggleHeaderCell"
@click="editor.chain().focus().toggleHeaderCell().run()"
>
{{ $t('input.editor.table.toggleHeaderCell') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().mergeOrSplit().run()"
:disabled="!editor.can().mergeOrSplit"
@click="editor.chain().focus().mergeOrSplit().run()"
>
{{ $t('input.editor.table.mergeOrSplit') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().fixTables().run()"
:disabled="!editor.can().fixTables"
@click="editor.chain().focus().fixTables().run()"
>
{{ $t('input.editor.table.fixTables') }}
</BaseButton>

View File

@ -1,5 +1,8 @@
<template>
<div class="tiptap" ref="tiptapInstanceRef">
<div
ref="tiptapInstanceRef"
class="tiptap"
>
<EditorToolbar
v-if="editor && isEditing"
:editor="editor"
@ -11,56 +14,56 @@
class="editor-bubble__wrapper"
>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
v-tooltip="$t('input.editor.bold')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('bold') }"
@click="editor.chain().focus().toggleBold().run()"
>
<icon :icon="['fa', 'fa-bold']"/>
<icon :icon="['fa', 'fa-bold']" />
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
v-tooltip="$t('input.editor.italic')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('italic') }"
@click="editor.chain().focus().toggleItalic().run()"
>
<icon :icon="['fa', 'fa-italic']"/>
<icon :icon="['fa', 'fa-italic']" />
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': editor.isActive('underline') }"
v-tooltip="$t('input.editor.underline')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('underline') }"
@click="editor.chain().focus().toggleUnderline().run()"
>
<icon :icon="['fa', 'fa-underline']"/>
<icon :icon="['fa', 'fa-underline']" />
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
v-tooltip="$t('input.editor.strikethrough')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('strike') }"
@click="editor.chain().focus().toggleStrike().run()"
>
<icon :icon="['fa', 'fa-strikethrough']"/>
<icon :icon="['fa', 'fa-strikethrough']" />
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
v-tooltip="$t('input.editor.code')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('code') }"
@click="editor.chain().focus().toggleCode().run()"
>
<icon :icon="['fa', 'fa-code']"/>
<icon :icon="['fa', 'fa-code']" />
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="setLink"
:class="{ 'is-active': editor.isActive('link') }"
v-tooltip="$t('input.editor.link')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('link') }"
@click="setLink"
>
<icon :icon="['fa', 'fa-link']"/>
<icon :icon="['fa', 'fa-link']" />
</BaseButton>
</BubbleMenu>
<editor-content
<EditorContent
class="tiptap__editor"
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
:editor="editor"
@ -69,52 +72,66 @@
<input
v-if="isEditing"
type="file"
id="tiptap__image-upload"
class="is-hidden"
ref="uploadInputRef"
type="file"
class="is-hidden"
@change="addImage"
/>
>
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length === 0 && !isEditing && isEditEnabled">
<ul
v-if="bottomActions.length === 0 && !isEditing && isEditEnabled"
class="tiptap__editor-actions d-print-none"
>
<li>
<BaseButton
class="done-edit"
@click="setEdit"
class="done-edit">
>
{{ $t('input.editor.edit') }}
</BaseButton>
</li>
</ul>
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length > 0">
<ul
v-if="bottomActions.length > 0"
class="tiptap__editor-actions d-print-none"
>
<li v-if="isEditing && showSave">
<BaseButton
class="done-edit"
@click="bubbleSave"
class="done-edit">
>
{{ $t('misc.save') }}
</BaseButton>
</li>
<li v-if="!isEditing">
<BaseButton
class="done-edit"
@click="setEdit"
class="done-edit">
>
{{ $t('input.editor.edit') }}
</BaseButton>
</li>
<li v-for="(action, k) in bottomActions" :key="k">
<BaseButton @click="action.action">{{ action.title }}</BaseButton>
<li
v-for="(action, k) in bottomActions"
:key="k"
>
<BaseButton @click="action.action">
{{ action.title }}
</BaseButton>
</li>
</ul>
<x-button
<XButton
v-else-if="isEditing && showSave"
v-cy="'saveEditor'"
class="mt-4"
@click="bubbleSave"
variant="secondary"
:shadow="false"
v-cy="'saveEditor'"
:disabled="!contentHasChanged"
@click="bubbleSave"
>
{{ $t('misc.save') }}
</x-button>
</XButton>
</div>
</template>
@ -177,6 +194,26 @@ import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
const {
modelValue,
uploadCallback,
isEditEnabled = true,
bottomActions = [],
showSave = false,
placeholder = '',
editShortcut = '',
} = defineProps<{
modelValue: string,
uploadCallback?: UploadCallback,
isEditEnabled?: boolean,
bottomActions?: BottomAction[],
showSave?: boolean,
placeholder?: string,
editShortcut?: string,
}>()
const emit = defineEmits(['update:modelValue', 'save'])
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
const {t} = useI18n()
@ -270,26 +307,6 @@ const CustomImage = Image.extend({
type Mode = 'edit' | 'preview'
const {
modelValue,
uploadCallback,
isEditEnabled = true,
bottomActions = [],
showSave = false,
placeholder = '',
editShortcut = '',
} = defineProps<{
modelValue: string,
uploadCallback?: UploadCallback,
isEditEnabled?: boolean,
bottomActions?: BottomAction[],
showSave?: boolean,
placeholder?: string,
editShortcut?: string,
}>()
const emit = defineEmits(['update:modelValue', 'save'])
const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const contentHasChanged = ref<boolean>(false)
@ -304,7 +321,7 @@ watch(
)
const editor = useEditor({
content: modelValue,
// eslint-disable-next-line vue/no-ref-object-destructure
editable: isEditing.value,
extensions: [
// Starterkit:
@ -746,7 +763,7 @@ watch(
height: auto;
&.ProseMirror-selectednode {
outline: 3px solid #68cef8;
outline: 3px solid var(--primary);
}
}

View File

@ -26,32 +26,42 @@ const withoutInitialState = ref<boolean | undefined>()
</FancyCheckbox>
Visualisation
<input type="checkbox" v-model="isChecked">
<input
v-model="isChecked"
type="checkbox"
>
{{ isChecked }}
</Variant>
<Variant title="Enabled Initially">
<FancyCheckbox
:disabled="isDisabled"
v-model="isCheckedInitiallyEnabled"
:disabled="isDisabled"
>
We want you to use this option
</FancyCheckbox>
Visualisation
<input type="checkbox" v-model="isCheckedInitiallyEnabled">
<input
v-model="isCheckedInitiallyEnabled"
type="checkbox"
>
{{ isCheckedInitiallyEnabled }}
</Variant>
<Variant title="Disabled">
<FancyCheckbox
disabled
:modelValue="isCheckedDisabled"
@update:model-value="logEvent('Setting disabled: This should never happen', $event)"
:model-value="isCheckedDisabled"
@update:modelValue="logEvent('Setting disabled: This should never happen', $event)"
>
You can't change this
</FancyCheckbox>
Visualisation
<input type="checkbox" v-model="isCheckedDisabled" disabled>
<input
v-model="isCheckedDisabled"
type="checkbox"
disabled
>
{{ isCheckedDisabled }}
</Variant>
@ -64,7 +74,11 @@ const withoutInitialState = ref<boolean | undefined>()
</FancyCheckbox>
Visualisation
<input type="checkbox" v-model="withoutInitialState" disabled>
<input
v-model="withoutInitialState"
type="checkbox"
disabled
>
{{ withoutInitialState }}
</Variant>
</Story>

View File

@ -7,11 +7,14 @@
}"
:disabled="disabled"
:model-value="modelValue"
@update:model-value="value => emit('update:modelValue', value)"
@update:modelValue="value => emit('update:modelValue', value)"
>
<CheckboxIcon class="fancycheckbox__icon" />
<span v-if="$slots.default" class="fancycheckbox__content">
<slot/>
<span
v-if="$slots.default"
class="fancycheckbox__content"
>
<slot />
</span>
</BaseCheckbox>
</template>

View File

@ -1,12 +1,15 @@
<template>
<div
ref="multiselectRoot"
class="multiselect"
:class="{'has-search-results': searchResultsVisible}"
ref="multiselectRoot"
tabindex="-1"
@focus="focus"
>
<div class="control" :class="{'is-loading': loading || localLoading}">
<div
class="control"
:class="{'is-loading': loading || localLoading}"
>
<div
class="input-wrapper input"
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable}"
@ -18,51 +21,67 @@
:remove="remove"
>
<template v-for="(item, key) in internalValue">
<slot name="tag" :item="item">
<span :key="`item${key}`" class="tag ml-2 mt-2">
<slot
name="tag"
:item="item"
>
<span
:key="`item${key}`"
class="tag ml-2 mt-2"
>
{{ label !== '' ? item[label] : item }}
<BaseButton @click="() => remove(item)" class="delete is-small"></BaseButton>
<BaseButton
class="delete is-small"
@click="() => remove(item)"
/>
</span>
</slot>
</template>
</slot>
<input
ref="searchInput"
v-model="query"
type="text"
class="input"
v-model="query"
@keyup="search"
@keyup.enter.exact.prevent="() => createOrSelectOnEnter()"
:placeholder="placeholder"
@keydown.down.exact.prevent="() => preSelect(0)"
ref="searchInput"
@focus="handleFocus"
:autocomplete="autocompleteEnabled ? undefined : 'off'"
:spellcheck="autocompleteEnabled ? undefined : 'false'"
/>
@keyup="search"
@keyup.enter.exact.prevent="() => createOrSelectOnEnter()"
@keydown.down.exact.prevent="() => preSelect(0)"
@focus="handleFocus"
>
<BaseButton
v-if="removalAvailable"
class="removal-button"
@click="resetSelectedValue"
>
<icon icon="times"/>
<icon icon="times" />
</BaseButton>
</div>
</div>
<CustomTransition name="fade">
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
<div
v-if="searchResultsVisible"
class="search-results"
:class="{'search-results-inline': inline}"
>
<BaseButton
class="search-result-button is-fullwidth"
v-for="(data, index) in filteredSearchResults"
:key="index"
:ref="(el) => setResult(el, index)"
class="search-result-button is-fullwidth"
@keydown.up.prevent="() => preSelect(index - 1)"
@keydown.down.prevent="() => preSelect(index + 1)"
@click.prevent.stop="() => select(data)"
>
<span>
<slot name="searchResult" :option="data">
<slot
name="searchResult"
:option="data"
>
<span class="search-result">{{ label !== '' ? data[label] : data }}</span>
</slot>
</span>
@ -73,15 +92,18 @@
<BaseButton
v-if="creatableAvailable"
class="search-result-button is-fullwidth"
:ref="(el) => setResult(el, filteredSearchResults.length)"
class="search-result-button is-fullwidth"
@keydown.up.prevent="() => preSelect(filteredSearchResults.length - 1)"
@keydown.down.prevent="() => preSelect(filteredSearchResults.length + 1)"
@keyup.enter.prevent="create"
@click.prevent.stop="create"
>
<span>
<slot name="searchResult" :option="query">
<slot
name="searchResult"
:option="query"
>
<span class="search-result">
{{ query }}
</span>
@ -107,16 +129,6 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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.
if (label !== '') {
return elem[label] === query
}
return elem === query
}
const props = defineProps({
/**
* When true, shows a loading spinner
@ -245,6 +257,16 @@ const emit = defineEmits<{
(e: 'remove', value: null): void
}>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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.
if (label !== '') {
return elem[label] === query
}
return elem === query
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query = ref<string | { [key: string]: any }>('')
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)

View File

@ -1,27 +1,31 @@
<template>
<div class="password-field">
<input
class="input"
id="password"
class="input"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
:type="passwordFieldType"
autocomplete="current-password"
@keyup.enter="e => $emit('submit', e)"
:tabindex="props.tabindex"
@keyup.enter="e => $emit('submit', e)"
@focusout="validate"
@input="handleInput"
/>
>
<BaseButton
@click="togglePasswordFieldType"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
class="password-field-type-toggle"
:aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
@click="togglePasswordFieldType"
>
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'" />
</BaseButton>
</div>
<p class="help is-danger" v-if="!isValid">
<p
v-if="!isValid"
class="help is-danger"
>
{{ $t('user.auth.passwordRequired') }}
</p>
</template>

View File

@ -1,5 +1,7 @@
<template>
<BaseButton class="button-link"><slot/></BaseButton>
<BaseButton class="button-link">
<slot />
</BaseButton>
</template>
<script setup lang="ts">

View File

@ -1,5 +1,5 @@
<template>
<div
<div
v-if="isDone"
class="is-done"
:class="{ 'is-done--small': variant === 'small' }"

View File

@ -31,10 +31,10 @@ function openQuickActions() {
<template>
<BaseButton
@click="openQuickActions"
class="trigger-button"
:title="$t('keyboardShortcuts.quickSearch')"
@click="openQuickActions"
>
<icon icon="search"/>
<icon icon="search" />
</BaseButton>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import {ref} from 'vue'
import ProgressBar from './ProgressBar.vue'
const value = ref(50)
</script>
<template>
<Story>
<Variant title="Default">
<ProgressBar :value="value" />
</Variant>
</Story>
</template>

View File

@ -0,0 +1,139 @@
<template>
<progress
class="progress-bar"
:class="{
'is-small': isSmall,
'is-primary': isPrimary,
}"
:value="value"
max="100"
>
{{ value }}%
</progress>
</template>
<script setup lang="ts">
import {defineProps} from 'vue'
defineProps({
value: {
type: Number,
required: true,
},
isSmall: {
type: Boolean,
default: false,
},
isPrimary: {
type: Boolean,
required: false,
},
})
</script>
<style lang="scss" scoped>
.progress-bar {
--progress-height: #{$size-normal};
--progress-bar-background-color: var(--border-light, #{$border-light});
--progress-value-background-color: var(--grey-500, #{$text});
--progress-border-radius: #{$radius};
--progress-indeterminate-duration: 1.5s;
appearance: none;
border: none;
border-radius: var(--progress-border-radius);
height: var(--progress-height);
overflow: hidden;
padding: 0;
min-width: 6vw;
width: 50px;
margin: 0 .5rem 0 0;
flex: 3 1 auto;
&::-moz-progress-bar,
&::-webkit-progress-value {
background: var(--progress-value-background-color);
}
@media screen and (max-width: $tablet) {
margin: 0.5rem 0 0 0;
order: 1;
width: 100%;
}
&::-webkit-progress-bar {
background-color: var(--progress-bar-background-color);
}
&::-webkit-progress-value {
background-color: var(--progress-value-background-color);
}
&::-moz-progress-bar {
background-color: var(--progress-value-background-color);
}
&::-ms-fill {
background-color: var(--progress-value-background-color);
border: none;
}
// Colors
@each $name, $pair in $colors {
$color: nth($pair, 1);
&.is-#{$name} {
--progress-value-background-color: var(--#{$name}, #{$color});
&:indeterminate {
background-image: linear-gradient(
to right,
var(--#{$name}, #{$color}) 30%,
var(--progress-bar-background-color) 30%
);
}
}
}
&:indeterminate {
animation-duration: var(--progress-indeterminate-duration);
animation-iteration-count: infinite;
animation-name: moveIndeterminate;
animation-timing-function: linear;
background-color: var(--progress-bar-background-color);
background-image: linear-gradient(
to right,
var(--text, #{$text}) 30%,
var(--progress-bar-background-color) 30%
);
background-position: top left;
background-repeat: no-repeat;
background-size: 150% 150%;
&::-webkit-progress-bar {
background-color: transparent;
}
&::-moz-progress-bar {
background-color: transparent;
}
&::-ms-fill {
animation-name: none;
}
}
&.is-small {
--progress-height: #{$size-small};
}
}
@keyframes moveIndeterminate {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
</style>

View File

@ -1,38 +1,62 @@
<template>
<div class="api-config">
<div v-if="configureApi">
<label class="label" for="api-url">{{ $t('apiConfig.url') }}</label>
<label
class="label"
for="api-url"
>{{ $t('apiConfig.url') }}</label>
<div class="field has-addons">
<div class="control is-expanded">
<input
class="input"
id="api-url"
v-model="apiUrl"
v-focus
class="input"
:placeholder="$t('apiConfig.urlPlaceholder')"
required
type="url"
v-focus
v-model="apiUrl"
@keyup.enter="setApiUrl"
/>
>
</div>
<div class="control">
<x-button @click="setApiUrl" :disabled="apiUrl === '' || undefined">
<x-button
:disabled="apiUrl === '' || undefined"
@click="setApiUrl"
>
{{ $t('apiConfig.change') }}
</x-button>
</div>
</div>
</div>
<div class="api-url-info" v-else>
<i18n-t keypath="apiConfig.use" scope="global">
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
<div
v-else
class="api-url-info"
>
<i18n-t
keypath="apiConfig.use"
scope="global"
>
<span
v-tooltip="apiUrl"
class="url"
> {{ apiDomain }} </span>
</i18n-t>
<br/>
<ButtonLink class="api-config__change-button" @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</ButtonLink>
<br>
<ButtonLink
class="api-config__change-button"
@click="() => (configureApi = true)"
>
{{ $t('apiConfig.change') }}
</ButtonLink>
</div>
<message variant="danger" v-if="errorMsg !== ''" class="mt-2">
<Message
v-if="errorMsg !== ''"
variant="danger"
class="mt-2"
>
{{ errorMsg }}
</message>
</Message>
</div>
</template>
@ -57,7 +81,7 @@ const props = defineProps({
const emit = defineEmits(['foundApi'])
const apiUrl = ref(window.API_URL)
const configureApi = ref(apiUrl.value === '')
const configureApi = ref(window.API_URL === '')
// Because we're only using this to parse the hostname, it should be fine to just prefix with http://
// regardless of whether the url is actually reachable under http.

View File

@ -1,18 +1,24 @@
<template>
<div class="card" :class="{'has-no-shadow': !shadow}">
<header class="card-header" v-if="title !== ''">
<div
class="card"
:class="{'has-no-shadow': !shadow}"
>
<header
v-if="title !== ''"
class="card-header"
>
<p class="card-header-title">
{{ title }}
</p>
<BaseButton
v-if="hasClose"
v-tooltip="$t('misc.close')"
class="card-header-icon"
:aria-label="$t('misc.close')"
@click="$emit('close')"
v-tooltip="$t('misc.close')"
>
<span class="icon">
<icon :icon="closeIcon"/>
<icon :icon="closeIcon" />
</span>
</BaseButton>
</header>
@ -24,12 +30,15 @@
}"
>
<div :class="{'content': hasContent}">
<slot/>
<slot />
</div>
</div>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer"/>
<footer
v-if="$slots.footer"
class="card-footer"
>
<slot name="footer" />
</footer>
</div>
</template>

View File

@ -2,7 +2,7 @@
<span
:style="{backgroundColor: color }"
class="color-bubble"
></span>
/>
</template>
<script lang="ts" setup>

View File

@ -1,16 +1,20 @@
<template>
<modal @close="$router.back()" :overflow="true" :wide="wide">
<modal
:overflow="true"
:wide="wide"
@close="$router.back()"
>
<card
:title="title"
:shadow="false"
:padding="false"
class="has-text-left"
:has-close="true"
@close="$router.back()"
:loading="loading"
@close="$router.back()"
>
<div class="p-4">
<slot/>
<slot />
</div>
<template #footer>
@ -32,10 +36,10 @@
<x-button
v-if="hasPrimaryAction"
variant="primary"
@click.prevent.stop="primary()"
:icon="primaryIcon"
:disabled="primaryDisabled || loading"
class="ml-2"
@click.prevent.stop="primary()"
>
{{ primaryLabel || $t('misc.create') }}
</x-button>

View File

@ -5,10 +5,10 @@
class="icon is-small"
:class="iconClass"
>
<Icon :icon="icon"/>
<Icon :icon="icon" />
</span>
<span>
<slot/>
<slot />
</span>
</BaseButton>
</template>

View File

@ -1,15 +1,32 @@
<template>
<div class="dropdown" ref="dropdown">
<slot name="trigger" :close="close" :toggleOpen="toggleOpen" :open="open">
<BaseButton class="dropdown-trigger is-flex" @click="toggleOpen">
<icon :icon="triggerIcon" class="icon"/>
<div
ref="dropdown"
class="dropdown"
>
<slot
name="trigger"
:close="close"
:toggle-open="toggleOpen"
:open="open"
>
<BaseButton
class="dropdown-trigger is-flex"
@click="toggleOpen"
>
<icon
:icon="triggerIcon"
class="icon"
/>
</BaseButton>
</slot>
<CustomTransition name="fade">
<div class="dropdown-menu" v-if="open">
<div
v-if="open"
class="dropdown-menu"
>
<div class="dropdown-content">
<slot :close="close"></slot>
<slot :close="close" />
</div>
</div>
</CustomTransition>

View File

@ -1,10 +1,17 @@
<template>
<message variant="danger">
<i18n-t keypath="loadingError.failed" scope="global">
<ButtonLink @click="reload">{{ $t('loadingError.tryAgain') }}</ButtonLink>
<ButtonLink href="https://vikunja.io/contact/">{{ $t('loadingError.contact') }}</ButtonLink>
<Message variant="danger">
<i18n-t
keypath="loadingError.failed"
scope="global"
>
<ButtonLink @click="reload">
{{ $t('loadingError.tryAgain') }}
</ButtonLink>
<ButtonLink href="https://vikunja.io/contact/">
{{ $t('loadingError.contact') }}
</ButtonLink>
</i18n-t>
</message>
</Message>
</template>
<script lang="ts">

View File

@ -1,11 +1,11 @@
<template>
<input
v-bind="attrs"
ref="root"
type="text"
data-input
:disabled="disabled"
v-bind="attrs"
ref="root"
/>
>
</template>
<script lang="ts">

View File

@ -1,10 +1,20 @@
<template>
<modal @close="close()">
<card class="has-background-white keyboard-shortcuts" :shadow="false" :title="$t('keyboardShortcuts.title')">
<template v-for="(s, i) in shortcuts" :key="i">
<card
class="has-background-white keyboard-shortcuts"
:shadow="false"
:title="$t('keyboardShortcuts.title')"
>
<template
v-for="(s, i) in shortcuts"
:key="i"
>
<h3>{{ $t(s.title) }}</h3>
<message class="mb-4" v-if="s.available">
<Message
v-if="s.available"
class="mb-4"
>
{{
typeof s.available === 'undefined' ?
$t('keyboardShortcuts.allPages') :
@ -14,14 +24,19 @@
: $t('keyboardShortcuts.somePagesOnly')
)
}}
</message>
</Message>
<dl class="shortcut-list">
<template v-for="(sc, si) in s.shortcuts" :key="si">
<dt class="shortcut-title">{{ $t(sc.title) }}</dt>
<shortcut
class="shortcut-keys"
<template
v-for="(sc, si) in s.shortcuts"
:key="si"
>
<dt class="shortcut-title">
{{ $t(sc.title) }}
</dt>
<Shortcut
is="dd"
class="shortcut-keys"
:keys="sc.keys"
:combination="sc.combination && $t(`keyboardShortcuts.${sc.combination}`)"
/>

View File

@ -1,8 +1,18 @@
<template>
<div class="legal-links">
<BaseButton :href="imprintUrl" v-if="imprintUrl">{{ $t('navigation.imprint') }}</BaseButton>
<BaseButton
v-if="imprintUrl"
:href="imprintUrl"
>
{{ $t('navigation.imprint') }}
</BaseButton>
<span v-if="imprintUrl && privacyPolicyUrl"> | </span>
<BaseButton :href="privacyPolicyUrl" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</BaseButton>
<BaseButton
v-if="privacyPolicyUrl"
:href="privacyPolicyUrl"
>
{{ $t('navigation.privacy') }}
</BaseButton>
</div>
</template>

View File

@ -1,5 +1,8 @@
<template>
<div class="loader-container is-loading" :class="{'is-small': variant === 'small'}"></div>
<div
class="loader-container is-loading"
:class="{'is-small': variant === 'small'}"
/>
</template>
<script lang="ts">

View File

@ -1,7 +1,10 @@
<template>
<div class="message-wrapper">
<div class="message" :class="[variant, textAlignClass]">
<slot/>
<div
class="message"
:class="[variant, textAlignClass]"
>
<slot />
</div>
</div>
</template>
@ -9,14 +12,6 @@
<script lang="ts" setup>
import {computed, type PropType} from 'vue'
const TEXT_ALIGN_MAP = Object.freeze({
left: '',
center: 'has-text-centered',
right: 'has-text-right',
})
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
const props = defineProps({
variant: {
type: String,
@ -28,6 +23,14 @@ const props = defineProps({
},
})
const TEXT_ALIGN_MAP = Object.freeze({
left: '',
center: 'has-text-centered',
right: 'has-text-right',
})
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
const textAlignClass = computed(() => TEXT_ALIGN_MAP[props.textAlign])
</script>

View File

@ -1,56 +1,59 @@
<template>
<Teleport to="body">
<!-- FIXME: transition should not be included in the modal -->
<CustomTransition :name="transitionName" appear>
<CustomTransition
:name="transitionName"
appear
>
<section
v-if="enabled"
ref="modal"
class="modal-mask"
:class="[
{ 'has-overflow': overflow },
variant,
]"
ref="modal"
v-bind="attrs"
>
<div
v-shortcut="'Escape'"
class="modal-container"
@mousedown.self.prevent.stop="$emit('close')"
v-shortcut="'Escape'"
>
<div
class="modal-content"
:class="{
'has-overflow': overflow,
'is-wide': wide
}"
'has-overflow': overflow,
'is-wide': wide
}"
>
<BaseButton
@click="$emit('close')"
class="close"
@click="$emit('close')"
>
<icon icon="times"/>
<icon icon="times" />
</BaseButton>
<slot>
<div class="header">
<slot name="header"></slot>
<slot name="header" />
</div>
<div class="content">
<slot name="text"></slot>
<slot name="text" />
</div>
<div class="actions">
<x-button
@click="$emit('close')"
variant="tertiary"
class="has-text-danger"
@click="$emit('close')"
>
{{ $t('misc.cancel') }}
</x-button>
<x-button
@click="$emit('submit')"
variant="primary"
v-cy="'modalPrimary'"
variant="primary"
:shadow="false"
@click="$emit('submit')"
>
{{ $t('misc.doit') }}
</x-button>

View File

@ -1,8 +1,15 @@
<template>
<div class="no-auth-wrapper">
<Logo class="logo" width="200" height="58"/>
<Logo
class="logo"
width="200"
height="58"
/>
<div class="noauth-container">
<section class="image" :class="{'has-message': motd !== ''}">
<section
class="image"
:class="{'has-message': motd !== ''}"
>
<Message v-if="motd !== ''">
{{ motd }}
</Message>
@ -12,14 +19,22 @@
</section>
<section class="content">
<div>
<h2 class="title" v-if="title">{{ title }}</h2>
<api-config v-if="showApiConfig"/>
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
<h2
v-if="title"
class="title"
>
{{ title }}
</h2>
<ApiConfig v-if="showApiConfig" />
<Message
v-if="motd !== ''"
class="is-hidden-tablet mb-4"
>
{{ motd }}
</Message>
<slot/>
<slot />
</div>
<legal/>
<Legal />
</section>
</div>
</div>
@ -38,6 +53,11 @@ import ApiConfig from '@/components/misc/api-config.vue'
import {useTitle} from '@/composables/useTitle'
import {useConfigStore} from '@/stores/config'
const {
showApiConfig = true,
} = defineProps<{
showApiConfig?: boolean
}>()
const configStore = useConfigStore()
const motd = computed(() => configStore.motd)
@ -46,11 +66,6 @@ const {t} = useI18n({useScope: 'global'})
const title = computed(() => t(route.meta?.title as string || ''))
useTitle(() => title.value)
const {
showApiConfig = true,
} = defineProps<{
showApiConfig?: boolean
}>()
</script>
<style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template>
<p class="has-text-centered has-text-grey is-italic p-4 mb-4">
<slot></slot>
<slot />
</p>
</template>

View File

@ -1,30 +1,43 @@
<template>
<notifications position="bottom left" :max="2" class="global-notification">
<notifications
position="bottom left"
:max="2"
class="global-notification"
>
<template #body="{ item, close }">
<!-- FIXME: overlay whole notification with button and add event listener on that button instead -->
<div
class="vue-notification-template vue-notification"
:class="[
'vue-notification-template',
'vue-notification',
item.type,
]"
@click="close()"
>
<div v-if="item.title" class="notification-title">{{ item.title }}</div>
<div
v-if="item.title"
class="notification-title"
>
{{ item.title }}
</div>
<div class="notification-content">
<template v-for="(t, k) in item.text" :key="k">{{ t }}<br /></template>
<template
v-for="(t, k) in item.text"
:key="k"
>
{{ t }}<br>
</template>
</div>
<div
class="buttons is-right"
v-if="item.data?.actions?.length > 0"
class="buttons is-right"
>
<x-button
v-for="(action, i) in item.data.actions"
:key="'action_' + i"
@click="action.callback"
:shadow="false"
class="is-small"
variant="secondary"
v-for="(action, i) in item.data.actions"
@click="action.callback"
>
{{ action.title }}
</x-button>

View File

@ -1,25 +1,33 @@
<template>
<nav
v-if="totalPages > 1"
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
v-if="totalPages > 1"
>
<router-link
:disabled="currentPage === 1 || undefined"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous">
class="pagination-previous"
>
{{ $t('misc.previous') }}
</router-link>
<router-link
:disabled="currentPage === totalPages || undefined"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next">
class="pagination-next"
>
{{ $t('misc.next') }}
</router-link>
<ul class="pagination-list">
<li :key="`page-${i}`" v-for="(p, i) in pages">
<span class="pagination-ellipsis" v-if="p.isEllipsis">&hellip;</span>
<li
v-for="(p, i) in pages"
:key="`page-${i}`"
>
<span
v-if="p.isEllipsis"
class="pagination-ellipsis"
>&hellip;</span>
<router-link
v-else
class="pagination-link"
@ -37,6 +45,17 @@
<script lang="ts" setup>
import {computed} from 'vue'
const props = defineProps({
totalPages: {
type: Number,
required: true,
},
currentPage: {
type: Number,
default: 0,
},
})
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
@ -81,17 +100,6 @@ function getRouteForPagination(page = 1, type = null) {
}
}
const props = defineProps({
totalPages: {
type: Number,
required: true,
},
currentPage: {
type: Number,
default: 0,
},
})
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>

View File

@ -1,14 +1,24 @@
<template>
<slot name="trigger" :isOpen="open" :toggle="toggle" :close="close"></slot>
<slot
name="trigger"
:is-open="open"
:toggle="toggle"
:close="close"
/>
<div
ref="popup"
class="popup"
:class="{
'is-open': open,
'has-overflow': props.hasOverflow && open
}"
ref="popup"
>
<slot name="content" :isOpen="open" :toggle="toggle" :close="close"/>
<slot
name="content"
:is-open="open"
:toggle="toggle"
:close="close"
/>
</div>
</template>

View File

@ -1,37 +1,55 @@
<template>
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
<div class="offline" style="height: 0;width: 0;"></div>
<div class="app offline" v-if="!online">
<div
class="offline"
style="height: 0;width: 0;"
/>
<div
v-if="!online"
class="app offline"
>
<div class="offline-message">
<h1 class="title">{{ $t('offline.title') }}</h1>
<h1 class="title">
{{ $t('offline.title') }}
</h1>
<p>{{ $t('offline.text') }}</p>
</div>
</div>
<template v-else-if="ready">
<slot/>
<slot />
</template>
<section v-else-if="error !== ''">
<no-auth-wrapper :show-api-config="false">
<NoAuthWrapper :show-api-config="false">
<p v-if="error === ERROR_NO_API_URL">
{{ $t('ready.noApiUrlConfigured') }}
</p>
<message variant="danger" v-else class="mb-4">
<Message
v-else
variant="danger"
class="mb-4"
>
<p>
{{ $t('ready.errorOccured') }}<br/>
{{ $t('ready.errorOccured') }}<br>
{{ error }}
</p>
<p>
{{ $t('ready.checkApiUrl') }}
</p>
</message>
<api-config :configure-open="true" @found-api="load"/>
</no-auth-wrapper>
</Message>
<ApiConfig
:configure-open="true"
@foundApi="load"
/>
</NoAuthWrapper>
</section>
<CustomTransition name="fade">
<section class="vikunja-loading" v-if="showLoading">
<Logo class="logo"/>
<section
v-if="showLoading"
class="vikunja-loading"
>
<Logo class="logo" />
<p>
<span class="loader-container is-loading-small is-loading"></span>
<span class="loader-container is-loading-small is-loading" />
{{ $t('ready.loading') }}
</p>
</section>

View File

@ -1,6 +1,12 @@
<template>
<component :is="is" class="shortcuts">
<template v-for="(k, i) in keys" :key="i">
<component
:is="is"
class="shortcuts"
>
<template
v-for="(k, i) in keys"
:key="i"
>
<kbd>{{ k }}</kbd>
<span v-if="i < keys.length - 1">{{ combination }}</span>
</template>

View File

@ -1,32 +1,32 @@
<template>
<x-button
v-if="type === 'button'"
v-tooltip="tooltipText"
variant="secondary"
:icon="iconName"
v-tooltip="tooltipText"
@click="changeSubscription"
:disabled="disabled"
@click="changeSubscription"
>
{{ buttonText }}
</x-button>
<DropdownItem
v-else-if="type === 'dropdown'"
v-tooltip="tooltipText"
@click="changeSubscription"
:disabled="disabled"
:icon="iconName"
@click="changeSubscription"
>
{{ buttonText }}
</DropdownItem>
<BaseButton
v-else
v-tooltip="tooltipText"
@click="changeSubscription"
:class="{'is-disabled': disabled}"
:disabled="disabled"
@click="changeSubscription"
>
<span class="icon">
<icon :icon="iconName"/>
<icon :icon="iconName" />
</span>
{{ buttonText }}
</BaseButton>
@ -63,10 +63,10 @@ const props = defineProps({
},
})
const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
const emit = defineEmits(['update:modelValue'])
const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n({useScope: 'global'})

View File

@ -4,14 +4,17 @@
:class="{'is-inline': isInline}"
>
<img
v-tooltip="displayName"
:height="avatarSize"
:src="getAvatarUrl(user, avatarSize)"
:width="avatarSize"
:alt="'Avatar of ' + displayName"
class="avatar"
v-tooltip="displayName"
/>
<span class="username" v-if="showUsername">{{ displayName }}</span>
>
<span
v-if="showUsername"
class="username"
>{{ displayName }}</span>
</div>
</template>

View File

@ -1,51 +1,80 @@
<template>
<div class="notifications">
<slot name="trigger" toggleOpen="() => showNotifications = !showNotifications" :has-unread-notifications="unreadNotifications > 0">
<BaseButton class="trigger-button" @click.stop="showNotifications = !showNotifications">
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
<icon icon="bell"/>
<slot
name="trigger"
toggle-open="() => showNotifications = !showNotifications"
:has-unread-notifications="unreadNotifications > 0"
>
<BaseButton
class="trigger-button"
@click.stop="showNotifications = !showNotifications"
>
<span
v-if="unreadNotifications > 0"
class="unread-indicator"
/>
<icon icon="bell" />
</BaseButton>
</slot>
<CustomTransition name="fade">
<div class="notifications-list" v-if="showNotifications" ref="popup">
<div
v-if="showNotifications"
ref="popup"
class="notifications-list"
>
<span class="head">{{ $t('notification.title') }}</span>
<div
v-for="(n, index) in notifications"
:key="n.id"
class="single-notification"
>
<div class="read-indicator" :class="{'read': n.readAt !== null}"></div>
<user
<div
class="read-indicator"
:class="{'read': n.readAt !== null}"
/>
<User
v-if="n.notification.doer"
:user="n.notification.doer"
:show-username="false"
:avatar-size="16"
v-if="n.notification.doer"
/>
<div class="detail">
<div>
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
<span
v-if="n.notification.doer"
class="has-text-weight-bold mr-1"
>
{{ getDisplayName(n.notification.doer) }}
</span>
<BaseButton @click="() => to(n, index)()" class="has-text-left">
<BaseButton
class="has-text-left"
@click="() => to(n, index)()"
>
{{ n.toText(userInfo) }}
</BaseButton>
</div>
<span class="created" v-tooltip="formatDateLong(n.created)">
<span
v-tooltip="formatDateLong(n.created)"
class="created"
>
{{ formatDateSince(n.created) }}
</span>
</div>
</div>
<x-button
<XButton
v-if="notifications.length > 0 && unreadNotifications > 0"
variant="tertiary"
class="mt-2 is-fullwidth"
@click="markAllRead"
variant="tertiary"
class="mt-2 is-fullwidth"
>
{{ $t('notification.markAllRead') }}
</x-button>
<p class="nothing" v-if="notifications.length === 0">
{{ $t('notification.none') }}<br/>
</XButton>
<p
v-if="notifications.length === 0"
class="nothing"
>
{{ $t('notification.none') }}<br>
<span class="explainer">
{{ $t('notification.explainer') }}
</span>
@ -156,6 +185,8 @@ async function markAllRead() {
const notificationService = new NotificationService()
await notificationService.markAllRead()
success({message: t('notification.markAllReadSuccess')})
notifications.value.forEach(n => n.readAt = new Date())
}
</script>

View File

@ -14,7 +14,7 @@
:title="$t('keyboardShortcuts.project.switchToListView')"
class="switch-view-button"
:class="{'is-active': viewName === 'project'}"
:to="{ name: 'project.list', params: { projectId } }"
:to="{ name: 'project.list', params: { projectId } }"
>
{{ $t('project.list.title') }}
</BaseButton>
@ -23,7 +23,7 @@
:title="$t('keyboardShortcuts.project.switchToGanttView')"
class="switch-view-button"
:class="{'is-active': viewName === 'gantt'}"
:to="{ name: 'project.gantt', params: { projectId } }"
:to="{ name: 'project.gantt', params: { projectId } }"
>
{{ $t('project.gantt.title') }}
</BaseButton>
@ -32,7 +32,7 @@
:title="$t('keyboardShortcuts.project.switchToTableView')"
class="switch-view-button"
:class="{'is-active': viewName === 'table'}"
:to="{ name: 'project.table', params: { projectId } }"
:to="{ name: 'project.table', params: { projectId } }"
>
{{ $t('project.table.title') }}
</BaseButton>
@ -49,12 +49,16 @@
<slot name="header" />
</div>
<CustomTransition name="fade">
<Message variant="warning" v-if="currentProject?.isArchived" class="mb-4">
<Message
v-if="currentProject?.isArchived"
variant="warning"
class="mb-4"
>
{{ $t('project.archivedMessage') }}
</Message>
</CustomTransition>
<slot v-if="loadedProjectId"/>
<slot v-if="loadedProjectId" />
</div>
</template>

View File

@ -15,11 +15,20 @@
:class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
/>
<span v-if="project.isArchived" class="is-archived" >{{ $t('project.archived') }}</span>
<span
v-if="project.isArchived"
class="is-archived"
>{{ $t('project.archived') }}</span>
<div class="project-title" aria-hidden="true">
<span v-if="project.id < -1" class="saved-filter-icon icon">
<icon icon="filter"/>
<div
class="project-title"
aria-hidden="true"
>
<span
v-if="project.id < -1"
class="saved-filter-icon icon"
>
<icon icon="filter" />
</span>
{{ project.title }}
</div>

View File

@ -1,13 +1,13 @@
<template>
<ul class="project-grid">
<li
v-for="(item, index) in filteredProjects"
:key="`project_${item.id}_${index}`"
class="project-grid-item"
>
<ProjectCard :project="item" />
</li>
</ul>
<ul class="project-grid">
<li
v-for="(item, index) in filteredProjects"
:key="`project_${item.id}_${index}`"
class="project-grid-item"
>
<ProjectCard :project="item" />
</li>
</ul>
</template>
<script lang="ts" setup>

View File

@ -7,9 +7,9 @@
{{ $t('filters.clear') }}
</x-button>
<x-button
@click="() => modalOpen = true"
variant="secondary"
icon="filter"
@click="() => modalOpen = true"
>
{{ $t('filters.title') }}
</x-button>
@ -20,10 +20,10 @@
variant="hint-modal"
@close="() => modalOpen = false"
>
<filters
:has-title="true"
v-model="value"
<Filters
ref="filters"
v-model="value"
:has-title="true"
class="filter-popup"
/>
</modal>

View File

@ -1,134 +1,157 @@
<template>
<card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''">
<card
class="filters has-overflow"
:title="hasTitle ? $t('filters.title') : ''"
>
<div class="field is-flex is-flex-direction-column">
<fancycheckbox
<Fancycheckbox
v-model="params.filter_include_nulls"
@update:model-value="change()"
@update:modelValue="change()"
>
{{ $t('filters.attributes.includeNulls') }}
</fancycheckbox>
<fancycheckbox
</Fancycheckbox>
<Fancycheckbox
v-model="filters.requireAllFilters"
@update:model-value="setFilterConcat()"
@update:modelValue="setFilterConcat()"
>
{{ $t('filters.attributes.requireAll') }}
</fancycheckbox>
<fancycheckbox
</Fancycheckbox>
<Fancycheckbox
v-model="filters.done"
@update:model-value="setDoneFilter"
@update:modelValue="setDoneFilter"
>
{{ $t('filters.attributes.showDoneTasks') }}
</fancycheckbox>
<fancycheckbox
</Fancycheckbox>
<Fancycheckbox
v-if="!['project.kanban', 'project.table'].includes($route.name as string)"
v-model="sortAlphabetically"
@update:model-value="change()"
@update:modelValue="change()"
>
{{ $t('filters.attributes.sortAlphabetically') }}
</fancycheckbox>
</Fancycheckbox>
</div>
<div class="field">
<label class="label">{{ $t('misc.search') }}</label>
<div class="control">
<input
v-model="params.s"
class="input"
:placeholder="$t('misc.search')"
v-model="params.s"
@blur="change()"
@keyup.enter="change()"
/>
>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.priority') }}</label>
<div class="control single-value-control">
<priority-select
<PrioritySelect
v-model.number="filters.priority"
@update:model-value="setPriority"
:disabled="!filters.usePriority || undefined"
@update:modelValue="setPriority"
/>
<fancycheckbox
<Fancycheckbox
v-model="filters.usePriority"
@update:model-value="setPriority"
@update:modelValue="setPriority"
>
{{ $t('filters.attributes.enablePriority') }}
</fancycheckbox>
</Fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.percentDone') }}</label>
<div class="control single-value-control">
<percent-done-select
<PercentDoneSelect
v-model.number="filters.percentDone"
@update:model-value="setPercentDoneFilter"
:disabled="!filters.usePercentDone || undefined"
@update:modelValue="setPercentDoneFilter"
/>
<fancycheckbox
<Fancycheckbox
v-model="filters.usePercentDone"
@update:model-value="setPercentDoneFilter"
@update:modelValue="setPercentDoneFilter"
>
{{ $t('filters.attributes.enablePercentDone') }}
</fancycheckbox>
</Fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.dueDate') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.dueDate"
@update:model-value="values => setDateFilter('due_date', values)"
@update:modelValue="values => setDateFilter('due_date', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
<x-button
variant="secondary"
:shadow="false"
class="mb-2"
@click.prevent.stop="toggle()"
>
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.startDate') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.startDate"
@update:model-value="values => setDateFilter('start_date', values)"
@update:modelValue="values => setDateFilter('start_date', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
<x-button
variant="secondary"
:shadow="false"
class="mb-2"
@click.prevent.stop="toggle()"
>
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.endDate') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.endDate"
@update:model-value="values => setDateFilter('end_date', values)"
@update:modelValue="values => setDateFilter('end_date', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
<x-button
variant="secondary"
:shadow="false"
class="mb-2"
@click.prevent.stop="toggle()"
>
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.reminders') }}</label>
<div class="control">
<datepicker-with-range
<DatepickerWithRange
v-model="filters.reminders"
@update:model-value="values => setDateFilter('reminders', values)"
@update:modelValue="values => setDateFilter('reminders', values)"
>
<template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
<x-button
variant="secondary"
:shadow="false"
class="mb-2"
@click.prevent.stop="toggle()"
>
{{ buttonText }}
</x-button>
</template>
</datepicker-with-range>
</DatepickerWithRange>
</div>
</div>
@ -146,10 +169,10 @@
<div class="field">
<label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control labels-list">
<edit-labels
:creatable="false"
<EditLabels
v-model="entities.labels"
@update:model-value="changeLabelFilter"
:creatable="false"
@update:modelValue="changeLabelFilter"
/>
</div>
</div>
@ -162,9 +185,9 @@
<div class="control">
<SelectProject
v-model="entities.projects"
:project-filter="p => p.id > 0"
@select="changeMultiselectFilter('projects', 'project_id')"
@remove="changeMultiselectFilter('projects', 'project_id')"
:project-filter="p => p.id > 0"
/>
</div>
</div>
@ -205,6 +228,18 @@ import ProjectService from '@/services/project'
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
import {getDefaultParams} from '@/composables/useTaskList'
const props = defineProps({
modelValue: {
required: true,
},
hasTitle: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
const DEFAULT_PARAMS = {
sort_by: [],
@ -233,18 +268,6 @@ const DEFAULT_FILTERS = {
project_id: '',
} as const
const props = defineProps({
modelValue: {
required: true,
},
hasTitle: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const {modelValue} = toRefs(props)
const labelStore = useLabelStore()

View File

@ -1,99 +1,108 @@
<template>
<dropdown>
<Dropdown>
<template #trigger="triggerProps">
<slot name="trigger" v-bind="triggerProps">
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
<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(project)">
<dropdown-item
<DropdownItem
:to="{ name: 'filter.settings.edit', params: { projectId: project.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
:to="{ name: 'filter.settings.delete', params: { projectId: project.id } }"
icon="trash-alt"
>
{{ $t('misc.delete') }}
</dropdown-item>
</DropdownItem>
</template>
<template v-else-if="project.isArchived">
<dropdown-item
<DropdownItem
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
icon="archive"
>
{{ $t('menu.unarchive') }}
</dropdown-item>
</DropdownItem>
</template>
<template v-else>
<dropdown-item
<DropdownItem
:to="{ name: 'project.settings.edit', params: { projectId: project.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
v-if="backgroundsEnabled"
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
icon="image"
>
{{ $t('menu.setBackground') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
:to="{ name: 'project.settings.share', params: { projectId: project.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
:to="{ name: 'project.settings.duplicate', params: { projectId: project.id } }"
icon="paste"
>
{{ $t('menu.duplicate') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
icon="archive"
>
{{ $t('menu.archive') }}
</dropdown-item>
</DropdownItem>
<Subscription
class="has-no-shadow"
:is-button="false"
entity="project"
:entity-id="project.id"
:model-value="project.subscription"
@update:model-value="setSubscriptionInStore"
type="dropdown"
@update:modelValue="setSubscriptionInStore"
/>
<dropdown-item
<DropdownItem
:to="{ name: 'project.settings.webhooks', params: { projectId: project.id } }"
icon="bolt"
>
{{ $t('project.webhooks.title') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
v-if="level < 2"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
icon="layer-group"
>
{{ $t('menu.createProject') }}
</dropdown-item>
<dropdown-item
</DropdownItem>
<DropdownItem
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
icon="trash-alt"
class="has-text-danger"
>
{{ $t('menu.delete') }}
</dropdown-item>
</DropdownItem>
</template>
</dropdown>
</Dropdown>
</template>
<script setup lang="ts">

View File

@ -1,33 +1,53 @@
<template>
<modal :enabled="active" @close="closeQuickActions" :overflow="isNewTaskCommand">
<modal
:enabled="active"
:overflow="isNewTaskCommand"
@close="closeQuickActions"
>
<div class="card quick-actions">
<div class="action-input" :class="{'has-active-cmd': selectedCmd !== null}">
<div class="active-cmd tag" v-if="selectedCmd !== null">
<div
class="action-input"
:class="{'has-active-cmd': selectedCmd !== null}"
>
<div
v-if="selectedCmd !== null"
class="active-cmd tag"
>
{{ selectedCmd.title }}
</div>
<input
ref="searchInput"
v-model="query"
v-focus
class="input"
:class="{'is-loading': loading}"
v-model="query"
:placeholder="placeholder"
@keyup="search"
ref="searchInput"
@keydown.down.prevent="select(0, 0)"
@keyup.prevent.delete="unselectCmd"
@keyup.prevent.enter="doCmd"
@keyup.prevent.esc="closeQuickActions"
/>
>
</div>
<div class="help has-text-grey-light p-2" v-if="hintText !== '' && !isNewTaskCommand">
<div
v-if="hintText !== '' && !isNewTaskCommand"
class="help has-text-grey-light p-2"
>
{{ hintText }}
</div>
<quick-add-magic v-if="isNewTaskCommand"/>
<QuickAddMagic v-if="isNewTaskCommand" />
<div class="results" v-if="selectedCmd === null">
<div v-for="(r, k) in results" :key="k" class="result">
<div
v-if="selectedCmd === null"
class="results"
>
<div
v-for="(r, k) in results"
:key="k"
class="result"
>
<span class="result-title">
{{ r.title }}
</span>
@ -35,9 +55,9 @@
<BaseButton
v-for="(i, key) in r.items"
:key="key"
:ref="(el: Element | ComponentPublicInstance | null) => setResultRefs(el, k, key)"
class="result-item-button"
:class="{'is-strikethrough': (i as DoAction<ITask>)?.done}"
:ref="(el: Element | ComponentPublicInstance | null) => setResultRefs(el, k, key)"
@keydown.up.prevent="select(k, key - 1)"
@keydown.down.prevent="select(k, key + 1)"
@click.prevent.stop="doAction(r.type, i)"
@ -45,10 +65,10 @@
@keyup.prevent.esc="searchInput?.focus()"
>
<template v-if="r.type === ACTION_TYPE.LABELS">
<x-label :label="i"/>
<XLabel :label="i" />
</template>
<template v-else-if="r.type === ACTION_TYPE.TASK">
<single-task-inline-readonly
<SingleTaskInlineReadonly
:task="i"
:show-project="true"
/>

View File

@ -3,8 +3,9 @@
<p class="has-text-weight-bold">
{{ $t('project.share.links.title') }}
<span
v-tooltip="$t('project.share.links.explanation')"
class="is-size-7 has-text-grey is-italic ml-3"
v-tooltip="$t('project.share.links.explanation')">
>
{{ $t('project.share.links.what') }}
</span>
</p>
@ -12,20 +13,30 @@
<div class="sharables-project">
<x-button
v-if="!(linkShares.length === 0 || showNewForm)"
@click="showNewForm = true"
icon="plus"
class="mb-4">
class="mb-4"
@click="showNewForm = true"
>
{{ $t('project.share.links.create') }}
</x-button>
<div class="p-4" v-if="linkShares.length === 0 || showNewForm">
<div
v-if="linkShares.length === 0 || showNewForm"
class="p-4"
>
<div class="field">
<label class="label" for="linkShareRight">
<label
class="label"
for="linkShareRight"
>
{{ $t('project.share.right.title') }}
</label>
<div class="control">
<div class="select">
<select v-model="selectedRight" id="linkShareRight">
<select
id="linkShareRight"
v-model="selectedRight"
>
<option :value="RIGHTS.READ">
{{ $t('project.share.right.read') }}
</option>
@ -40,131 +51,150 @@
</div>
</div>
<div class="field">
<label class="label" for="linkShareName">
<label
class="label"
for="linkShareName"
>
{{ $t('project.share.links.name') }}
</label>
<div class="control">
<input
id="linkShareName"
v-model="name"
v-tooltip="$t('project.share.links.nameExplanation')"
class="input"
:placeholder="$t('project.share.links.namePlaceholder')"
v-tooltip="$t('project.share.links.nameExplanation')"
v-model="name"
/>
>
</div>
</div>
<div class="field">
<label class="label" for="linkSharePassword">
<label
class="label"
for="linkSharePassword"
>
{{ $t('project.share.links.password') }}
</label>
<div class="control">
<input
id="linkSharePassword"
v-model="password"
v-tooltip="$t('project.share.links.passwordExplanation')"
type="password"
class="input"
:placeholder="$t('user.auth.passwordPlaceholder')"
v-tooltip="$t('project.share.links.passwordExplanation')"
v-model="password"
/>
>
</div>
</div>
<x-button @click="add(projectId)" icon="plus">
<x-button
icon="plus"
@click="add(projectId)"
>
{{ $t('project.share.share') }}
</x-button>
</div>
<table
class="table has-actions is-striped is-hoverable is-fullwidth"
v-if="linkShares.length > 0"
class="table has-actions is-striped is-hoverable is-fullwidth"
>
<thead>
<tr>
<th></th>
<th>{{ $t('project.share.links.view') }}</th>
<th>{{ $t('project.share.attributes.delete') }}</th>
</tr>
<tr>
<th />
<th>{{ $t('project.share.links.view') }}</th>
<th>{{ $t('project.share.attributes.delete') }}</th>
</tr>
</thead>
<tbody>
<tr :key="s.id" v-for="s in linkShares">
<td>
<p class="mb-2 is-italic" v-if="s.name !== ''">
{{ s.name }}
</p>
<tr
v-for="s in linkShares"
:key="s.id"
>
<td>
<p
v-if="s.name !== ''"
class="mb-2 is-italic"
>
{{ s.name }}
</p>
<p class="mb-2">
<i18n-t keypath="project.share.links.sharedBy" scope="global">
<strong>{{ getDisplayName(s.sharedBy) }}</strong>
</i18n-t>
</p>
<p class="mb-2">
<i18n-t
keypath="project.share.links.sharedBy"
scope="global"
>
<strong>{{ getDisplayName(s.sharedBy) }}</strong>
</i18n-t>
</p>
<p class="mb-2">
<template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small">
<icon icon="lock"/>
</span>&nbsp;
{{ $t('project.share.right.admin') }}
</template>
<template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small">
<icon icon="pen"/>
</span>&nbsp;
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users"/>
</span>&nbsp;
{{ $t('project.share.right.read') }}
</template>
</p>
<p class="mb-2">
<template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small">
<icon icon="lock" />
</span>&nbsp;
{{ $t('project.share.right.admin') }}
</template>
<template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small">
<icon icon="pen" />
</span>&nbsp;
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users" />
</span>&nbsp;
{{ $t('project.share.right.read') }}
</template>
</p>
<div class="field has-addons no-input-mobile">
<div class="control">
<input
<div class="field has-addons no-input-mobile">
<div class="control">
<input
:value="getShareLink(s.hash, selectedView[s.id])"
class="input"
readonly
type="text"
/>
</div>
<div class="control">
<x-button
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
:shadow="false"
>
</div>
<div class="control">
<x-button
v-tooltip="$t('misc.copy')"
>
<span class="icon">
<icon icon="paste"/>
</span>
</x-button>
:shadow="false"
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
>
<span class="icon">
<icon icon="paste" />
</span>
</x-button>
</div>
</div>
</div>
</td>
<td>
<div class="select">
<select v-model="selectedView[s.id]">
<option
v-for="(title, key) in availableViews"
:value="key"
:key="key">
{{ title }}
</option>
</select>
</div>
</td>
<td class="actions">
<x-button
@click="
</td>
<td>
<div class="select">
<select v-model="selectedView[s.id]">
<option
v-for="(title, key) in availableViews"
:key="key"
:value="key"
>
{{ title }}
</option>
</select>
</div>
</td>
<td class="actions">
<x-button
class="is-danger"
icon="trash-alt"
@click="
() => {
linkIdToDelete = s.id
showDeleteModal = true
}
"
class="is-danger"
icon="trash-alt"
/>
</td>
</tr>
/>
</td>
</tr>
</tbody>
</table>
</div>
@ -207,7 +237,7 @@ import {useConfigStore} from '@/stores/config'
const props = defineProps({
projectId: {
default: 0,
required: true,
required: false,
},
})

View File

@ -10,108 +10,119 @@
:class="{ 'is-loading': searchService.loading }"
>
<Multiselect
v-model="sharable"
:loading="searchService.loading"
:placeholder="$t('misc.searchPlaceholder')"
@search="find"
:search-results="found"
:label="searchLabel"
v-model="sharable"
@search="find"
/>
</p>
<p class="control">
<x-button @click="add()">{{ $t('project.share.share') }}</x-button>
<x-button @click="add()">
{{ $t('project.share.share') }}
</x-button>
</p>
</div>
</div>
<table class="table has-actions is-striped is-hoverable is-fullwidth mb-4" v-if="sharables.length > 0">
<table
v-if="sharables.length > 0"
class="table has-actions is-striped is-hoverable is-fullwidth mb-4"
>
<tbody>
<tr :key="s.id" v-for="s in sharables">
<template v-if="shareType === 'user'">
<td>{{ getDisplayName(s) }}</td>
<td>
<template v-if="s.id === userInfo.id">
<b class="is-success">{{ $t('project.share.userTeam.you') }}</b>
</template>
</td>
</template>
<template v-if="shareType === 'team'">
<td>
<router-link
:to="{
<tr
v-for="s in sharables"
:key="s.id"
>
<template v-if="shareType === 'user'">
<td>{{ getDisplayName(s) }}</td>
<td>
<template v-if="s.id === userInfo.id">
<b class="is-success">{{ $t('project.share.userTeam.you') }}</b>
</template>
</td>
</template>
<template v-if="shareType === 'team'">
<td>
<router-link
:to="{
name: 'teams.edit',
params: { id: s.id },
}"
>
{{ s.name }}
</router-link>
>
{{ s.name }}
</router-link>
</td>
</template>
<td class="type">
<template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small">
<icon icon="lock" />
</span>
{{ $t('project.share.right.admin') }}
</template>
<template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small">
<icon icon="pen" />
</span>
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users" />
</span>
{{ $t('project.share.right.read') }}
</template>
</td>
</template>
<td class="type">
<template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small">
<icon icon="lock"/>
</span>
{{ $t('project.share.right.admin') }}
</template>
<template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small">
<icon icon="pen"/>
</span>
{{ $t('project.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users"/>
</span>
{{ $t('project.share.right.read') }}
</template>
</td>
<td class="actions" v-if="userIsAdmin">
<div class="select">
<select
@change="toggleType(s)"
class="mr-2"
v-model="selectedRight[s.id]"
>
<option
:selected="s.right === RIGHTS.READ"
:value="RIGHTS.READ"
<td
v-if="userIsAdmin"
class="actions"
>
<div class="select">
<select
v-model="selectedRight[s.id]"
class="mr-2"
@change="toggleType(s)"
>
{{ $t('project.share.right.read') }}
</option>
<option
:selected="s.right === RIGHTS.READ_WRITE"
:value="RIGHTS.READ_WRITE"
>
{{ $t('project.share.right.readWrite') }}
</option>
<option
:selected="s.right === RIGHTS.ADMIN"
:value="RIGHTS.ADMIN"
>
{{ $t('project.share.right.admin') }}
</option>
</select>
</div>
<x-button
@click="
<option
:selected="s.right === RIGHTS.READ"
:value="RIGHTS.READ"
>
{{ $t('project.share.right.read') }}
</option>
<option
:selected="s.right === RIGHTS.READ_WRITE"
:value="RIGHTS.READ_WRITE"
>
{{ $t('project.share.right.readWrite') }}
</option>
<option
:selected="s.right === RIGHTS.ADMIN"
:value="RIGHTS.ADMIN"
>
{{ $t('project.share.right.admin') }}
</option>
</select>
</div>
<x-button
class="is-danger"
icon="trash-alt"
@click="
() => {
sharable = s
showDeleteModal = true
}
"
class="is-danger"
icon="trash-alt"
/>
</td>
</tr>
/>
</td>
</tr>
</tbody>
</table>
<nothing v-else>
<Nothing v-else>
{{ $t('project.share.userTeam.notShared', {type: shareTypeNames}) }}
</nothing>
</Nothing>
<modal
:enabled="showDeleteModal"
@ -120,8 +131,8 @@
>
<template #header>
<span>{{
$t('project.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
}}</span>
$t('project.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
}}</span>
</template>
<template #text>
<p>{{ $t('project.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
@ -131,7 +142,7 @@
</template>
<script lang="ts">
export default {name: 'userTeamShare'}
export default {name: 'UserTeamShare'}
</script>
<script setup lang="ts">

View File

@ -3,7 +3,11 @@
v-if="props.isLoading && !ganttBars.length || dayjsLanguageLoading"
class="gantt-container"
/>
<div ref="ganttContainer" class="gantt-container" v-else>
<div
v-else
ref="ganttContainer"
class="gantt-container"
>
<GGanttChart
:date-format="DAYJS_ISO_DATE_FORMAT"
:chart-start="isoToKebabDate(filters.dateFrom)"
@ -12,9 +16,9 @@
bar-start="startDate"
bar-end="endDate"
:grid="true"
@dragend-bar="updateGanttTask"
@dblclick-bar="openTask"
:width="ganttChartWidth"
@dragendBar="updateGanttTask"
@dblclickBar="openTask"
>
<template #timeunit="{value, date}">
<div
@ -72,14 +76,14 @@ export interface GanttChartProps {
defaultTaskEndDate: DateISO
}
const DAYJS_ISO_DATE_FORMAT = 'YYYY-MM-DD'
const props = defineProps<GanttChartProps>()
const emit = defineEmits<{
(e: 'update:task', task: ITaskPartialWithId): void
}>()
const DAYJS_ISO_DATE_FORMAT = 'YYYY-MM-DD'
const {tasks, filters} = toRefs(props)
// setup dayjs for vue-ganttastic
@ -123,6 +127,23 @@ watch(
function transformTaskToGanttBar(t: ITask) {
const black = 'var(--grey-800)'
const taskColor = getHexColor(t.hexColor)
let textColor = black
let backgroundColor = 'var(--grey-100)'
if(t.startDate) {
backgroundColor = taskColor ?? ''
if(typeof taskColor === 'undefined') {
textColor = 'white'
backgroundColor = 'var(--primary)'
} else if(colorIsDark(taskColor)) {
textColor = black
} else {
textColor = 'white'
}
}
return [{
startDate: isoToKebabDate(t.startDate ? t.startDate.toISOString() : props.defaultTaskStartDate),
endDate: isoToKebabDate(t.endDate ? t.endDate.toISOString() : props.defaultTaskEndDate),
@ -131,8 +152,8 @@ function transformTaskToGanttBar(t: ITask) {
label: t.title,
hasHandles: true,
style: {
color: t.startDate ? (colorIsDark(getHexColor(t.hexColor)) ? black : 'white') : black,
backgroundColor: t.startDate ? getHexColor(t.hexColor) : 'var(--grey-100)',
color: textColor,
backgroundColor,
border: t.startDate ? '' : '2px dashed var(--grey-300)',
'text-decoration': t.done ? 'line-through' : null,
},

View File

@ -1,20 +1,24 @@
<template>
<form
@submit.prevent="createTask"
class="add-new-task"
@submit.prevent="createTask"
>
<CustomTransition name="width">
<input
v-if="newTaskFieldActive"
ref="newTaskTitleField"
v-model="newTaskTitle"
class="input"
type="text"
@blur="hideCreateNewTask"
@keyup.esc="newTaskFieldActive = false"
class="input"
ref="newTaskTitleField"
type="text"
/>
>
</CustomTransition>
<x-button @click="showCreateTaskOrCreate" :shadow="false" icon="plus">
<x-button
:shadow="false"
icon="plus"
@click="showCreateTaskOrCreate"
>
{{ $t('task.new') }}
</x-button>
</form>
@ -27,7 +31,7 @@ import type {ITask} from '@/modelTypes/ITask'
import CustomTransition from '@/components/misc/CustomTransition.vue'
const emit = defineEmits<{
(e: 'create-task', title: string): Promise<ITask>
(e: 'createTask', title: string): Promise<ITask>
}>()
const newTaskFieldActive = ref(false)
@ -56,7 +60,7 @@ async function createTask() {
if (!newTaskFieldActive.value) {
return
}
await emit('create-task', newTaskTitle.value)
await emit('createTask', newTaskTitle.value)
newTaskTitle.value = ''
hideCreateNewTask()
}

View File

@ -1,31 +1,34 @@
<template>
<div class="task-add" ref="taskAdd">
<div
ref="taskAdd"
class="task-add"
>
<div class="add-task__field field is-grouped">
<p class="control has-icons-left has-icons-right is-expanded">
<textarea
ref="newTaskInput"
v-model="newTaskTitle"
v-focus
class="add-task-textarea input"
:class="{'textarea-empty': newTaskTitle === ''}"
:placeholder="$t('project.list.addPlaceholder')"
rows="1"
v-focus
v-model="newTaskTitle"
ref="newTaskInput"
@keyup="resetEmptyTitleError"
@keydown.enter="handleEnter"
/>
<span class="icon is-small is-left">
<icon icon="tasks"/>
<icon icon="tasks" />
</span>
<quick-add-magic :highlight-hint-icon="taskAddHovered"/>
<QuickAddMagic :highlight-hint-icon="taskAddHovered" />
</p>
<p class="control">
<x-button
class="add-task-button"
:disabled="newTaskTitle === '' || loading || undefined"
@click="addTask()"
icon="plus"
:loading="loading"
:aria-label="$t('project.list.add')"
@click="addTask()"
>
<span class="button-text">
{{ $t('project.list.add') }}
@ -34,7 +37,10 @@
</p>
</div>
<Expandable :open="errorMessage !== ''">
<p class="pt-3 mt-0 help is-danger" v-if="errorMessage !== ''">
<p
v-if="errorMessage !== ''"
class="pt-3 mt-0 help is-danger"
>
{{ errorMessage }}
</p>
</Expandable>

View File

@ -22,26 +22,29 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
</script>
<template>
<div class="assignees-list" :class="{'is-inline': inline}">
<div
class="assignees-list"
:class="{'is-inline': inline}"
>
<span
v-for="user in assignees"
class="assignee"
:key="user.id"
class="assignee"
>
<User
:key="'user'+user.id"
:avatar-size="avatarSize"
:show-username="false"
:user="user"
:class="{'m-2': hasDelete, 'mr-3': !hasDelete}"
:class="{'m-2': hasDelete}"
/>
<BaseButton
:key="'delete'+user.id"
v-if="hasDelete"
@click="remove(user)"
:key="'delete'+user.id"
class="remove-assignee"
@click="remove(user)"
>
<icon icon="times"/>
<icon icon="times" />
</BaseButton>
</span>
</div>

View File

@ -2,37 +2,38 @@
<div class="attachments">
<h3>
<span class="icon is-grey">
<icon icon="paperclip"/>
<icon icon="paperclip" />
</span>
{{ $t('task.attachment.title') }}
</h3>
<input
v-if="editEnabled"
:disabled="loading || undefined"
@change="uploadNewAttachment()"
id="files"
multiple
ref="filesRef"
:disabled="loading || undefined"
multiple
type="file"
/>
<progress
v-if="attachmentService.uploadProgress > 0"
:value="attachmentService.uploadProgress"
class="progress is-primary"
max="100"
@change="uploadNewAttachment()"
>
{{ attachmentService.uploadProgress }}%
</progress>
<div class="files" v-if="attachments.length > 0">
<ProgressBar
v-if="attachmentService.uploadProgress > 0"
:value="attachmentService.uploadProgress * 100"
is-primary
/>
<div
v-if="attachments.length > 0"
class="files"
>
<!-- FIXME: don't use a for element that wraps other links / buttons
Instead: overlay element with button that is inside.
-->
<a
class="attachment"
v-for="a in attachments"
:key="a.id"
class="attachment"
@click="viewOrDownload(a)"
>
<div class="filename">
@ -46,7 +47,10 @@
</div>
<div class="info">
<p class="attachment-info-meta">
<i18n-t keypath="task.attachment.createdBy" scope="global">
<i18n-t
keypath="task.attachment.createdBy"
scope="global"
>
<span v-tooltip="formatDateLong(a.created)">
{{ formatDateSince(a.created) }}
</span>
@ -65,24 +69,24 @@
</p>
<p>
<BaseButton
v-tooltip="$t('task.attachment.downloadTooltip')"
class="attachment-info-meta-button"
@click.prevent.stop="downloadAttachment(a)"
v-tooltip="$t('task.attachment.downloadTooltip')"
>
{{ $t('misc.download') }}
</BaseButton>
<BaseButton
v-tooltip="$t('task.attachment.copyUrlTooltip')"
class="attachment-info-meta-button"
@click.stop="copyUrl(a)"
v-tooltip="$t('task.attachment.copyUrlTooltip')"
>
{{ $t('task.attachment.copyUrl') }}
</BaseButton>
<BaseButton
v-if="editEnabled"
v-tooltip="$t('task.attachment.deleteTooltip')"
class="attachment-info-meta-button"
@click.prevent.stop="setAttachmentToDelete(a)"
v-tooltip="$t('task.attachment.deleteTooltip')"
>
{{ $t('misc.delete') }}
</BaseButton>
@ -105,11 +109,11 @@
<x-button
v-if="editEnabled"
:disabled="loading"
@click="filesRef?.click()"
class="mb-4"
icon="cloud-upload-alt"
variant="secondary"
:shadow="false"
@click="filesRef?.click()"
>
{{ $t('task.attachment.upload') }}
</x-button>
@ -117,15 +121,17 @@
<!-- Dropzone -->
<Teleport to="body">
<div
v-if="editEnabled"
:class="{ hidden: !isOverDropZone }"
class="dropzone"
v-if="editEnabled"
>
<div class="drop-hint">
<div class="icon">
<icon icon="cloud-upload-alt"/>
<icon icon="cloud-upload-alt" />
</div>
<div class="hint">
{{ $t('task.attachment.drop') }}
</div>
<div class="hint">{{ $t('task.attachment.drop') }}</div>
</div>
</div>
</Teleport>
@ -142,7 +148,7 @@
<template #text>
<p>
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/>
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br>
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
</p>
</template>
@ -153,7 +159,10 @@
:enabled="attachmentImageBlobUrl !== null"
@close="attachmentImageBlobUrl = null"
>
<img :src="attachmentImageBlobUrl" alt=""/>
<img
:src="attachmentImageBlobUrl"
alt=""
>
</modal>
</div>
</template>
@ -163,6 +172,7 @@ import {ref, shallowReactive, computed} from 'vue'
import {useDropZone} from '@vueuse/core'
import User from '@/components/misc/user.vue'
import ProgressBar from '@/components/misc/ProgressBar.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment'
@ -179,9 +189,6 @@ import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useI18n} from 'vue-i18n'
const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'})
const {
task,
editEnabled = true,
@ -189,9 +196,10 @@ const {
task: ITask,
editEnabled: boolean,
}>()
// FIXME: this should go through the store
const emit = defineEmits(['task-changed'])
const emit = defineEmits(['taskChanged'])
const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'})
const attachmentService = shallowReactive(new AttachmentService())
@ -267,7 +275,7 @@ function copyUrl(attachment: IAttachment) {
async function setCoverImage(attachment: IAttachment | null) {
const updatedTask = await taskStore.setCoverImage(task, attachment)
emit('task-changed', updatedTask)
emit('taskChanged', updatedTask)
success({message: t('task.attachment.successfullyChangedCoverImage')})
}
</script>

View File

@ -1,9 +1,29 @@
<template>
<span v-if="checklist.total > 0" class="checklist-summary">
<svg width="12" height="12">
<circle stroke-width="2" fill="transparent" cx="50%" cy="50%" r="5"></circle>
<circle stroke-width="2" stroke-dasharray="31" :stroke-dashoffset="checklistCircleDone"
stroke-linecap="round" fill="transparent" cx="50%" cy="50%" r="5"></circle>
<span
v-if="checklist.total > 0"
class="checklist-summary"
>
<svg
width="12"
height="12"
>
<circle
stroke-width="2"
fill="transparent"
cx="50%"
cy="50%"
r="5"
/>
<circle
stroke-width="2"
stroke-dasharray="31"
:stroke-dashoffset="checklistCircleDone"
stroke-linecap="round"
fill="transparent"
cx="50%"
cy="50%"
r="5"
/>
</svg>
<span>{{ label }}</span>
</span>

View File

@ -1,20 +1,30 @@
<template>
<div class="content details" v-if="enabled">
<h3 v-if="canWrite || comments.length > 0" :class="{'d-print-none': comments.length === 0}">
<div
v-if="enabled"
class="content details"
>
<h3
v-if="canWrite || comments.length > 0"
:class="{'d-print-none': comments.length === 0}"
>
<span class="icon is-grey">
<icon :icon="['far', 'comments']"/>
<icon :icon="['far', 'comments']" />
</span>
{{ $t('task.comment.title') }}
</h3>
<div class="comments">
<span
class="is-inline-flex is-align-items-center"
v-if="taskCommentService.loading && saving === null && !creating"
class="is-inline-flex is-align-items-center"
>
<span class="loader is-inline-block mr-2"></span>
<span class="loader is-inline-block mr-2" />
{{ $t('task.comment.loading') }}
</span>
<div :key="c.id" class="media comment" v-for="c in comments">
<div
v-for="c in comments"
:key="c.id"
class="media comment"
>
<figure class="media-left is-hidden-mobile">
<img
:src="getAvatarUrl(c.author, 48)"
@ -22,7 +32,7 @@
class="image is-avatar"
height="48"
width="48"
/>
>
</figure>
<div class="media-content">
<div class="comment-info">
@ -32,9 +42,12 @@
class="image is-avatar d-print-none"
height="20"
width="20"
/>
>
<strong>{{ getDisplayName(c.author) }}</strong>&nbsp;
<span v-tooltip="formatDateLong(c.created)" class="has-text-grey">
<span
v-tooltip="formatDateLong(c.created)"
class="has-text-grey"
>
{{ formatDateSince(c.created) }}
</span>
<span
@ -45,32 +58,35 @@
</span>
<CustomTransition name="fade">
<span
class="is-inline-flex"
v-if="
taskCommentService.loading &&
saving === c.id
saving === c.id
"
class="is-inline-flex"
>
<span class="loader is-inline-block mr-2"></span>
<span class="loader is-inline-block mr-2" />
{{ $t('misc.saving') }}
</span>
<span
class="has-text-success"
v-else-if="
!taskCommentService.loading &&
saved === c.id
saved === c.id
"
class="has-text-success"
>
{{ $t('misc.saved') }}
</span>
</CustomTransition>
</div>
<editor
<Editor
v-model="c.comment"
:is-edit-enabled="canWrite && c.author.id === currentUserId"
:upload-callback="attachmentUpload"
:upload-enabled="true"
v-model="c.comment"
@update:model-value="
:bottom-actions="actions[c.id]"
:show-save="true"
initial-mode="preview"
@update:modelValue="
() => {
toggleEdit(c)
editCommentWithDelay()
@ -80,13 +96,13 @@
toggleEdit(c)
editComment()
}"
:bottom-actions="actions[c.id]"
:show-save="true"
initial-mode="preview"
/>
</div>
</div>
<div class="media comment d-print-none" v-if="canWrite">
<div
v-if="canWrite"
class="media comment d-print-none"
>
<figure class="media-left is-hidden-mobile">
<img
:src="userAvatar"
@ -94,7 +110,7 @@
class="image is-avatar"
height="48"
width="48"
/>
>
</figure>
<div class="media-content">
<div class="form">
@ -103,12 +119,14 @@
v-if="taskCommentService.loading && creating"
class="is-inline-flex"
>
<span class="loader is-inline-block mr-2"></span>
<span class="loader is-inline-block mr-2" />
{{ $t('task.comment.creating') }}
</span>
</CustomTransition>
<div class="field">
<editor
<Editor
v-if="editorActive"
v-model="newComment.comment"
:class="{
'is-loading':
taskCommentService.loading &&
@ -117,8 +135,6 @@
:upload-callback="attachmentUpload"
:upload-enabled="true"
:placeholder="$t('task.comment.placeholder')"
v-if="editorActive"
v-model="newComment.comment"
@save="addComment()"
/>
</div>
@ -141,11 +157,13 @@
@close="showDeleteModal = false"
@submit="() => deleteComment(commentToDelete)"
>
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
<template #header>
<span>{{ $t('task.comment.delete') }}</span>
</template>
<template #text>
<p>
{{ $t('task.comment.deleteText1') }}<br/>
{{ $t('task.comment.deleteText1') }}<br>
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
</p>
</template>

View File

@ -1,24 +1,42 @@
<template>
<p class="created">
<time :datetime="formatISO(task.created)" v-tooltip="formatDateLong(task.created)">
<i18n-t keypath="task.detail.created" scope="global">
<time
v-tooltip="formatDateLong(task.created)"
:datetime="formatISO(task.created)"
>
<i18n-t
keypath="task.detail.created"
scope="global"
>
<span>{{ formatDateSince(task.created) }}</span>
{{ getDisplayName(task.createdBy) }}
</i18n-t>
</time>
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
<br/>
<br>
<!-- Computed properties to show the actual date every time it gets updated -->
<time :datetime="formatISO(task.updated)" v-tooltip="updatedFormatted">
<i18n-t keypath="task.detail.updated" scope="global">
<time
v-tooltip="updatedFormatted"
:datetime="formatISO(task.updated)"
>
<i18n-t
keypath="task.detail.updated"
scope="global"
>
<span>{{ updatedSince }}</span>
</i18n-t>
</time>
</template>
<template v-if="task.done">
<br/>
<time :datetime="formatISO(task.doneAt)" v-tooltip="doneFormatted">
<i18n-t keypath="task.detail.doneAt" scope="global">
<br>
<time
v-tooltip="doneFormatted"
:datetime="formatISO(task.doneAt)"
>
<i18n-t
keypath="task.detail.doneAt"
scope="global"
>
<span>{{ doneSince }}</span>
</i18n-t>
</time>

View File

@ -6,33 +6,33 @@
<label class="label">{{ $t('task.deferDueDate.title') }}</label>
<div class="defer-days">
<x-button
@click.prevent.stop="() => deferDays(1)"
:shadow="false"
variant="secondary"
@click.prevent.stop="() => deferDays(1)"
>
{{ $t('task.deferDueDate.1day') }}
</x-button>
<x-button
@click.prevent.stop="() => deferDays(3)"
:shadow="false"
variant="secondary"
@click.prevent.stop="() => deferDays(3)"
>
{{ $t('task.deferDueDate.3days') }}
</x-button>
<x-button
@click.prevent.stop="() => deferDays(7)"
:shadow="false"
variant="secondary"
@click.prevent.stop="() => deferDays(7)"
>
{{ $t('task.deferDueDate.1week') }}
</x-button>
</div>
<flat-pickr
v-model="dueDate"
:class="{ disabled: taskService.loading }"
:config="flatPickerConfig"
:disabled="taskService.loading || undefined"
class="input"
v-model="dueDate"
/>
</div>
</template>

View File

@ -2,29 +2,35 @@
<div>
<h3>
<span class="icon is-grey">
<icon icon="align-left"/>
<icon icon="align-left" />
</span>
{{ $t('task.attributes.description') }}
<CustomTransition name="fade">
<span class="is-small is-inline-flex" v-if="loading && saving">
<span class="loader is-inline-block mr-2"></span>
<span
v-if="loading && saving"
class="is-small is-inline-flex"
>
<span class="loader is-inline-block mr-2" />
{{ $t('misc.saving') }}
</span>
<span class="is-small has-text-success" v-else-if="!loading && saved">
<icon icon="check"/>
<span
v-else-if="!loading && saved"
class="is-small has-text-success"
>
<icon icon="check" />
{{ $t('misc.saved') }}
</span>
</CustomTransition>
</h3>
<editor
<Editor
v-model="description"
class="tiptap__task-description"
:is-edit-enabled="canWrite"
:upload-callback="uploadCallback"
:placeholder="$t('task.description.placeholder')"
:show-save="true"
edit-shortcut="e"
v-model="description"
@update:model-value="saveWithDelay"
@update:modelValue="saveWithDelay"
@save="save"
/>
</div>

View File

@ -1,21 +1,31 @@
<template>
<Multiselect
v-model="assignees"
class="edit-assignees"
:class="{'has-assignees': assignees.length > 0}"
:loading="projectUserService.loading"
:placeholder="$t('task.assignee.placeholder')"
:multiple="true"
@search="findUser"
:search-results="foundUsers"
@select="addAssignee"
label="name"
:select-placeholder="$t('task.assignee.selectPlaceholder')"
v-model="assignees"
:autocomplete-enabled="false"
@search="findUser"
@select="addAssignee"
>
<template #items="{items}">
<assignee-list :assignees="items" :remove="removeAssignee" :disabled="disabled"/>
<AssigneeList
:assignees="items"
:remove="removeAssignee"
:disabled="disabled"
/>
</template>
<template #searchResult="{option: user}">
<user :avatar-size="24" :show-username="true" :user="user"/>
<User
:avatar-size="24"
:show-username="true"
:user="user"
/>
</template>
</Multiselect>
</template>
@ -115,3 +125,9 @@ async function findUser(query: string) {
})
}
</script>
<style lang="scss">
.edit-assignees.has-assignees.multiselect .input {
padding-left: 0;
}
</style>

View File

@ -1,37 +1,44 @@
<template>
<Multiselect
v-model="labels"
:loading="loading"
:placeholder="$t('task.label.placeholder')"
:multiple="true"
@search="findLabel"
:search-results="foundLabels"
@select="addLabel"
label="title"
:creatable="creatable"
@create="createAndAddLabel"
:create-placeholder="$t('task.label.createPlaceholder')"
v-model="labels"
:search-delay="10"
:close-after-select="false"
@search="findLabel"
@select="addLabel"
@create="createAndAddLabel"
>
<template #tag="{item: label}">
<span
:style="{'background': label.hexColor, 'color': label.textColor}"
class="tag">
class="tag"
>
<span>{{ label.title }}</span>
<BaseButton v-cy="'taskDetail.removeLabel'" @click="removeLabel(label)" class="delete is-small" />
<BaseButton
v-cy="'taskDetail.removeLabel'"
class="delete is-small"
@click="removeLabel(label)"
/>
</span>
</template>
<template #searchResult="{option}">
<span
v-if="typeof option === 'string'"
class="tag search-result">
class="tag search-result"
>
<span>{{ option }}</span>
</span>
<span
v-else
:style="{'background': option.hexColor, 'color': option.textColor}"
class="tag search-result">
class="tag search-result"
>
<span>{{ option.title }}</span>
</span>
</template>

View File

@ -1,8 +1,15 @@
<template>
<div class="heading">
<div class="flex is-align-items-center">
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
<Done class="heading__done" :is-done="task.done"/>
<BaseButton @click="copyUrl">
<h1 class="title task-id">
{{ textIdentifier }}
</h1>
</BaseButton>
<Done
class="heading__done"
:is-done="task.done"
/>
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
@ -12,10 +19,10 @@
<h1
class="title input"
:class="{'disabled': !canWrite}"
@blur="save(($event.target as HTMLInputElement).textContent as string)"
@keydown.enter.prevent.stop="($event.target as HTMLInputElement).blur()"
:contenteditable="canWrite ? true : undefined"
:spellcheck="false"
@blur="save(($event.target as HTMLInputElement).textContent as string)"
@keydown.enter.prevent.stop="($event.target as HTMLInputElement).blur()"
>
{{ task.title.trim() }}
</h1>
@ -24,14 +31,17 @@
v-if="loading && saving"
class="is-inline-flex is-align-items-center"
>
<span class="loader is-inline-block mr-2"></span>
<span class="loader is-inline-block mr-2" />
{{ $t('misc.saving') }}
</span>
<span
v-else-if="!loading && showSavedMessage"
class="has-text-success is-inline-flex is-align-content-center"
>
<icon icon="check" class="mr-2"/>
<icon
icon="check"
class="mr-2"
/>
{{ $t('misc.saved') }}
</span>
</CustomTransition>

View File

@ -4,10 +4,10 @@
:class="{
'is-loading': loadingInternal || loading,
'draggable': !(loadingInternal || loading),
'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color),
'has-custom-background-color': color !== TASK_DEFAULT_COLOR ? color : undefined,
'has-light-text': !colorIsDark(color),
'has-custom-background-color': color ?? undefined,
}"
:style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : undefined}"
:style="{'background-color': color ?? undefined}"
@click.exact="openTaskDetail()"
@click.ctrl="() => toggleTaskDone(task)"
@click.meta="() => toggleTaskDone(task)"
@ -17,10 +17,14 @@
:src="coverImageBlobUrl"
alt=""
class="cover-image"
/>
>
<div class="p-2">
<span class="task-id">
<Done class="kanban-card__done" :is-done="task.done" variant="small"/>
<Done
class="kanban-card__done"
:is-done="task.done"
variant="small"
/>
<template v-if="task.identifier === ''">
#{{ task.index }}
</template>
@ -29,45 +33,59 @@
</template>
</span>
<span
v-if="task.dueDate > 0"
v-tooltip="formatDateLong(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="due-date"
v-if="task.dueDate > 0"
v-tooltip="formatDateLong(task.dueDate)">
>
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
<icon :icon="['far', 'calendar-alt']" />
</span>
<time :datetime="formatISO(task.dueDate)">
{{ formatDateSince(task.dueDate) }}
</time>
</span>
<h3>{{ task.title }}</h3>
<progress
class="progress is-small"
<ProgressBar
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
{{ task.percentDone * 100 }}%
</progress>
class="task-progress"
:value="task.percentDone * 100"
/>
<div class="footer">
<labels :labels="task.labels"/>
<priority-label
<Labels :labels="task.labels" />
<PriorityLabel
:priority="task.priority"
:done="task.done"
class="is-inline-flex is-align-items-center"/>
<assignee-list
class="is-inline-flex is-align-items-center"
/>
<AssigneeList
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="24"
class="mr-1"
/>
<checklist-summary :task="task" class="checklist"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
<ChecklistSummary
:task="task"
class="checklist"
/>
<span
v-if="task.attachments.length > 0"
class="icon"
>
<icon icon="paperclip" />
</span>
<span v-if="!isEditorContentEmpty(task.description)" class="icon">
<icon icon="align-left"/>
<span
v-if="!isEditorContentEmpty(task.description)"
class="icon"
>
<icon icon="align-left" />
</span>
<span class="icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
<span
v-if="task.repeatAfter.amount > 0"
class="icon"
>
<icon icon="history" />
</span>
</div>
</div>
@ -79,11 +97,12 @@ import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import ProgressBar from '@/components/misc/ProgressBar.vue'
import Done from '@/components/misc/Done.vue'
import Labels from '@/components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary.vue'
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
import {getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import AttachmentService from '@/services/attachment'
@ -96,10 +115,6 @@ import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const router = useRouter()
const loadingInternal = ref(false)
const {
task,
loading = false,
@ -108,6 +123,10 @@ const {
loading: boolean,
}>()
const router = useRouter()
const loadingInternal = ref(false)
const color = computed(() => getHexColor(task.hexColor))
async function toggleTaskDone(task: ITask) {
@ -187,11 +206,6 @@ $task-background: var(--white);
word-break: break-word;
}
.progress {
margin: 8px 0 0 0;
width: 100%;
height: 0.5rem;
}
.due-date {
float: right;
@ -331,4 +345,10 @@ $task-background: var(--white);
.kanban-card__done {
margin-right: .25rem;
}
.task-progress {
margin: 8px 0 0 0;
width: 100%;
height: 0.5rem;
}
</style>

View File

@ -2,8 +2,8 @@
<div class="label-wrapper">
<XLabel
v-for="label in labels"
:label="label"
:key="label.id"
:label="label"
/>
</div>
</template>
@ -25,5 +25,9 @@ defineProps({
<style lang="scss" scoped>
.label-wrapper {
display: inline;
:deep(.tag) {
margin-bottom: .25rem;
}
}
</style>

View File

@ -4,17 +4,39 @@
v-model.number="percentDone"
:disabled="disabled || undefined"
>
<option value="0">0%</option>
<option value="0.1">10%</option>
<option value="0.2">20%</option>
<option value="0.3">30%</option>
<option value="0.4">40%</option>
<option value="0.5">50%</option>
<option value="0.6">60%</option>
<option value="0.7">70%</option>
<option value="0.8">80%</option>
<option value="0.9">90%</option>
<option value="1">100%</option>
<option value="0">
0%
</option>
<option value="0.1">
10%
</option>
<option value="0.2">
20%
</option>
<option value="0.3">
30%
</option>
<option value="0.4">
40%
</option>
<option value="0.5">
50%
</option>
<option value="0.6">
60%
</option>
<option value="0.7">
70%
</option>
<option value="0.8">
80%
</option>
<option value="0.9">
90%
</option>
<option value="1">
100%
</option>
</select>
</div>
</template>

View File

@ -1,10 +1,14 @@
<template>
<span
v-if="!done && (showAll || priority >= priorities.HIGH)"
:class="{'not-so-high': priority === priorities.HIGH, 'high-priority': priority >= priorities.HIGH}"
class="priority-label"
v-if="!done && (showAll || priority >= priorities.HIGH)">
<span class="icon" v-if="priority >= priorities.HIGH">
<icon icon="exclamation"/>
>
<span
v-if="priority >= priorities.HIGH"
class="icon"
>
<icon icon="exclamation" />
</span>
<span>
<template v-if="priority === priorities.UNSET">{{ $t('task.priority.unset') }}</template>
@ -14,8 +18,11 @@
<template v-if="priority === priorities.URGENT">{{ $t('task.priority.urgent') }}</template>
<template v-if="priority === priorities.DO_NOW">{{ $t('task.priority.doNow') }}</template>
</span>
<span class="icon pr-0" v-if="priority === priorities.DO_NOW">
<icon icon="exclamation"/>
<span
v-if="priority === priorities.DO_NOW"
class="icon pr-0"
>
<icon icon="exclamation" />
</span>
</span>
</template>

View File

@ -2,15 +2,27 @@
<div class="select">
<select
v-model="priority"
@change="updateData"
:disabled="disabled || undefined"
@change="updateData"
>
<option :value="PRIORITIES.UNSET">{{ $t('task.priority.unset') }}</option>
<option :value="PRIORITIES.LOW">{{ $t('task.priority.low') }}</option>
<option :value="PRIORITIES.MEDIUM">{{ $t('task.priority.medium') }}</option>
<option :value="PRIORITIES.HIGH">{{ $t('task.priority.high') }}</option>
<option :value="PRIORITIES.URGENT">{{ $t('task.priority.urgent') }}</option>
<option :value="PRIORITIES.DO_NOW">{{ $t('task.priority.doNow') }}</option>
<option :value="PRIORITIES.UNSET">
{{ $t('task.priority.unset') }}
</option>
<option :value="PRIORITIES.LOW">
{{ $t('task.priority.low') }}
</option>
<option :value="PRIORITIES.MEDIUM">
{{ $t('task.priority.medium') }}
</option>
<option :value="PRIORITIES.HIGH">
{{ $t('task.priority.high') }}
</option>
<option :value="PRIORITIES.URGENT">
{{ $t('task.priority.urgent') }}
</option>
<option :value="PRIORITIES.DO_NOW">
{{ $t('task.priority.doNow') }}
</option>
</select>
</div>
</template>

View File

@ -6,12 +6,15 @@
label="title"
:select-placeholder="$t('project.searchSelect')"
:model-value="project"
@update:model-value="Object.assign(project, $event)"
@update:modelValue="Object.assign(project, $event)"
@select="select"
@search="findProjects"
>
<template #searchResult="{option}">
<span class="has-text-grey" v-if="projectStore.getAncestors(option).length > 1">
<span
v-if="projectStore.getAncestors(option).length > 1"
class="has-text-grey"
>
{{ projectStore.getAncestors(option).filter(p => p.id !== option.id).map(p => getProjectTitle(p)).join(' &gt; ') }} &gt;
</span>
{{ getProjectTitle(option) }}

View File

@ -1,22 +1,25 @@
<template>
<template v-if="mode !== 'disabled' && prefixes !== undefined">
<BaseButton
@click="() => visible = true"
class="icon is-small show-helper-text"
v-tooltip="$t('task.quickAddMagic.hint')"
class="icon is-small show-helper-text"
:aria-label="$t('task.quickAddMagic.hint')"
:class="{'is-highlighted': highlightHintIcon}"
@click="() => visible = true"
>
<icon :icon="['far', 'circle-question']"/>
<icon :icon="['far', 'circle-question']" />
</BaseButton>
<modal
:enabled="visible"
@close="() => visible = false"
transition-name="fade"
:overflow="true"
variant="hint-modal"
@close="() => visible = false"
>
<card class="has-no-shadow" :title="$t('task.quickAddMagic.title')">
<card
class="has-no-shadow"
:title="$t('task.quickAddMagic.title')"
>
<p>{{ $t('task.quickAddMagic.intro') }}</p>
<h3>{{ $t('task.attributes.labels') }}</h3>
@ -102,15 +105,15 @@ import BaseButton from '@/components/base/BaseButton.vue'
import {PREFIXES} from '@/modules/parseTaskText'
import {useAuthStore} from '@/stores/auth'
defineProps<{
highlightHintIcon?: boolean,
}>()
const authStore = useAuthStore()
const visible = ref(false)
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
defineProps<{
highlightHintIcon?: boolean,
}>()
const prefixes = computed(() => PREFIXES[mode.value])
</script>

View File

@ -2,41 +2,53 @@
<div class="task-relations">
<x-button
v-if="editEnabled && Object.keys(relatedTasks).length > 0"
@click="showNewRelationForm = !showNewRelationForm"
id="showRelatedTasksFormButton"
v-tooltip="$t('task.relation.add')"
class="is-pulled-right add-task-relation-button d-print-none"
:class="{'is-active': showNewRelationForm}"
v-tooltip="$t('task.relation.add')"
variant="secondary"
icon="plus"
:shadow="false"
id="showRelatedTasksFormButton"
@click="showNewRelationForm = !showNewRelationForm"
/>
<transition-group name="fade">
<template v-if="editEnabled && showCreate">
<label class="label" key="label">
<label
key="label"
class="label"
>
{{ $t('task.relation.new') }}
<CustomTransition name="fade">
<span class="is-inline-flex" v-if="taskRelationService.loading">
<span class="loader is-inline-block mr-2"></span>
<span
v-if="taskRelationService.loading"
class="is-inline-flex"
>
<span class="loader is-inline-block mr-2" />
{{ $t('misc.saving') }}
</span>
<span class="has-text-success" v-else-if="!taskRelationService.loading && saved">
<span
v-else-if="!taskRelationService.loading && saved"
class="has-text-success"
>
{{ $t('misc.saved') }}
</span>
</CustomTransition>
</label>
<div class="field" key="field-search">
<div
key="field-search"
class="field"
>
<Multiselect
v-model="newTaskRelation.task"
v-focus
:placeholder="$t('task.relation.searchPlaceholder')"
@search="findTasks"
:loading="taskService.loading"
:search-results="mappedFoundTasks"
label="title"
v-model="newTaskRelation.task"
:creatable="true"
:create-placeholder="$t('task.relation.createPlaceholder')"
@search="findTasks"
@create="createAndRelateTask"
v-focus
>
<template #searchResult="{option: task}">
<span
@ -45,62 +57,86 @@
:class="{'is-strikethrough': task.done}"
>
<span
class="different-project"
v-if="task.projectId !== projectId"
class="different-project"
>
<span
v-if="task.differentProject !== null"
v-tooltip="$t('task.relation.differentProject')">
v-tooltip="$t('task.relation.differentProject')"
>
{{ task.differentProject }} >
</span>
</span>
{{ task.title }}
</span>
<span class="search-result" v-else>
<span
v-else
class="search-result"
>
{{ task }}
</span>
</template>
</Multiselect>
</div>
<div class="field has-addons mb-4" key="field-kind">
<div
key="field-kind"
class="field has-addons mb-4"
>
<div class="control is-expanded">
<div class="select is-fullwidth has-defaults">
<select v-model="newTaskRelation.kind">
<option value="unset">{{ $t('task.relation.select') }}</option>
<option :key="`option_${rk}`" :value="rk" v-for="rk in RELATION_KINDS">
<option value="unset">
{{ $t('task.relation.select') }}
</option>
<option
v-for="rk in RELATION_KINDS"
:key="`option_${rk}`"
:value="rk"
>
{{ $t(`task.relation.kinds.${rk}`, 1) }}
</option>
</select>
</div>
</div>
<div class="control">
<x-button @click="addTaskRelation()">{{ $t('task.relation.add') }}</x-button>
<x-button @click="addTaskRelation()">
{{ $t('task.relation.add') }}
</x-button>
</div>
</div>
</template>
</transition-group>
<div :key="rts.kind" class="related-tasks" v-for="rts in mappedRelatedTasks">
<div
v-for="rts in mappedRelatedTasks"
:key="rts.kind"
class="related-tasks"
>
<span class="title">{{ rts.title }}</span>
<div class="tasks">
<div :key="t.id" class="task" v-for="t in rts.tasks">
<div
v-for="t in rts.tasks"
:key="t.id"
class="task"
>
<div class="is-flex is-align-items-center">
<Fancycheckbox
class="task-done-checkbox"
v-model="t.done"
@update:model-value="toggleTaskDone(t)"
class="task-done-checkbox"
@update:modelValue="toggleTaskDone(t)"
/>
<router-link
:to="{ name: route.name as string, params: { id: t.id } }"
:class="{ 'is-strikethrough': t.done}"
>
<span
class="different-project"
v-if="t.projectId !== projectId"
class="different-project"
>
<span
v-if="t.differentProject !== null"
v-tooltip="$t('task.relation.differentProject')">
v-tooltip="$t('task.relation.differentProject')"
>
{{ t.differentProject }} >
</span>
</span>
@ -109,18 +145,21 @@
</div>
<BaseButton
v-if="editEnabled"
class="remove"
@click="setRelationToDelete({
relationKind: rts.kind,
otherTaskId: t.id
})"
class="remove"
>
<icon icon="trash-alt"/>
<icon icon="trash-alt" />
</BaseButton>
</div>
</div>
</div>
<p class="none" v-if="showNoRelationsNotice && Object.keys(relatedTasks).length === 0">
<p
v-if="showNoRelationsNotice && Object.keys(relatedTasks).length === 0"
class="none"
>
{{ $t('task.relation.noneYet') }}
</p>
@ -129,11 +168,13 @@
@close="relationToDelete = undefined"
@submit="removeTaskRelation()"
>
<template #header><span>{{ $t('task.relation.delete') }}</span></template>
<template #header>
<span>{{ $t('task.relation.delete') }}</span>
</template>
<template #text>
<p>
{{ $t('task.relation.deleteText1') }}<br/>
{{ $t('task.relation.deleteText1') }}<br>
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
</p>
</template>

View File

@ -10,8 +10,15 @@
</SimpleButton>
</template>
<template #content="{isOpen, close}">
<Card class="reminder-options-popup" :class="{'is-open': isOpen}" :padding="false">
<div class="options" v-if="activeForm === null">
<Card
class="reminder-options-popup"
:class="{'is-open': isOpen}"
:padding="false"
>
<div
v-if="activeForm === null"
class="options"
>
<SimpleButton
v-for="(p, k) in presets"
:key="k"
@ -22,16 +29,16 @@
{{ formatReminder(p) }}
</SimpleButton>
<SimpleButton
@click="showFormSwitch = 'relative'"
class="option-button"
:class="{'currently-active': typeof modelValue !== 'undefined' && modelValue?.relativeTo !== null && presets.find(p => p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo) === undefined}"
@click="showFormSwitch = 'relative'"
>
{{ $t('task.reminder.custom') }}
</SimpleButton>
<SimpleButton
@click="showFormSwitch = 'absolute'"
class="option-button"
:class="{'currently-active': modelValue?.relativeTo === null}"
@click="showFormSwitch = 'absolute'"
>
{{ $t('task.reminder.dateAndTime') }}
</SimpleButton>
@ -46,7 +53,7 @@
<DatepickerInline
v-if="activeForm === 'absolute'"
v-model="reminderDate"
@update:modelValue="setReminderDate(close)"
@update:modelValue="setReminderDateAndClose(close)"
/>
<x-button
@ -81,8 +88,6 @@ import TaskReminderModel from '@/models/taskReminder'
import Card from '@/components/misc/card.vue'
import SimpleButton from '@/components/input/SimpleButton.vue'
const {t} = useI18n({useScope: 'global'})
const {
modelValue,
clearAfterUpdate = false,
@ -95,6 +100,8 @@ const {
const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'})
const reminder = ref<ITaskReminder>(new TaskReminderModel())
const presets = computed<TaskReminderModel[]>(() => [
@ -105,7 +112,7 @@ const presets = computed<TaskReminderModel[]>(() => [
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: defaultRelativeTo},
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: defaultRelativeTo},
])
const reminderDate = ref(null)
const reminderDate = ref<Date|null>(null)
type availableForms = null | 'relative' | 'absolute'
@ -135,7 +142,17 @@ const reminderText = computed(() => {
watch(
() => modelValue,
(newReminder) => {
reminder.value = newReminder || new TaskReminderModel()
if(newReminder) {
reminder.value = newReminder
if(newReminder.relativeTo === null) {
reminderDate.value = new Date(newReminder.reminder)
}
return
}
reminder.value = new TaskReminderModel()
},
{immediate: true},
)
@ -148,7 +165,7 @@ function updateData() {
}
}
function setReminderDate(close) {
function setReminderDateAndClose(close) {
reminder.value.reminder = reminderDate.value === null
? null
: new Date(reminderDate.value)

Some files were not shown because too many files have changed in this diff Show More