Compare commits
38 Commits
1d2fd2f88d
...
7634c6622c
Author | SHA1 | Date | |
---|---|---|---|
7634c6622c | |||
|
83a7032b6f | ||
49261a6fcc | |||
5630c90dee | |||
47d589002c | |||
99e2161c09 | |||
20f61baf03 | |||
4e6b99544e | |||
d57e1909c4 | |||
99d8fbdfa7 | |||
442d0342a9 | |||
a4b369470a | |||
0ca73e0851 | |||
9fc829115f | |||
1e19548563 | |||
c327d86a71 | |||
3044560759 | |||
c3f85fcb19 | |||
53434952d3 | |||
e9b0640660 | |||
ae57e5d314 | |||
6e7928b2e4 | |||
47639b00f8 | |||
|
e63cecceca | ||
|
55e2e323ed | ||
f7e22c8c56 | |||
a9fb306e46 | |||
58a1f46668 | |||
6cbbe17bd8 | |||
c01957aae2 | |||
1ad03877fb | |||
fc72a82a2a | |||
63ef09b020 | |||
|
311b1d7594 | ||
cade3df3e9 | |||
37975c1931 | |||
0d500182e7 | |||
|
f647d6e9b4 |
|
@ -57,6 +57,7 @@ ENV VIKUNJA_SENTRY_ENABLED false
|
||||||
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
||||||
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
|
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
|
||||||
ENV VIKUNJA_ALLOW_ICON_CHANGES true
|
ENV VIKUNJA_ALLOW_ICON_CHANGES true
|
||||||
|
ENV VIKUNJA_CUSTOM_LOGO_URL ""
|
||||||
|
|
||||||
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
||||||
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
|
||||||
const testAndAssertFailed = fixture => {
|
const testAndAssertFailed = fixture => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
||||||
|
@ -13,26 +14,28 @@ const testAndAssertFailed = fixture => {
|
||||||
cy.get('div.message.danger').contains('Wrong username or password.')
|
cy.get('div.message.danger').contains('Wrong username or password.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = 'test'
|
const credentials = {
|
||||||
|
username: 'test',
|
||||||
|
password: '1234',
|
||||||
|
}
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
cy.get('input[id=username]').type(credentials.username)
|
||||||
|
cy.get('input[id=password]').type(credentials.password)
|
||||||
|
cy.get('.button').contains('Login').click()
|
||||||
|
cy.url().should('include', '/')
|
||||||
|
}
|
||||||
|
|
||||||
context('Login', () => {
|
context('Login', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UserFactory.create(1, {username})
|
UserFactory.create(1, {username: credentials.username})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log in with the right credentials', () => {
|
it('Should log in with the right credentials', () => {
|
||||||
const fixture = {
|
|
||||||
username: 'test',
|
|
||||||
password: '1234',
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.visit('/login')
|
cy.visit('/login')
|
||||||
cy.get('input[id=username]').type(fixture.username)
|
login()
|
||||||
cy.get('input[id=password]').type(fixture.password)
|
|
||||||
cy.get('.button').contains('Login').click()
|
|
||||||
cy.url().should('include', '/')
|
|
||||||
cy.clock(1625656161057) // 13:00
|
cy.clock(1625656161057) // 13:00
|
||||||
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
|
cy.get('h2').should('contain', `Hi ${credentials.username}!`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a bad password', () => {
|
it('Should fail with a bad password', () => {
|
||||||
|
@ -57,4 +60,15 @@ context('Login', () => {
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.url().should('include', '/login')
|
cy.url().should('include', '/login')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should redirect to the previous route after logging in', () => {
|
||||||
|
const projects = ProjectFactory.create(1)
|
||||||
|
cy.visit(`/projects/${projects[0].id}/list`)
|
||||||
|
|
||||||
|
cy.url().should('include', '/login')
|
||||||
|
|
||||||
|
login()
|
||||||
|
|
||||||
|
cy.url().should('include', `/projects/${projects[0].id}/list`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,5 +13,6 @@ sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g"
|
||||||
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
||||||
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
|
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
|
||||||
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
|
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
|
||||||
|
sed -ri "s:^(\s*window.CUSTOM_LOGO_URL\s*=)\s*.+:\1 ${VIKUNJA_CUSTOM_LOGO_URL}:g" /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
date -uIseconds | xargs echo 'info: started at'
|
date -uIseconds | xargs echo 'info: started at'
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
||||||
// Allow changing the logo and other icons based on various occasions throughout the year.
|
// Allow changing the logo and other icons based on various occasions throughout the year.
|
||||||
window.ALLOW_ICON_CHANGES = true
|
window.ALLOW_ICON_CHANGES = true
|
||||||
|
// Allow using a custom logo via external URL.
|
||||||
|
window.CUSTOM_LOGO_URL = ''
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
42
package.json
42
package.json
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://vikunja.io/",
|
"homepage": "https://vikunja.io/",
|
||||||
"funding": "https://opencollective.com/vikunja",
|
"funding": "https://opencollective.com/vikunja",
|
||||||
"packageManager": "pnpm@8.6.12",
|
"packageManager": "pnpm@8.7.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"todo",
|
"todo",
|
||||||
"productivity",
|
"productivity",
|
||||||
|
@ -51,11 +51,11 @@
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
"@github/hotkey": "2.0.1",
|
"@github/hotkey": "2.0.1",
|
||||||
"@infectoone/vue-ganttastic": "2.1.4",
|
"@infectoone/vue-ganttastic": "2.1.4",
|
||||||
"@intlify/unplugin-vue-i18n": "0.12.3",
|
"@intlify/unplugin-vue-i18n": "0.13.0",
|
||||||
"@kyvg/vue3-notification": "2.9.1",
|
"@kyvg/vue3-notification": "2.9.1",
|
||||||
"@sentry/tracing": "7.60.0",
|
"@sentry/tracing": "7.60.0",
|
||||||
"@sentry/vue": "7.60.0",
|
"@sentry/vue": "7.60.0",
|
||||||
"@vueuse/core": "10.3.0",
|
"@vueuse/core": "10.4.0",
|
||||||
"axios": "1.4.0",
|
"axios": "1.4.0",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
|
@ -92,53 +92,53 @@
|
||||||
"@cypress/vite-dev-server": "5.0.5",
|
"@cypress/vite-dev-server": "5.0.5",
|
||||||
"@cypress/vue": "5.0.5",
|
"@cypress/vue": "5.0.5",
|
||||||
"@faker-js/faker": "8.0.2",
|
"@faker-js/faker": "8.0.2",
|
||||||
"@histoire/plugin-screenshot": "0.16.5",
|
"@histoire/plugin-screenshot": "0.17.0",
|
||||||
"@histoire/plugin-vue": "0.16.5",
|
"@histoire/plugin-vue": "0.17.1",
|
||||||
"@rushstack/eslint-patch": "1.3.3",
|
"@rushstack/eslint-patch": "1.3.3",
|
||||||
"@tsconfig/node18": "18.2.0",
|
"@tsconfig/node18": "18.2.1",
|
||||||
"@types/codemirror": "5.60.8",
|
"@types/codemirror": "5.60.9",
|
||||||
"@types/dompurify": "3.0.2",
|
"@types/dompurify": "3.0.2",
|
||||||
"@types/flexsearch": "0.7.3",
|
"@types/flexsearch": "0.7.3",
|
||||||
"@types/is-touch-device": "1.0.0",
|
"@types/is-touch-device": "1.0.0",
|
||||||
"@types/lodash.debounce": "4.0.7",
|
"@types/lodash.debounce": "4.0.7",
|
||||||
"@types/marked": "5.0.1",
|
"@types/marked": "5.0.1",
|
||||||
"@types/node": "18.17.6",
|
"@types/node": "18.17.12",
|
||||||
"@types/postcss-preset-env": "7.7.0",
|
"@types/postcss-preset-env": "7.7.0",
|
||||||
"@types/sortablejs": "1.15.1",
|
"@types/sortablejs": "1.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "6.4.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"@typescript-eslint/parser": "6.4.0",
|
"@typescript-eslint/parser": "6.5.0",
|
||||||
"@vitejs/plugin-legacy": "4.1.1",
|
"@vitejs/plugin-legacy": "4.1.1",
|
||||||
"@vitejs/plugin-vue": "4.3.2",
|
"@vitejs/plugin-vue": "4.3.4",
|
||||||
"@vue/eslint-config-typescript": "11.0.3",
|
"@vue/eslint-config-typescript": "11.0.3",
|
||||||
"@vue/test-utils": "2.4.1",
|
"@vue/test-utils": "2.4.1",
|
||||||
"@vue/tsconfig": "0.4.0",
|
"@vue/tsconfig": "0.4.0",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"browserslist": "4.21.10",
|
"browserslist": "4.21.10",
|
||||||
"caniuse-lite": "1.0.30001522",
|
"caniuse-lite": "1.0.30001524",
|
||||||
"css-has-pseudo": "6.0.0",
|
"css-has-pseudo": "6.0.0",
|
||||||
"csstype": "3.1.2",
|
"csstype": "3.1.2",
|
||||||
"cypress": "12.17.4",
|
"cypress": "12.17.4",
|
||||||
"esbuild": "0.19.2",
|
"esbuild": "0.19.2",
|
||||||
"eslint": "8.47.0",
|
"eslint": "8.48.0",
|
||||||
"eslint-plugin-vue": "9.17.0",
|
"eslint-plugin-vue": "9.17.0",
|
||||||
"happy-dom": "10.10.4",
|
"happy-dom": "10.11.1",
|
||||||
"histoire": "0.16.5",
|
"histoire": "0.17.0",
|
||||||
"postcss": "8.4.28",
|
"postcss": "8.4.28",
|
||||||
"postcss-easing-gradients": "3.0.1",
|
"postcss-easing-gradients": "3.0.1",
|
||||||
"postcss-easings": "4.0.0",
|
"postcss-easings": "4.0.0",
|
||||||
"postcss-focus-within": "8.0.0",
|
"postcss-focus-within": "8.0.0",
|
||||||
"postcss-preset-env": "9.1.1",
|
"postcss-preset-env": "9.1.2",
|
||||||
"rollup": "3.28.0",
|
"rollup": "3.28.1",
|
||||||
"rollup-plugin-visualizer": "5.9.2",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"sass": "1.66.1",
|
"sass": "1.66.1",
|
||||||
"start-server-and-test": "2.0.0",
|
"start-server-and-test": "2.0.0",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.2.2",
|
||||||
"vite": "4.4.9",
|
"vite": "4.4.9",
|
||||||
"vite-plugin-inject-preload": "1.3.2",
|
"vite-plugin-inject-preload": "1.3.3",
|
||||||
"vite-plugin-pwa": "0.16.4",
|
"vite-plugin-pwa": "0.16.4",
|
||||||
"vite-plugin-sentry": "1.3.0",
|
"vite-plugin-sentry": "1.3.0",
|
||||||
"vite-svg-loader": "4.0.0",
|
"vite-svg-loader": "4.0.0",
|
||||||
"vitest": "0.34.2",
|
"vitest": "0.34.3",
|
||||||
"vue-tsc": "1.8.8",
|
"vue-tsc": "1.8.8",
|
||||||
"wait-on": "7.0.1",
|
"wait-on": "7.0.1",
|
||||||
"workbox-cli": "7.0.0"
|
"workbox-cli": "7.0.0"
|
||||||
|
|
700
pnpm-lock.yaml
700
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -9,15 +9,21 @@ import {MILLISECONDS_A_HOUR} from '@/constants/date'
|
||||||
const now = useNow({
|
const now = useNow({
|
||||||
interval: MILLISECONDS_A_HOUR,
|
interval: MILLISECONDS_A_HOUR,
|
||||||
})
|
})
|
||||||
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 6 ? LogoFullPride : LogoFull)
|
||||||
|
const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Logo alt="Vikunja" class="logo" />
|
<div>
|
||||||
|
<Logo v-if="!CustomLogo" alt="Vikunja" class="logo" />
|
||||||
|
<img v-show="CustomLogo" :src="CustomLogo" alt="Vikunja" class="logo" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logo {
|
.logo {
|
||||||
color: var(--logo-text-color);
|
color: var(--logo-text-color);
|
||||||
|
max-width: 168px;
|
||||||
|
max-height: 48px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -60,6 +60,14 @@
|
||||||
:can-collapse="false"
|
:can-collapse="false"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<nav class="menu" v-if="savedFilterProjects">
|
||||||
|
<ProjectsNavigation
|
||||||
|
:model-value="savedFilterProjects"
|
||||||
|
:can-edit-order="false"
|
||||||
|
:can-collapse="false"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<nav class="menu">
|
<nav class="menu">
|
||||||
<ProjectsNavigation
|
<ProjectsNavigation
|
||||||
|
@ -91,6 +99,7 @@ const projectStore = useProjectStore()
|
||||||
|
|
||||||
const projects = computed(() => projectStore.notArchivedRootProjects)
|
const projects = computed(() => projectStore.notArchivedRootProjects)
|
||||||
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
||||||
|
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="!project.isArchived"
|
v-if="!project.isArchived && project.id > -1"
|
||||||
class="favorite"
|
class="favorite"
|
||||||
:class="{'is-favorite': project.isFavorite}"
|
:class="{'is-favorite': project.isFavorite}"
|
||||||
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
||||||
|
|
|
@ -157,7 +157,7 @@
|
||||||
<template
|
<template
|
||||||
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
|
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ $t('project.lists') }}</label>
|
<label class="label">{{ $t('project.projects') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SelectProject
|
<SelectProject
|
||||||
v-model="entities.projects"
|
v-model="entities.projects"
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
{{ hintText }}
|
{{ hintText }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<quick-add-magic class="p-2 modal-container-smaller" v-if="isNewTaskCommand"/>
|
<quick-add-magic v-if="isNewTaskCommand"/>
|
||||||
|
|
||||||
<div class="results" v-if="selectedCmd === null">
|
<div class="results" v-if="selectedCmd === null">
|
||||||
<div v-for="(r, k) in results" :key="k" class="result">
|
<div v-for="(r, k) in results" :key="k" class="result">
|
||||||
|
@ -44,7 +44,18 @@
|
||||||
@keyup.prevent.enter="doAction(r.type, i)"
|
@keyup.prevent.enter="doAction(r.type, i)"
|
||||||
@keyup.prevent.esc="searchInput?.focus()"
|
@keyup.prevent.esc="searchInput?.focus()"
|
||||||
>
|
>
|
||||||
{{ i.title }}
|
<template v-if="r.type === ACTION_TYPE.LABELS">
|
||||||
|
<x-label :label="i"/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="r.type === ACTION_TYPE.TASK">
|
||||||
|
<single-task-inline-readonly
|
||||||
|
:task="i"
|
||||||
|
:show-project="true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ i.title }}
|
||||||
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,6 +77,8 @@ import ProjectModel from '@/models/project'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||||
|
import XLabel from '@/components/tasks/partials/label.vue'
|
||||||
|
import SingleTaskInlineReadonly from '@/components/tasks/partials/singleTaskInlineReadonly.vue'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
@ -97,6 +110,7 @@ enum ACTION_TYPE {
|
||||||
TASK = 'task',
|
TASK = 'task',
|
||||||
PROJECT = 'project',
|
PROJECT = 'project',
|
||||||
TEAM = 'team',
|
TEAM = 'team',
|
||||||
|
LABELS = 'labels',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum COMMAND_TYPE {
|
enum COMMAND_TYPE {
|
||||||
|
@ -134,24 +148,38 @@ function closeQuickActions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundProjects = computed(() => {
|
const foundProjects = computed(() => {
|
||||||
const { project } = parsedQuery.value
|
const {project, text, labels, assignees} = parsedQuery.value
|
||||||
if (
|
|
||||||
searchMode.value === SEARCH_MODE.ALL ||
|
if (project !== null) {
|
||||||
searchMode.value === SEARCH_MODE.PROJECTS ||
|
return projectStore.searchProject(project ?? text)
|
||||||
project === null
|
.filter(p => Boolean(p))
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
if (labels.length > 0 || assignees.length > 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = getHistory()
|
if (text === '') {
|
||||||
const allProjects = [
|
const history = getHistory()
|
||||||
...new Set([
|
return history.map((p) => projectStore.projects[p.id])
|
||||||
...history.map((l) => projectStore.projects[l.id]),
|
.filter(p => Boolean(p))
|
||||||
...projectStore.searchProject(project),
|
}
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
return allProjects.filter(l => Boolean(l))
|
return projectStore.searchProject(project ?? text)
|
||||||
|
.filter(p => Boolean(p))
|
||||||
|
})
|
||||||
|
|
||||||
|
const foundLabels = computed(() => {
|
||||||
|
const {labels, text} = parsedQuery.value
|
||||||
|
if (text === '' && labels.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labels.length > 0) {
|
||||||
|
return labelStore.filterLabelsByQuery([], labels[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelStore.filterLabelsByQuery([], text)
|
||||||
})
|
})
|
||||||
|
|
||||||
// FIXME: use fuzzysearch
|
// FIXME: use fuzzysearch
|
||||||
|
@ -172,15 +200,20 @@ const results = computed<Result[]>(() => {
|
||||||
title: t('quickActions.commands'),
|
title: t('quickActions.commands'),
|
||||||
items: foundCommands.value,
|
items: foundCommands.value,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: ACTION_TYPE.PROJECT,
|
||||||
|
title: t('quickActions.projects'),
|
||||||
|
items: foundProjects.value,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: ACTION_TYPE.TASK,
|
type: ACTION_TYPE.TASK,
|
||||||
title: t('quickActions.tasks'),
|
title: t('quickActions.tasks'),
|
||||||
items: foundTasks.value,
|
items: foundTasks.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ACTION_TYPE.PROJECT,
|
type: ACTION_TYPE.LABELS,
|
||||||
title: t('quickActions.projects'),
|
title: t('quickActions.labels'),
|
||||||
items: foundProjects.value,
|
items: foundLabels.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ACTION_TYPE.TEAM,
|
type: ACTION_TYPE.TEAM,
|
||||||
|
@ -190,7 +223,7 @@ const results = computed<Result[]>(() => {
|
||||||
].filter((i) => i.items.length > 0)
|
].filter((i) => i.items.length > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = computed(() =>
|
const loading = computed(() =>
|
||||||
taskService.loading ||
|
taskService.loading ||
|
||||||
projectStore.isLoading ||
|
projectStore.isLoading ||
|
||||||
teamService.loading,
|
teamService.loading,
|
||||||
|
@ -262,10 +295,12 @@ const searchMode = computed(() => {
|
||||||
if (query.value === '') {
|
if (query.value === '') {
|
||||||
return SEARCH_MODE.ALL
|
return SEARCH_MODE.ALL
|
||||||
}
|
}
|
||||||
const { text, project, labels, assignees } = parsedQuery.value
|
|
||||||
|
const {text, project, labels, assignees} = parsedQuery.value
|
||||||
if (assignees.length === 0 && text !== '') {
|
if (assignees.length === 0 && text !== '') {
|
||||||
return SEARCH_MODE.TASKS
|
return SEARCH_MODE.TASKS
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
assignees.length === 0 &&
|
assignees.length === 0 &&
|
||||||
project !== null &&
|
project !== null &&
|
||||||
|
@ -274,6 +309,7 @@ const searchMode = computed(() => {
|
||||||
) {
|
) {
|
||||||
return SEARCH_MODE.PROJECTS
|
return SEARCH_MODE.PROJECTS
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
assignees.length > 0 &&
|
assignees.length > 0 &&
|
||||||
project === null &&
|
project === null &&
|
||||||
|
@ -282,6 +318,7 @@ const searchMode = computed(() => {
|
||||||
) {
|
) {
|
||||||
return SEARCH_MODE.TEAMS
|
return SEARCH_MODE.TEAMS
|
||||||
}
|
}
|
||||||
|
|
||||||
return SEARCH_MODE.ALL
|
return SEARCH_MODE.ALL
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -292,12 +329,12 @@ const isNewTaskCommand = computed(() => (
|
||||||
|
|
||||||
const taskSearchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
const taskSearchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
type Filter = {by: string, value: string | number, comparator: string}
|
type Filter = { by: string, value: string | number, comparator: string }
|
||||||
|
|
||||||
function filtersToParams(filters: Filter[]) {
|
function filtersToParams(filters: Filter[]) {
|
||||||
const filter_by : Filter['by'][] = []
|
const filter_by: Filter['by'][] = []
|
||||||
const filter_value : Filter['value'][] = []
|
const filter_value: Filter['value'][] = []
|
||||||
const filter_comparator : Filter['comparator'][] = []
|
const filter_comparator: Filter['comparator'][] = []
|
||||||
|
|
||||||
filters.forEach(({by, value, comparator}) => {
|
filters.forEach(({by, value, comparator}) => {
|
||||||
filter_by.push(by)
|
filter_by.push(by)
|
||||||
|
@ -315,7 +352,8 @@ function filtersToParams(filters: Filter[]) {
|
||||||
function searchTasks() {
|
function searchTasks() {
|
||||||
if (
|
if (
|
||||||
searchMode.value !== SEARCH_MODE.ALL &&
|
searchMode.value !== SEARCH_MODE.ALL &&
|
||||||
searchMode.value !== SEARCH_MODE.TASKS
|
searchMode.value !== SEARCH_MODE.TASKS &&
|
||||||
|
searchMode.value !== SEARCH_MODE.PROJECTS
|
||||||
) {
|
) {
|
||||||
foundTasks.value = []
|
foundTasks.value = []
|
||||||
return
|
return
|
||||||
|
@ -330,7 +368,7 @@ function searchTasks() {
|
||||||
taskSearchTimeout.value = null
|
taskSearchTimeout.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, project: projectName, labels } = parsedQuery.value
|
const {text, project: projectName, labels} = parsedQuery.value
|
||||||
|
|
||||||
const filters: Filter[] = []
|
const filters: Filter[] = []
|
||||||
|
|
||||||
|
@ -349,8 +387,9 @@ function searchTasks() {
|
||||||
|
|
||||||
if (projectName !== null) {
|
if (projectName !== null) {
|
||||||
const project = projectStore.findProjectByExactname(projectName)
|
const project = projectStore.findProjectByExactname(projectName)
|
||||||
|
console.log({project})
|
||||||
if (project !== null) {
|
if (project !== null) {
|
||||||
addFilter('projectId', project.id, 'equals')
|
addFilter('project_id', project.id, 'equals')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,19 +400,16 @@ function searchTasks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
s: text,
|
s: text,
|
||||||
...filtersToParams(filters),
|
sort_by: 'done',
|
||||||
}
|
...filtersToParams(filters),
|
||||||
|
}
|
||||||
|
|
||||||
taskSearchTimeout.value = setTimeout(async () => {
|
taskSearchTimeout.value = setTimeout(async () => {
|
||||||
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
|
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
|
||||||
foundTasks.value = r.map((t) => {
|
foundTasks.value = r.map((t) => {
|
||||||
t.type = ACTION_TYPE.TASK
|
t.type = ACTION_TYPE.TASK
|
||||||
const project = projectStore.projects[t.projectId]
|
|
||||||
if (project !== null) {
|
|
||||||
t.title = `${t.title} (${project.title})`
|
|
||||||
}
|
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
}, 150)
|
}, 150)
|
||||||
|
@ -396,10 +432,10 @@ function searchTeams() {
|
||||||
clearTimeout(teamSearchTimeout.value)
|
clearTimeout(teamSearchTimeout.value)
|
||||||
teamSearchTimeout.value = null
|
teamSearchTimeout.value = null
|
||||||
}
|
}
|
||||||
const { assignees } = parsedQuery.value
|
const {assignees} = parsedQuery.value
|
||||||
teamSearchTimeout.value = setTimeout(async () => {
|
teamSearchTimeout.value = setTimeout(async () => {
|
||||||
const teamSearchPromises = assignees.map((t) =>
|
const teamSearchPromises = assignees.map((t) =>
|
||||||
teamService.getAll({}, { s: t }),
|
teamService.getAll({}, {s: t}),
|
||||||
)
|
)
|
||||||
const teamsResult = await Promise.all(teamSearchPromises)
|
const teamsResult = await Promise.all(teamSearchPromises)
|
||||||
foundTeams.value = teamsResult.flat().map((team) => {
|
foundTeams.value = teamsResult.flat().map((team) => {
|
||||||
|
@ -422,21 +458,21 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||||
closeQuickActions()
|
closeQuickActions()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'project.index',
|
name: 'project.index',
|
||||||
params: { projectId: (item as DoAction<IProject>).id },
|
params: {projectId: (item as DoAction<IProject>).id},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case ACTION_TYPE.TASK:
|
case ACTION_TYPE.TASK:
|
||||||
closeQuickActions()
|
closeQuickActions()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
params: { id: (item as DoAction<ITask>).id },
|
params: {id: (item as DoAction<ITask>).id},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case ACTION_TYPE.TEAM:
|
case ACTION_TYPE.TEAM:
|
||||||
closeQuickActions()
|
closeQuickActions()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'teams.edit',
|
name: 'teams.edit',
|
||||||
params: { id: (item as DoAction<ITeam>).id },
|
params: {id: (item as DoAction<ITeam>).id},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case ACTION_TYPE.CMD:
|
case ACTION_TYPE.CMD:
|
||||||
|
@ -444,6 +480,11 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||||
selectedCmd.value = item as DoAction<Command>
|
selectedCmd.value = item as DoAction<Command>
|
||||||
searchInput.value?.focus()
|
searchInput.value?.focus()
|
||||||
break
|
break
|
||||||
|
case ACTION_TYPE.LABELS:
|
||||||
|
query.value = '*' + item.title
|
||||||
|
searchInput.value?.focus()
|
||||||
|
searchTasks()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,8 +511,8 @@ async function newTask() {
|
||||||
title: query.value,
|
title: query.value,
|
||||||
projectId: currentProject.value.id,
|
projectId: currentProject.value.id,
|
||||||
})
|
})
|
||||||
success({ message: t('task.createSuccess') })
|
success({message: t('task.createSuccess')})
|
||||||
await router.push({ name: 'task.detail', params: { id: task.id } })
|
await router.push({name: 'task.detail', params: {id: task.id}})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newProject() {
|
async function newProject() {
|
||||||
|
@ -481,17 +522,17 @@ async function newProject() {
|
||||||
await projectStore.createProject(new ProjectModel({
|
await projectStore.createProject(new ProjectModel({
|
||||||
title: query.value,
|
title: query.value,
|
||||||
}))
|
}))
|
||||||
success({ message: t('project.create.createdSuccess')})
|
success({message: t('project.create.createdSuccess')})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newTeam() {
|
async function newTeam() {
|
||||||
const newTeam = new TeamModel({ name: query.value })
|
const newTeam = new TeamModel({name: query.value})
|
||||||
const team = await teamService.create(newTeam)
|
const team = await teamService.create(newTeam)
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'teams.edit',
|
name: 'teams.edit',
|
||||||
params: { id: team.id },
|
params: {id: team.id},
|
||||||
})
|
})
|
||||||
success({ message: t('team.create.success') })
|
success({message: t('team.create.success')})
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseButtonInstance = InstanceType<typeof BaseButton>
|
type BaseButtonInstance = InstanceType<typeof BaseButton>
|
||||||
|
@ -502,7 +543,7 @@ function setResultRefs(el: Element | ComponentPublicInstance | null, index: numb
|
||||||
resultRefs.value[index] = []
|
resultRefs.value[index] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
resultRefs.value[index][key] = el as (BaseButtonInstance | null)
|
resultRefs.value[index][key] = el as (BaseButtonInstance | null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(parentIndex: number, index: number) {
|
function select(parentIndex: number, index: number) {
|
||||||
|
@ -547,7 +588,7 @@ function reset() {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.quick-actions {
|
.quick-actions {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// FIXME: changed position should be an option of the modal
|
// FIXME: changed position should be an option of the modal
|
||||||
:deep(.modal-content) {
|
:deep(.modal-content) {
|
||||||
top: 3rem;
|
top: 3rem;
|
||||||
|
@ -569,6 +610,7 @@ function reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-cmd {
|
.active-cmd {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-left: .5rem;
|
margin-left: .5rem;
|
||||||
|
@ -614,10 +656,4 @@ function reset() {
|
||||||
background: var(--grey-100);
|
background: var(--grey-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK:
|
|
||||||
// FIXME:
|
|
||||||
.modal-container-smaller :deep(.hint-modal .modal-container) {
|
|
||||||
height: calc(100vh - 5rem);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
25
src/components/tasks/partials/label.vue
Normal file
25
src/components/tasks/partials/label.vue
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {ILabel} from '@/modelTypes/ILabel'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
label: ILabel
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:key="label.id"
|
||||||
|
:style="{'background': label.hexColor, 'color': label.textColor}"
|
||||||
|
class="tag"
|
||||||
|
>
|
||||||
|
<span>{{ label.title }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tag {
|
||||||
|
& + & {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="label-wrapper">
|
<div class="label-wrapper">
|
||||||
<span
|
<XLabel
|
||||||
|
v-for="label in labels"
|
||||||
|
:label="label"
|
||||||
:key="label.id"
|
:key="label.id"
|
||||||
:style="{'background': label.hexColor, 'color': label.textColor}"
|
/>
|
||||||
class="tag"
|
|
||||||
v-for="label in labels">
|
|
||||||
<span>{{ label.title }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -14,6 +12,8 @@
|
||||||
import type {PropType} from 'vue'
|
import type {PropType} from 'vue'
|
||||||
import type {ILabel} from '@/modelTypes/ILabel'
|
import type {ILabel} from '@/modelTypes/ILabel'
|
||||||
|
|
||||||
|
import XLabel from '@/components/tasks/partials/label.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
labels: {
|
labels: {
|
||||||
type: Array as PropType<ILabel[]>,
|
type: Array as PropType<ILabel[]>,
|
||||||
|
@ -26,10 +26,4 @@ defineProps({
|
||||||
.label-wrapper {
|
.label-wrapper {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
|
||||||
& + & {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -108,7 +108,7 @@ const visible = ref(false)
|
||||||
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
|
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
highlightHintIcon: boolean,
|
highlightHintIcon?: boolean,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const prefixes = computed(() => PREFIXES[mode.value])
|
const prefixes = computed(() => PREFIXES[mode.value])
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
|
id="showRelatedTasksFormButton"
|
||||||
/>
|
/>
|
||||||
<transition-group name="fade">
|
<transition-group name="fade">
|
||||||
<template v-if="editEnabled && showCreate">
|
<template v-if="editEnabled && showCreate">
|
||||||
|
|
192
src/components/tasks/partials/singleTaskInlineReadonly.vue
Normal file
192
src/components/tasks/partials/singleTaskInlineReadonly.vue
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<template>
|
||||||
|
<div class="task">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
v-if="showProject && typeof project !== 'undefined'"
|
||||||
|
class="task-project"
|
||||||
|
:class="{'mr-2': task.hexColor !== ''}"
|
||||||
|
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
||||||
|
>
|
||||||
|
{{ project.title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ColorBubble
|
||||||
|
v-if="task.hexColor !== ''"
|
||||||
|
:color="getHexColor(task.hexColor)"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
||||||
|
<span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
|
||||||
|
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
||||||
|
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">, </template>
|
||||||
|
</template>
|
||||||
|
›
|
||||||
|
</span>
|
||||||
|
{{ task.title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<labels
|
||||||
|
v-if="task.labels.length > 0"
|
||||||
|
class="labels ml-2 mr-1"
|
||||||
|
:labels="task.labels"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<User
|
||||||
|
v-for="(a, i) in task.assignees"
|
||||||
|
:avatar-size="20"
|
||||||
|
:key="task.id + 'assignee' + a.id + i"
|
||||||
|
:show-username="false"
|
||||||
|
:user="a"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="+new Date(task.dueDate) > 0"
|
||||||
|
class="dueDate"
|
||||||
|
v-tooltip="formatDateLong(task.dueDate)"
|
||||||
|
>
|
||||||
|
<time
|
||||||
|
:datetime="formatISO(task.dueDate)"
|
||||||
|
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||||
|
class="is-italic"
|
||||||
|
>
|
||||||
|
– {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
|
||||||
|
</time>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<priority-label :priority="task.priority" :done="task.done"/>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<span class="project-task-icon" v-if="task.attachments.length > 0">
|
||||||
|
<icon icon="paperclip"/>
|
||||||
|
</span>
|
||||||
|
<span class="project-task-icon" v-if="task.description">
|
||||||
|
<icon icon="align-left"/>
|
||||||
|
</span>
|
||||||
|
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
|
||||||
|
<icon icon="history"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<checklist-summary :task="task"/>
|
||||||
|
|
||||||
|
<progress
|
||||||
|
class="progress is-small"
|
||||||
|
v-if="task.percentDone > 0"
|
||||||
|
:value="task.percentDone * 100" max="100"
|
||||||
|
>
|
||||||
|
{{ task.percentDone * 100 }}%
|
||||||
|
</progress>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {computed} from 'vue'
|
||||||
|
|
||||||
|
import {getHexColor} from '@/models/task'
|
||||||
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
|
||||||
|
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||||
|
import Labels from '@/components/tasks/partials//labels.vue'
|
||||||
|
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
|
||||||
|
|
||||||
|
import User from '@/components/misc/user.vue'
|
||||||
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
|
||||||
|
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||||
|
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
|
const {
|
||||||
|
task,
|
||||||
|
showProject = false,
|
||||||
|
} = defineProps<{
|
||||||
|
task: ITask,
|
||||||
|
showProject?: boolean,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
|
const project = computed(() => projectStore.projects[task.projectId])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.task {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
transition: background-color $transition;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $radius;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
//display: -webkit-box;
|
||||||
|
hyphens: auto;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
//flex: 1 0 50%;
|
||||||
|
|
||||||
|
.dueDate {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overdue {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-project {
|
||||||
|
width: auto;
|
||||||
|
color: var(--grey-400);
|
||||||
|
font-size: .9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: bottom;
|
||||||
|
margin-left: .5rem;
|
||||||
|
height: 21px;
|
||||||
|
width: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-task-icon {
|
||||||
|
margin-left: 6px;
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text);
|
||||||
|
transition: color ease $transition-duration;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--grey-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasktext.done {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--grey-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.parent-tasks {
|
||||||
|
color: var(--grey-500);
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,10 @@
|
||||||
const LAST_VISITED_KEY = 'lastVisited'
|
const LAST_VISITED_KEY = 'lastVisited'
|
||||||
|
|
||||||
export const saveLastVisited = (name: string, params: object, query: object) => {
|
export const saveLastVisited = (name: string | undefined, params: object, query: object) => {
|
||||||
|
if (typeof name === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
|
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +13,7 @@ export const getLastVisited = () => {
|
||||||
if (lastVisited === null) {
|
if (lastVisited === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(lastVisited)
|
return JSON.parse(lastVisited)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const SUPPORTED_LOCALES = {
|
||||||
'es-ES': 'Español',
|
'es-ES': 'Español',
|
||||||
'da-DK': 'Dansk',
|
'da-DK': 'Dansk',
|
||||||
'ja-JP': '日本語',
|
'ja-JP': '日本語',
|
||||||
|
'hu-HU': 'Magyar',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||||
|
@ -47,8 +48,13 @@ export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocal
|
||||||
|
|
||||||
// If the language hasn't been loaded yet
|
// If the language hasn't been loaded yet
|
||||||
if (!i18n.global.availableLocales.includes(lang)) {
|
if (!i18n.global.availableLocales.includes(lang)) {
|
||||||
const messages = await import(`./lang/${lang}.json`)
|
try {
|
||||||
i18n.global.setLocaleMessage(lang, messages.default)
|
const messages = await import(`./lang/${lang}.json`)
|
||||||
|
i18n.global.setLocaleMessage(lang, messages.default)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to load language ${lang}:`, e)
|
||||||
|
return setLanguage(getBrowserLanguage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.global.locale.value = lang
|
i18n.global.locale.value = lang
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Úkoly",
|
"tasks": "Úkoly",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Týmy",
|
"teams": "Týmy",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Zadejte název nového úkolu…",
|
"newTask": "Zadejte název nového úkolu…",
|
||||||
"newTeam": "Zadejte název nového týmu…",
|
"newTeam": "Zadejte název nového týmu…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Opgaver",
|
"tasks": "Opgaver",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Hold",
|
"teams": "Hold",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Indtast titlen på den nye opgave…",
|
"newTask": "Indtast titlen på den nye opgave…",
|
||||||
"newTeam": "Indtast navnet på det nye hold…",
|
"newTeam": "Indtast navnet på det nye hold…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Aufgaben",
|
"tasks": "Aufgaben",
|
||||||
"projects": "Projekte",
|
"projects": "Projekte",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Gib den Titel des neuen Projekts ein…",
|
"newProject": "Gib den Titel des neuen Projekts ein…",
|
||||||
"newTask": "Gib den Titel der neuen Aufgabe ein …",
|
"newTask": "Gib den Titel der neuen Aufgabe ein …",
|
||||||
"newTeam": "Gib den Namen des neuen Teams ein …",
|
"newTeam": "Gib den Namen des neuen Teams ein …",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Uufgabe",
|
"tasks": "Uufgabe",
|
||||||
"projects": "Projekte",
|
"projects": "Projekte",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Gib den Titel des neuen Projekts ein…",
|
"newProject": "Gib den Titel des neuen Projekts ein…",
|
||||||
"newTask": "Gib en Titl für die neu Uufgab iih…",
|
"newTask": "Gib en Titl für die neu Uufgab iih…",
|
||||||
"newTeam": "Gib en Name für da neui Team iih…",
|
"newTeam": "Gib en Name für da neui Team iih…",
|
||||||
|
|
|
@ -905,6 +905,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tareas",
|
"tasks": "Tareas",
|
||||||
"projects": "Proyectos",
|
"projects": "Proyectos",
|
||||||
"teams": "Equipos",
|
"teams": "Equipos",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Introduzca el título del nuevo proyecto…",
|
"newProject": "Introduzca el título del nuevo proyecto…",
|
||||||
"newTask": "Introduzca el título de la nueva tarea…",
|
"newTask": "Introduzca el título de la nueva tarea…",
|
||||||
"newTeam": "Introduzca el nombre del nuevo equipo…",
|
"newTeam": "Introduzca el nombre del nuevo equipo…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tâches",
|
"tasks": "Tâches",
|
||||||
"projects": "Projets",
|
"projects": "Projets",
|
||||||
"teams": "Équipes",
|
"teams": "Équipes",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Saisissez le nom du nouveau projet…",
|
"newProject": "Saisissez le nom du nouveau projet…",
|
||||||
"newTask": "Saisir le nom de la nouvelle tâche…",
|
"newTask": "Saisir le nom de la nouvelle tâche…",
|
||||||
"newTeam": "Saisir le nom de la nouvelle équipe…",
|
"newTeam": "Saisir le nom de la nouvelle équipe…",
|
||||||
|
|
1009
src/i18n/lang/hu-HU.json
Normal file
1009
src/i18n/lang/hu-HU.json
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Attivitá",
|
"tasks": "Attivitá",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Gruppi",
|
"teams": "Gruppi",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Inserisci il titolo della nuova attività…",
|
"newTask": "Inserisci il titolo della nuova attività…",
|
||||||
"newTeam": "Inserisci il nome del nuovo gruppo…",
|
"newTeam": "Inserisci il nome del nuovo gruppo…",
|
||||||
|
|
|
@ -329,12 +329,12 @@
|
||||||
"title": "絞り込み",
|
"title": "絞り込み",
|
||||||
"clear": "絞り込みの解除",
|
"clear": "絞り込みの解除",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"title": "条件名",
|
"title": "絞り込み条件名",
|
||||||
"titlePlaceholder": "条件名を入力…",
|
"titlePlaceholder": "絞り込み条件名を入力…",
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"descriptionPlaceholder": "説明を入力…",
|
"descriptionPlaceholder": "絞り込み条件の説明を入力…",
|
||||||
"includeNulls": "値を設定していないタスクを含める",
|
"includeNulls": "値が設定されていないタスクを含める",
|
||||||
"requireAll": "Require all filters to be true for a task to show up",
|
"requireAll": "すべての条件に一致するタスクのみ表示",
|
||||||
"showDoneTasks": "完了したタスクを表示",
|
"showDoneTasks": "完了したタスクを表示",
|
||||||
"sortAlphabetically": "アルファベット順に並べ替える",
|
"sortAlphabetically": "アルファベット順に並べ替える",
|
||||||
"enablePriority": "優先度による絞り込みを有効化",
|
"enablePriority": "優先度による絞り込みを有効化",
|
||||||
|
@ -346,18 +346,18 @@
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "新しい絞り込み条件の作成",
|
"title": "新しい絞り込み条件の作成",
|
||||||
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
|
"description": "絞り込み条件は、複数の条件を組み合わせて保存できる仮想のプロジェクトです。",
|
||||||
"action": "新しい絞り込み条件を作成",
|
"action": "新しい絞り込み条件を作成",
|
||||||
"titleRequired": "Please provide a title for the filter."
|
"titleRequired": "絞り込み条件名を入力してください。"
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"header": "Delete this saved filter",
|
"header": "絞り込み条件の削除",
|
||||||
"text": "Are you sure you want to delete this saved filter?",
|
"text": "絞り込み条件を削除して本当によろしいですか?",
|
||||||
"success": "The filter was deleted successfully."
|
"success": "絞り込み条件は正常に削除されました。"
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"title": "Edit This Saved Filter",
|
"title": "絞り込み条件の編集",
|
||||||
"success": "The filter was saved successfully."
|
"success": "絞り込み条件は正常に保存されました。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"migrate": {
|
"migrate": {
|
||||||
|
@ -815,7 +815,7 @@
|
||||||
"namePlaceholder": "The team's name goes here…",
|
"namePlaceholder": "The team's name goes here…",
|
||||||
"nameRequired": "Please specify a name.",
|
"nameRequired": "Please specify a name.",
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"descriptionPlaceholder": "The teams description goes here…",
|
"descriptionPlaceholder": "チームの説明を入力…",
|
||||||
"admin": "管理者",
|
"admin": "管理者",
|
||||||
"member": "メンバー"
|
"member": "メンバー"
|
||||||
}
|
}
|
||||||
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "タスク",
|
"tasks": "タスク",
|
||||||
"projects": "プロジェクト",
|
"projects": "プロジェクト",
|
||||||
"teams": "チーム",
|
"teams": "チーム",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "新しいプロジェクト名を入力…",
|
"newProject": "新しいプロジェクト名を入力…",
|
||||||
"newTask": "新しいタスク名を入力…",
|
"newTask": "新しいタスク名を入力…",
|
||||||
"newTeam": "新しいチーム名を入力…",
|
"newTeam": "新しいチーム名を入力…",
|
||||||
|
@ -983,7 +984,7 @@
|
||||||
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
|
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
|
||||||
"10005": "There can be only one done bucket per project.",
|
"10005": "There can be only one done bucket per project.",
|
||||||
"11001": "The saved filter does not exist.",
|
"11001": "The saved filter does not exist.",
|
||||||
"11002": "Saved filters are not available for link shares.",
|
"11002": "絞り込み条件はリンクの共有には使用できません。",
|
||||||
"12001": "The subscription entity type is invalid.",
|
"12001": "The subscription entity type is invalid.",
|
||||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Taken",
|
"tasks": "Taken",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Oppgaver",
|
"tasks": "Oppgaver",
|
||||||
"projects": "Prosjekter",
|
"projects": "Prosjekter",
|
||||||
"teams": "Grupper",
|
"teams": "Grupper",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Skriv tittelen på det nye prosjektet…",
|
"newProject": "Skriv tittelen på det nye prosjektet…",
|
||||||
"newTask": "Skriv tittelen på den nye oppgaven…",
|
"newTask": "Skriv tittelen på den nye oppgaven…",
|
||||||
"newTeam": "Skriv inn navnet på den nye gruppen…",
|
"newTeam": "Skriv inn navnet på den nye gruppen…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Zadania",
|
"tasks": "Zadania",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Zespoły",
|
"teams": "Zespoły",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Wpisz tytuł nowego zadania…",
|
"newTask": "Wpisz tytuł nowego zadania…",
|
||||||
"newTeam": "Wpisz nazwę nowego zespołu…",
|
"newTeam": "Wpisz nazwę nowego zespołu…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tarefas",
|
"tasks": "Tarefas",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Equipes",
|
"teams": "Equipes",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tarefas",
|
"tasks": "Tarefas",
|
||||||
"projects": "Projetos",
|
"projects": "Projetos",
|
||||||
"teams": "Equipas",
|
"teams": "Equipas",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Insere o título do novo espaço…",
|
"newProject": "Insere o título do novo espaço…",
|
||||||
"newTask": "Insere o título da nova tarefa…",
|
"newTask": "Insere o título da nova tarefa…",
|
||||||
"newTeam": "Insere o nome da nova equipa…",
|
"newTeam": "Insere o nome da nova equipa…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Задачи",
|
"tasks": "Задачи",
|
||||||
"projects": "Проекты",
|
"projects": "Проекты",
|
||||||
"teams": "Команды",
|
"teams": "Команды",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Введите название проекта…",
|
"newProject": "Введите название проекта…",
|
||||||
"newTask": "Введите название задачи…",
|
"newTask": "Введите название задачи…",
|
||||||
"newTeam": "Введите название новой команды…",
|
"newTeam": "Введите название новой команды…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Uppgifter",
|
"tasks": "Uppgifter",
|
||||||
"projects": "Projekt",
|
"projects": "Projekt",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tác vụ",
|
"tasks": "Tác vụ",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Team",
|
"teams": "Team",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Đặt tên cho tác vụ mới…",
|
"newTask": "Đặt tên cho tác vụ mới…",
|
||||||
"newTeam": "Đặt tên cho đội nhóm mới…",
|
"newTeam": "Đặt tên cho đội nhóm mới…",
|
||||||
|
|
|
@ -902,6 +902,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -23,6 +23,7 @@ declare global {
|
||||||
SENTRY_DSN: string;
|
SENTRY_DSN: string;
|
||||||
PROJECT_INFINITE_NESTING_ENABLED: boolean;
|
PROJECT_INFINITE_NESTING_ENABLED: boolean;
|
||||||
ALLOW_ICON_CHANGES: boolean;
|
ALLOW_ICON_CHANGES: boolean;
|
||||||
|
CUSTOM_LOGO_URL?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +36,8 @@ if (apiUrlFromStorage !== null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the api url does not contain a / at the end
|
// Make sure the api url does not contain a / at the end
|
||||||
if (window.API_URL.slice(window.API_URL.length - 1, window.API_URL.length) === '/') {
|
if (window.API_URL.endsWith('/')) {
|
||||||
window.API_URL = window.API_URL.slice(0, window.API_URL.length - 1)
|
window.API_URL = window.API_URL.slice(0, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// directives
|
// directives
|
||||||
|
|
|
@ -85,7 +85,6 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||||
index = 0
|
index = 0
|
||||||
isFavorite = false
|
isFavorite = false
|
||||||
subscription: ISubscription = null
|
subscription: ISubscription = null
|
||||||
coverImageAttachmentId: IAttachment['id'] = null
|
|
||||||
|
|
||||||
position = 0
|
position = 0
|
||||||
kanbanPosition = 0
|
kanbanPosition = 0
|
||||||
|
|
|
@ -448,16 +448,9 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
// Check if the route the user wants to go to is a route which needs authentication. We use this to
|
||||||
// When trying this before the current user was fully loaded we might get a flash of the login screen
|
// redirect the user after successful login.
|
||||||
// in the user shell. To make shure this does not happen we check if everything is ready before trying.
|
const isValidUserAppRoute = ![
|
||||||
if (!baseStore.ready) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user is already logged in and redirect them to the home page if not
|
|
||||||
if (
|
|
||||||
![
|
|
||||||
'user.login',
|
'user.login',
|
||||||
'user.password-reset.request',
|
'user.password-reset.request',
|
||||||
'user.password-reset.reset',
|
'user.password-reset.reset',
|
||||||
|
@ -468,8 +461,19 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
||||||
localStorage.getItem('passwordResetToken') === null &&
|
localStorage.getItem('passwordResetToken') === null &&
|
||||||
localStorage.getItem('emailConfirmToken') === null &&
|
localStorage.getItem('emailConfirmToken') === null &&
|
||||||
!(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined'))
|
!(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined'))
|
||||||
) {
|
|
||||||
|
if (isValidUserAppRoute) {
|
||||||
saveLastVisited(to.name as string, to.params, to.query)
|
saveLastVisited(to.name as string, to.params, to.query)
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseStore = useBaseStore()
|
||||||
|
// When trying this before the current user was fully loaded we might get a flash of the login screen
|
||||||
|
// in the user shell. To make sure this does not happen we check if everything is ready before trying.
|
||||||
|
if (!baseStore.ready) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidUserAppRoute) {
|
||||||
return {name: 'user.login'}
|
return {name: 'user.login'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {success} from '@/message'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
||||||
|
|
||||||
const {remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
||||||
|
|
||||||
export interface ProjectState {
|
export interface ProjectState {
|
||||||
[id: IProject['id']]: IProject
|
[id: IProject['id']]: IProject
|
||||||
|
@ -36,9 +36,11 @@ export const useProjectStore = defineStore('project', () => {
|
||||||
const projectsArray = computed(() => Object.values(projects.value)
|
const projectsArray = computed(() => Object.values(projects.value)
|
||||||
.sort((a, b) => a.position - b.position))
|
.sort((a, b) => a.position - b.position))
|
||||||
const notArchivedRootProjects = computed(() => projectsArray.value
|
const notArchivedRootProjects = computed(() => projectsArray.value
|
||||||
.filter(p => p.parentProjectId === 0 && !p.isArchived))
|
.filter(p => p.parentProjectId === 0 && !p.isArchived && p.id > 0))
|
||||||
const favoriteProjects = computed(() => projectsArray.value
|
const favoriteProjects = computed(() => projectsArray.value
|
||||||
.filter(p => !p.isArchived && p.isFavorite))
|
.filter(p => !p.isArchived && p.isFavorite))
|
||||||
|
const savedFilterProjects = computed(() => projectsArray.value
|
||||||
|
.filter(p => !p.isArchived && p.id < -1))
|
||||||
const hasProjects = computed(() => projectsArray.value.length > 0)
|
const hasProjects = computed(() => projectsArray.value.length > 0)
|
||||||
|
|
||||||
const getChildProjects = computed(() => {
|
const getChildProjects = computed(() => {
|
||||||
|
@ -172,6 +174,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||||
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
|
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
|
||||||
projects.value = {}
|
projects.value = {}
|
||||||
setProjects(loadedProjects)
|
setProjects(loadedProjects)
|
||||||
|
loadedProjects.forEach(p => add(p))
|
||||||
|
|
||||||
return loadedProjects
|
return loadedProjects
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -198,6 +201,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||||
notArchivedRootProjects: readonly(notArchivedRootProjects),
|
notArchivedRootProjects: readonly(notArchivedRootProjects),
|
||||||
favoriteProjects: readonly(favoriteProjects),
|
favoriteProjects: readonly(favoriteProjects),
|
||||||
hasProjects: readonly(hasProjects),
|
hasProjects: readonly(hasProjects),
|
||||||
|
savedFilterProjects: readonly(savedFilterProjects),
|
||||||
|
|
||||||
getChildProjects,
|
getChildProjects,
|
||||||
findProjectByExactname,
|
findProjectByExactname,
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
||||||
{{ $t('project.list.empty') }}
|
{{ $t('project.list.empty') }}
|
||||||
<ButtonLink @click="focusNewTaskInput()">
|
<ButtonLink @click="focusNewTaskInput()" v-if="project.id > 0">
|
||||||
{{ $t('project.list.newTaskCta') }}
|
{{ $t('project.list.newTaskCta') }}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</nothing>
|
</nothing>
|
||||||
|
|
|
@ -378,7 +378,7 @@
|
||||||
{{ $t('task.detail.actions.attachments') }}
|
{{ $t('task.detail.actions.attachments') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('relatedTasks')"
|
@click="setRelatedTasksActive()"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="sitemap"
|
icon="sitemap"
|
||||||
v-shortcut="'r'"
|
v-shortcut="'r'"
|
||||||
|
@ -777,6 +777,19 @@ async function removeRepeatAfter() {
|
||||||
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||||
await saveTask()
|
await saveTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRelatedTasksActive() {
|
||||||
|
setFieldActive('relatedTasks')
|
||||||
|
|
||||||
|
// If the related tasks are already available, show the form again
|
||||||
|
const el = activeFieldElements['relatedTasks']
|
||||||
|
for (const child in el?.children) {
|
||||||
|
if (el?.children[child]?.id === 'showRelatedTasksFormButton') {
|
||||||
|
el?.children[child]?.click()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
Reference in New Issue
Block a user