Compare commits
91 Commits
b6393a6167
...
d0dc86fd58
Author | SHA1 | Date | |
---|---|---|---|
d0dc86fd58 | |||
0484923b8a | |||
5f2fb01e90 | |||
bd18524f36 | |||
7375a87f2f | |||
ccff276397 | |||
30b21fc11c | |||
7c98ddc20b | |||
6ba02a0f10 | |||
676d2b6215 | |||
|
85e612451f | ||
d411de99f1 | |||
228d652b03 | |||
b3e2107503 | |||
a579a8e65f | |||
ee980e2a00 | |||
394dbe0055 | |||
30d599369f | |||
631b02d2ee | |||
326bfb557a | |||
cd0149ef69 | |||
78d4a518a3 | |||
3c1041902e | |||
e3cae0ed7f | |||
fc8bd6a9ca | |||
5a6e5619e3 | |||
9c9f806e62 | |||
67216579bc | |||
a8df935ddb | |||
bb4746f226 | |||
31590236aa | |||
00d48a6178 | |||
5169cca8d8 | |||
255a7d565c | |||
8dbaee5dfb | |||
69b0b19482 | |||
eae89d37f1 | |||
7d19859816 | |||
c7b70844c6 | |||
b8c21c2ade | |||
57c99a22a0 | |||
8ea97f3ffc | |||
0b3604d167 | |||
c5ba7fcb73 | |||
5a25685d53 | |||
da311fce9e | |||
0fdf1ca027 | |||
f8e907a8c1 | |||
af7ca8ad8f | |||
92f7d9ded5 | |||
41ccaea78b | |||
c5696f3e2a | |||
898707664c | |||
d0b5bef68a | |||
e395d4efdb | |||
ce54132868 | |||
07d4d1e537 | |||
a701b0452e | |||
af65efcd27 | |||
dc2afb9e8d | |||
e123d4f825 | |||
b72c963256 | |||
149bbf17eb | |||
265d60cf42 | |||
23c9f51e73 | |||
ff697d0c7a | |||
00588cf59f | |||
01089f4f3d | |||
a7461d1ddd | |||
a451189bb6 | |||
bf9af27fc3 | |||
5619fda0f2 | |||
167953b26b | |||
664bf0a5f4 | |||
5e991f3024 | |||
28050d9cd5 | |||
e94b71d577 | |||
336ce217d3 | |||
|
ce01085951 | ||
96a6d43a3f | |||
13d63e34aa | |||
a8441c72b8 | |||
230fa6ce66 | |||
069c491fbd | |||
a9eae95d67 | |||
50502d9d11 | |||
18af6edc82 | |||
d048b61eb3 | |||
996607e670 | |||
e33ebe1831 | |||
557b0ffec7 |
16
.drone.yml
16
.drone.yml
|
@ -42,7 +42,7 @@ steps:
|
|||
# - .cache
|
||||
|
||||
- name: dependencies
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -55,7 +55,7 @@ steps:
|
|||
# - restore-cache
|
||||
|
||||
- name: lint
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -66,7 +66,7 @@ steps:
|
|||
- dependencies
|
||||
|
||||
- name: build-prod
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -77,7 +77,7 @@ steps:
|
|||
- dependencies
|
||||
|
||||
- name: test-unit
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
|
@ -87,7 +87,7 @@ steps:
|
|||
|
||||
- name: typecheck
|
||||
failure: ignore
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -202,7 +202,7 @@ steps:
|
|||
# - .cache
|
||||
|
||||
- name: build
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -285,7 +285,7 @@ steps:
|
|||
# - .cache
|
||||
|
||||
- name: build
|
||||
image: node:20.10-alpine
|
||||
image: node:20.11.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -532,6 +532,6 @@ steps:
|
|||
src/i18n/lang/en.json: en.json
|
||||
---
|
||||
kind: signature
|
||||
hmac: ecb706a867b39f2501cc6cf587a535fe4cd6cfd0c339833a733d61a3349c5a54
|
||||
hmac: a044c7c4db3c2a11299d4d118397e9d25be36db241723a1bbd0a2f9cc90ffdac
|
||||
|
||||
...
|
||||
|
|
110
CHANGELOG.md
110
CHANGELOG.md
|
@ -9,6 +9,116 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
|
|||
|
||||
The releases aim at the api versions which is why there are missing versions.
|
||||
|
||||
## [0.22.1] - 2024-01-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(auth)* Correctly construct redirect url from current window href
|
||||
* *(ci)* Use working crowdin image
|
||||
* *(ci)* Use working image for crowdin update step
|
||||
* *(ci)* Use working crowdin image
|
||||
* *(color picker)* When picking a color, the color picker should not be black afterwards
|
||||
* *(editor)* List icons
|
||||
* *(editor)* Use higher-contrast colors for links and code
|
||||
* *(editor)* Don't bubble up changes when no changes were made
|
||||
* *(editor)* Focus the editor when clicking on the whole edit container
|
||||
* *(editor)* Render images without crashing
|
||||
* *(editor)* Use a stable image id to prevent constant re-rendering
|
||||
* *(editor)* Use manual input prompt instead of window.prompt
|
||||
* *(filter)* Validate filter title field after loading a filter for edit
|
||||
* *(kanban)* Ensure text and icon color only depends on the card background, not on the color scheme
|
||||
* *(kanban)* Make sure the checklist summary uses the correct text color
|
||||
* *(kanban)* Make sure spacing between assignees and other task details works out evenly
|
||||
* *(labels)* Make color reset work
|
||||
* *(labels)* Text and background combination in dark mode
|
||||
* *(notifications)* Unread indicator spacing
|
||||
* *(notifications)* Always left-align notification text
|
||||
* *(notifications)* Read indicator size
|
||||
* *(openid)* Use the full path when building the redirect url, not only the host
|
||||
* *(openid)* Use the calculated redirect url when authenticating with openid providers
|
||||
* *(project)* Always use the appropriate color for task estimate during deletion dialoge
|
||||
* *(quick add magic)* Ensure month is removed from task text
|
||||
* *(table view)* Make sure popup does not overlap
|
||||
* *(task)* Don't immediately re-trigger date change when nothing changed
|
||||
* *(task)* Bubble date changes from the picker up
|
||||
* *(task)* Update due date when marking a task done
|
||||
* *(task)* Don't show edit button when the user does not have permission to edit the task
|
||||
* *(task)* Don't show assignee edit buttons and input when the user does not have the permission to edit
|
||||
* *(tasks)* Make sure tasks show up if their parent task is not available in the current view
|
||||
* *(tasks)* Don't load tasks multiple times when viewing list or gantt view
|
||||
* *(test)* Make date assertion not brittle
|
||||
* Lint ([5e991f3](5e991f3024f7856420614171ec66468eb2e2df63))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v2 (#3862)
|
||||
* *(deps)* Update pnpm to v8.14.0
|
||||
* *(deps)* Update dependency vue to v3.4.7 (#3873)
|
||||
* *(deps)* Update dependency axios to v1.6.5 (#3871)
|
||||
* *(deps)* Update dependency date-fns to v3 (#3857)
|
||||
* *(deps)* Update dev-dependencies (#3861)
|
||||
* *(deps)* Update dependency @kyvg/vue3-notification to v3.1.3 (#3864)
|
||||
* *(deps)* Update dependency node to v20.11.0
|
||||
* *(deps)* Update dependency vue-i18n to v9.9.0 (#3880)
|
||||
* *(deps)* Update dependency dompurify to v3.0.8 (#3881)
|
||||
* *(deps)* Update dependency floating-vue to v2.0.0 (#3883)
|
||||
* *(deps)* Update tiptap to v2.1.15 (#3884)
|
||||
* *(deps)* Update vueuse to v10.7.1 (#3872)
|
||||
* *(deps)* Update pnpm to v8.14.1 (#3885)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.93.0 (#3859)
|
||||
* *(deps)* Update dependency floating-vue to v5 (#3887)
|
||||
* *(deps)* Update dependency vue to v3.4.8 (#3886)
|
||||
* *(deps)* Update node.js to v20.11 (#3888)
|
||||
* *(deps)* Increase renovate timeout
|
||||
* *(deps)* Update tiptap to v2.1.16 (#3892)
|
||||
* *(deps)* Pin node.js (#3895)
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency sortablejs to v1.15.2
|
||||
* *(deps)* Update vueuse to v10.7.2
|
||||
* *(deps)* Update dependency floating-vue to v5.1.0
|
||||
* *(deps)* Update dependency vue to v3.4.14
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies (major) (#3890)
|
||||
* *(deps)* Update dependency floating-vue to v5.1.1
|
||||
* *(deps)* Update dependency floating-vue to v5.2.0
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency vue to v3.4.15
|
||||
* *(deps)* Update dependency happy-dom to v13.2.0
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.94.1
|
||||
* *(deps)* Update dependency vite to v5.0.12
|
||||
* *(deps)* Update dependency date-fns to v3.3.0
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update pnpm to v8.14.2
|
||||
* *(deps)* Update dependency date-fns to v3.3.1
|
||||
* *(deps)* Update dev-dependencies to v6.19.1
|
||||
* *(deps)* Update pnpm to v8.14.3
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.95.0
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency axios to v1.6.6
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.97.0
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.98.0
|
||||
* *(deps)* Update dependency axios to v1.6.7
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
|
||||
### Features
|
||||
|
||||
* *(reminders)* Show reminders in notifications bar
|
||||
* Datepicker locale support (#3878) ([92f7d9d](92f7d9ded5d56b95ba7d647eba01372f6ef682ad))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(perf)* Import some modules dynamically (#3179)
|
||||
* Only show webhooks overview table when there are webhooks ([326bfb5](326bfb557ab359fa154b163f5dd957928f46d3ec))
|
||||
* Only show webhooks overview table when there are webhooks ([631b02d](631b02d2eedc4a403b7c55f1c56ceaeca5379bf5))
|
||||
|
||||
## [0.22.0] - 2023-12-19
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:20.10-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:20.11.0-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.22.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.22.1-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
|
|
@ -541,6 +541,86 @@ describe('Task', () => {
|
|||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can set a due date to a specific date for a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
done: false,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Set Due Date')
|
||||
.click()
|
||||
cy.get('.task-view .columns.details .column')
|
||||
.contains('Due Date')
|
||||
.get('.date-input .datepicker .show')
|
||||
.click()
|
||||
cy.get('.datepicker-popup .flatpickr-innerContainer .flatpickr-days .flatpickr-day.today')
|
||||
.click()
|
||||
cy.get('[data-cy="closeDatepicker"]')
|
||||
.contains('Confirm')
|
||||
.click()
|
||||
|
||||
const today = new Date()
|
||||
const day = today.toLocaleString('default', {day: '2-digit'})
|
||||
const month = today.toLocaleString('default', {month: 'short'})
|
||||
const year = today.toLocaleString('default', {year: 'numeric'})
|
||||
const date = `${day} ${month} ${year}, 12:00:00`
|
||||
cy.get('.task-view .columns.details .column')
|
||||
.contains('Due Date')
|
||||
.get('.date-input .datepicker-popup')
|
||||
.should('not.exist')
|
||||
cy.get('.task-view .columns.details .column')
|
||||
.contains('Due Date')
|
||||
.get('.date-input')
|
||||
.should('contain.text', date)
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can change a due date to a specific date for a task', () => {
|
||||
const dueDate = new Date()
|
||||
dueDate.setHours(12)
|
||||
dueDate.setMinutes(0)
|
||||
dueDate.setSeconds(0)
|
||||
dueDate.setDate(1)
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
done: false,
|
||||
due_date: dueDate.toISOString(),
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Set Due Date')
|
||||
.click()
|
||||
cy.get('.task-view .columns.details .column')
|
||||
.contains('Due Date')
|
||||
.get('.date-input .datepicker .show')
|
||||
.click()
|
||||
cy.get('.datepicker-popup .flatpickr-innerContainer .flatpickr-days .flatpickr-day.today')
|
||||
.click()
|
||||
cy.get('[data-cy="closeDatepicker"]')
|
||||
.contains('Confirm')
|
||||
.click()
|
||||
|
||||
const today = new Date()
|
||||
const day = today.toLocaleString('default', {day: '2-digit'})
|
||||
const month = today.toLocaleString('default', {month: 'short'})
|
||||
const year = today.toLocaleString('default', {year: 'numeric'})
|
||||
const date = `${day} ${month} ${year}, 12:00:00`
|
||||
cy.get('.task-view .columns.details .column')
|
||||
.contains('Due Date')
|
||||
.get('.date-input .datepicker-popup')
|
||||
.should('not.exist')
|
||||
cy.get('.task-view .columns.details .column')
|
||||
.contains('Due Date')
|
||||
.get('.date-input')
|
||||
.should('contain.text', date)
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can set a reminder', () => {
|
||||
TaskReminderFactory.truncate()
|
||||
const tasks = TaskFactory.create(1, {
|
||||
|
@ -645,7 +725,7 @@ describe('Task', () => {
|
|||
.click()
|
||||
cy.get('.reminder-options-popup .card-content .reminder-period input')
|
||||
.first()
|
||||
.type('10')
|
||||
.type('{selectall}10')
|
||||
cy.get('.reminder-options-popup .card-content .reminder-period select')
|
||||
.first()
|
||||
.select('days')
|
||||
|
@ -771,7 +851,7 @@ describe('Task', () => {
|
|||
.should('exist')
|
||||
})
|
||||
|
||||
it.only('Can check items off a checklist', () => {
|
||||
it('Can check items off a checklist', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
description: `
|
||||
|
@ -858,7 +938,7 @@ describe('Task', () => {
|
|||
method: 'PUT',
|
||||
url: `${Cypress.env('API_URL')}/tasks/${tasks[0].id}/attachments`,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.localStorage.getItem('token')}`,
|
||||
'Authorization': `Bearer ${window.localStorage.getItem('token')}`,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
body: formData,
|
||||
|
|
138
package.json
138
package.json
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.14.0",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -48,61 +48,61 @@
|
|||
"@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.88.0",
|
||||
"@sentry/vue": "7.88.0",
|
||||
"@tiptap/core": "2.1.13",
|
||||
"@tiptap/extension-blockquote": "2.1.13",
|
||||
"@tiptap/extension-bold": "2.1.13",
|
||||
"@tiptap/extension-bullet-list": "2.1.13",
|
||||
"@tiptap/extension-code": "2.1.13",
|
||||
"@tiptap/extension-code-block-lowlight": "2.1.13",
|
||||
"@tiptap/extension-document": "2.1.13",
|
||||
"@tiptap/extension-dropcursor": "2.1.13",
|
||||
"@tiptap/extension-gapcursor": "2.1.13",
|
||||
"@tiptap/extension-hard-break": "2.1.13",
|
||||
"@tiptap/extension-heading": "2.1.13",
|
||||
"@tiptap/extension-history": "2.1.13",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.13",
|
||||
"@tiptap/extension-image": "2.1.13",
|
||||
"@tiptap/extension-italic": "2.1.13",
|
||||
"@tiptap/extension-link": "2.1.13",
|
||||
"@tiptap/extension-list-item": "2.1.13",
|
||||
"@tiptap/extension-ordered-list": "2.1.13",
|
||||
"@tiptap/extension-paragraph": "2.1.13",
|
||||
"@tiptap/extension-placeholder": "2.1.13",
|
||||
"@tiptap/extension-strike": "2.1.13",
|
||||
"@tiptap/extension-table": "2.1.13",
|
||||
"@tiptap/extension-table-cell": "2.1.13",
|
||||
"@tiptap/extension-table-header": "2.1.13",
|
||||
"@tiptap/extension-table-row": "2.1.13",
|
||||
"@tiptap/extension-task-item": "2.1.13",
|
||||
"@tiptap/extension-task-list": "2.1.13",
|
||||
"@tiptap/extension-text": "2.1.13",
|
||||
"@tiptap/extension-typography": "2.1.13",
|
||||
"@tiptap/extension-underline": "2.1.13",
|
||||
"@tiptap/pm": "2.1.13",
|
||||
"@tiptap/suggestion": "2.1.13",
|
||||
"@tiptap/vue-3": "2.1.13",
|
||||
"@sentry/tracing": "7.99.0",
|
||||
"@sentry/vue": "7.99.0",
|
||||
"@tiptap/core": "2.2.0",
|
||||
"@tiptap/extension-blockquote": "2.2.0",
|
||||
"@tiptap/extension-bold": "2.2.0",
|
||||
"@tiptap/extension-bullet-list": "2.2.0",
|
||||
"@tiptap/extension-code": "2.2.0",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.0",
|
||||
"@tiptap/extension-document": "2.2.0",
|
||||
"@tiptap/extension-dropcursor": "2.2.0",
|
||||
"@tiptap/extension-gapcursor": "2.2.0",
|
||||
"@tiptap/extension-hard-break": "2.2.0",
|
||||
"@tiptap/extension-heading": "2.2.0",
|
||||
"@tiptap/extension-history": "2.2.0",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.0",
|
||||
"@tiptap/extension-image": "2.2.0",
|
||||
"@tiptap/extension-italic": "2.2.0",
|
||||
"@tiptap/extension-link": "2.2.0",
|
||||
"@tiptap/extension-list-item": "2.2.0",
|
||||
"@tiptap/extension-ordered-list": "2.2.0",
|
||||
"@tiptap/extension-paragraph": "2.2.0",
|
||||
"@tiptap/extension-placeholder": "2.2.0",
|
||||
"@tiptap/extension-strike": "2.2.0",
|
||||
"@tiptap/extension-table": "2.2.0",
|
||||
"@tiptap/extension-table-cell": "2.2.0",
|
||||
"@tiptap/extension-table-header": "2.2.0",
|
||||
"@tiptap/extension-table-row": "2.2.0",
|
||||
"@tiptap/extension-task-item": "2.2.0",
|
||||
"@tiptap/extension-task-list": "2.2.0",
|
||||
"@tiptap/extension-text": "2.2.0",
|
||||
"@tiptap/extension-typography": "2.2.0",
|
||||
"@tiptap/extension-underline": "2.2.0",
|
||||
"@tiptap/pm": "2.2.0",
|
||||
"@tiptap/suggestion": "2.2.0",
|
||||
"@tiptap/vue-3": "2.2.0",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.7.0",
|
||||
"@vueuse/router": "10.7.0",
|
||||
"axios": "1.6.5",
|
||||
"@vueuse/core": "10.7.2",
|
||||
"@vueuse/router": "10.7.2",
|
||||
"axios": "1.6.7",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.2.0",
|
||||
"date-fns": "3.3.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.6",
|
||||
"dompurify": "3.0.8",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
"floating-vue": "2.0.0-beta.24",
|
||||
"floating-vue": "5.2.2",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lodash.debounce": "4.0.8",
|
||||
|
@ -110,13 +110,13 @@
|
|||
"pinia": "2.1.7",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.1",
|
||||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.3.2",
|
||||
"vue": "3.4.7",
|
||||
"vue": "3.4.15",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.8.0",
|
||||
"vue-i18n": "9.9.1",
|
||||
"vue-router": "4.2.5",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
|
@ -125,10 +125,10 @@
|
|||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@histoire/plugin-screenshot": "0.17.6",
|
||||
"@histoire/plugin-vue": "0.17.6",
|
||||
"@rushstack/eslint-patch": "1.6.1",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"@histoire/plugin-screenshot": "0.17.8",
|
||||
"@histoire/plugin-vue": "0.17.9",
|
||||
"@rushstack/eslint-patch": "1.7.2",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
|
@ -136,43 +136,43 @@
|
|||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.10.8",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@vitejs/plugin-legacy": "5.2.0",
|
||||
"@vitejs/plugin-vue": "4.6.2",
|
||||
"@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",
|
||||
"@vue/test-utils": "2.4.3",
|
||||
"@vue/test-utils": "2.4.4",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.16",
|
||||
"browserslist": "4.22.2",
|
||||
"caniuse-lite": "1.0.30001576",
|
||||
"autoprefixer": "10.4.17",
|
||||
"browserslist": "4.22.3",
|
||||
"caniuse-lite": "1.0.30001581",
|
||||
"css-has-pseudo": "6.0.1",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.6.2",
|
||||
"esbuild": "0.19.11",
|
||||
"cypress": "13.6.3",
|
||||
"esbuild": "0.20.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-vue": "9.19.2",
|
||||
"happy-dom": "12.10.3",
|
||||
"histoire": "0.17.6",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"happy-dom": "13.3.5",
|
||||
"histoire": "0.17.9",
|
||||
"postcss": "8.4.33",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.1",
|
||||
"postcss-preset-env": "9.3.0",
|
||||
"rollup": "4.9.4",
|
||||
"rollup": "4.9.6",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.69.7",
|
||||
"sass": "1.70.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.11",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.17.4",
|
||||
"vite-plugin-pwa": "0.17.5",
|
||||
"vite-plugin-sentry": "1.3.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.1.3",
|
||||
"vitest": "1.2.2",
|
||||
"vue-tsc": "1.8.27",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
|
|
3424
pnpm-lock.yaml
3424
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -4,6 +4,11 @@
|
|||
"extends": [
|
||||
"config:js-app"
|
||||
],
|
||||
"hostRules": [
|
||||
{
|
||||
"timeout": 600000
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["happy-dom"],
|
||||
|
|
|
@ -37,8 +37,6 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
|||
import Ready from '@/components/misc/ready.vue'
|
||||
|
||||
import {setLanguage} from '@/i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
@ -48,6 +46,9 @@ import {useBodyClass} from '@/composables/useBodyClass'
|
|||
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
|
||||
import DemoMode from '@/components/home/DemoMode.vue'
|
||||
|
||||
const importAccountDeleteService = () => import('@/services/accountDelete')
|
||||
const importMessage = () => import('@/message')
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
@ -68,8 +69,11 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
|||
return
|
||||
}
|
||||
|
||||
const messageP = importMessage()
|
||||
const AccountDeleteService = (await importAccountDeleteService()).default
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
await accountDeletionService.confirm(accountDeletionConfirm)
|
||||
const {success} = await messageP
|
||||
success({message: t('user.deletion.confirmSuccess')})
|
||||
authStore.refreshUserInfo()
|
||||
}, { immediate: true })
|
||||
|
|
|
@ -81,9 +81,8 @@ import Popup from '@/components/misc/popup.vue'
|
|||
import {DATE_RANGES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/datemathHelp.vue'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
@ -93,8 +92,6 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
// FIXME: This seems to always contain the default value - that breaks the picker
|
||||
const weekStart = computed(() => authStore.settings.weekStart ?? 0)
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
|
@ -102,9 +99,7 @@ const flatPickerConfig = computed(() => ({
|
|||
enableTime: false,
|
||||
wrap: true,
|
||||
mode: 'range',
|
||||
locale: {
|
||||
firstDayOf7Days: weekStart.value,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
const showHowItWorks = ref(false)
|
||||
|
|
|
@ -64,6 +64,15 @@ const emit = defineEmits(['update:modelValue'])
|
|||
watch(
|
||||
() => modelValue,
|
||||
(newValue) => {
|
||||
if (newValue === '' || newValue.startsWith('var(')) {
|
||||
color.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (!newValue.startsWith('#') && (newValue.length === 6 || newValue.length === 3)) {
|
||||
newValue = `#${newValue}`
|
||||
}
|
||||
|
||||
color.value = newValue
|
||||
},
|
||||
{immediate: true},
|
||||
|
|
|
@ -80,8 +80,8 @@ import {formatDate} from '@/helpers/time/formatDate'
|
|||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -105,8 +105,6 @@ watch(
|
|||
{immediate: true},
|
||||
)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const weekStart = computed(() => authStore.settings.weekStart)
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
|
@ -114,9 +112,7 @@ const flatPickerConfig = computed(() => ({
|
|||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: {
|
||||
firstDayOfWeek: weekStart.value,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||
|
@ -128,6 +124,12 @@ const flatPickrDate = computed({
|
|||
return
|
||||
}
|
||||
|
||||
if (date.value !== null) {
|
||||
const oldDate = formatDate(date.value, 'yyy-LL-dd H:mm')
|
||||
if (oldDate === newValue) {
|
||||
return
|
||||
}
|
||||
}
|
||||
date.value = createDateFromString(newValue)
|
||||
updateData()
|
||||
},
|
||||
|
@ -155,10 +157,6 @@ function updateData() {
|
|||
}
|
||||
|
||||
function setDate(dateString: string) {
|
||||
if (date.value === null) {
|
||||
date.value = new Date()
|
||||
}
|
||||
|
||||
const interval = calculateDayInterval(dateString)
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
|
@ -166,7 +164,6 @@ function setDate(dateString: string) {
|
|||
newDate.setMinutes(0)
|
||||
newDate.setSeconds(0)
|
||||
date.value = newDate
|
||||
flatPickrDate.value = newDate
|
||||
updateData()
|
||||
}
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
v-tooltip="$t('input.editor.bulletList')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-list-ol']"/>
|
||||
<icon :icon="['fa', 'fa-list-ul']"/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
|
@ -120,7 +120,7 @@
|
|||
v-tooltip="$t('input.editor.orderedList')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-list-ul']"/>
|
||||
<icon :icon="['fa', 'fa-list-ol']"/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
|
@ -336,6 +336,7 @@ import {ref} from 'vue'
|
|||
import {Editor} from '@tiptap/vue-3'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
||||
const {
|
||||
editor = null,
|
||||
|
@ -353,29 +354,8 @@ function openImagePicker() {
|
|||
document.getElementById('tiptap__image-upload').click()
|
||||
}
|
||||
|
||||
function setLink() {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: url, target: '_blank'})
|
||||
.run()
|
||||
function setLink(event) {
|
||||
setLinkInEditor(event.target.getBoundingClientRect(), editor)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
class="tiptap__editor"
|
||||
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
|
||||
:editor="editor"
|
||||
@click="focusIfEditing()"
|
||||
/>
|
||||
|
||||
<input
|
||||
|
@ -75,7 +76,7 @@
|
|||
@change="addImage"
|
||||
/>
|
||||
|
||||
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length === 0 && !isEditing">
|
||||
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length === 0 && !isEditing && isEditEnabled">
|
||||
<li>
|
||||
<BaseButton
|
||||
@click="setEdit"
|
||||
|
@ -110,6 +111,7 @@
|
|||
variant="secondary"
|
||||
:shadow="false"
|
||||
v-cy="'saveEditor'"
|
||||
:disabled="!contentHasChanged"
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
|
@ -171,8 +173,9 @@ import XButton from '@/components/input/button.vue'
|
|||
import {Placeholder} from '@tiptap/extension-placeholder'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {mergeAttributes} from '@tiptap/core'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
||||
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
|
@ -227,19 +230,20 @@ const CustomImage = Image.extend({
|
|||
renderHTML({HTMLAttributes}) {
|
||||
if (HTMLAttributes.src?.startsWith(window.API_URL) || HTMLAttributes['data-src']?.startsWith(window.API_URL)) {
|
||||
const imageUrl = HTMLAttributes['data-src'] ?? HTMLAttributes.src
|
||||
const id = 'tiptap-image-' + createRandomID()
|
||||
|
||||
// The url is something like /tasks/<id>/attachments/<id>
|
||||
const parts = imageUrl.slice(window.API_URL.length + 1).split('/')
|
||||
const taskId = Number(parts[1])
|
||||
const attachmentId = Number(parts[3])
|
||||
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
|
||||
const id = 'tiptap-image-' + cacheKey
|
||||
|
||||
nextTick(async () => {
|
||||
|
||||
const img = document.getElementById(id)
|
||||
|
||||
if (!img) return
|
||||
|
||||
// The url is something like /tasks/<id>/attachments/<id>
|
||||
const parts = imageUrl.slice(window.API_URL.length + 1).split('/')
|
||||
const taskId = Number(parts[1])
|
||||
const attachmentId = Number(parts[3])
|
||||
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
|
||||
|
||||
if (typeof loadedAttachments.value[cacheKey] === 'undefined') {
|
||||
|
||||
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
|
||||
|
@ -286,8 +290,18 @@ const {
|
|||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const internalMode = ref<Mode>('edit')
|
||||
const internalMode = ref<Mode>('preview')
|
||||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
const contentHasChanged = ref<boolean>(false)
|
||||
|
||||
watch(
|
||||
() => internalMode.value,
|
||||
mode => {
|
||||
if (mode === 'preview') {
|
||||
contentHasChanged.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const editor = useEditor({
|
||||
content: modelValue,
|
||||
|
@ -308,7 +322,9 @@ const editor = useEditor({
|
|||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Enter': () => {
|
||||
bubbleSave()
|
||||
if (contentHasChanged.value) {
|
||||
bubbleSave()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -420,6 +436,7 @@ function bubbleNow() {
|
|||
return
|
||||
}
|
||||
|
||||
contentHasChanged.value = true
|
||||
emit('update:modelValue', editor.value?.getHTML())
|
||||
}
|
||||
|
||||
|
@ -455,7 +472,7 @@ function uploadAndInsertFiles(files: File[] | FileList) {
|
|||
})
|
||||
}
|
||||
|
||||
function addImage() {
|
||||
async function addImage(event) {
|
||||
|
||||
if (typeof uploadCallback !== 'undefined') {
|
||||
const files = uploadInputRef.value?.files
|
||||
|
@ -469,7 +486,7 @@ function addImage() {
|
|||
return
|
||||
}
|
||||
|
||||
const url = window.prompt('URL')
|
||||
const url = await inputPrompt(event.target.getBoundingClientRect())
|
||||
|
||||
if (url) {
|
||||
editor.value?.chain().focus().setImage({src: url}).run()
|
||||
|
@ -477,34 +494,8 @@ function addImage() {
|
|||
}
|
||||
}
|
||||
|
||||
function setLink() {
|
||||
const previousUrl = editor.value?.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: url, target: '_blank'})
|
||||
.run()
|
||||
function setLink(event) {
|
||||
setLinkInEditor(event.target.getBoundingClientRect(), editor.value)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -558,6 +549,7 @@ function setFocusToEditor(event) {
|
|||
event.target.contentEditable === 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (!isEditing.value && isEditEnabled) {
|
||||
|
@ -567,6 +559,12 @@ function setFocusToEditor(event) {
|
|||
editor.value?.commands.focus()
|
||||
}
|
||||
|
||||
function focusIfEditing() {
|
||||
if (isEditing.value) {
|
||||
editor.value?.commands.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function clickTasklistCheckbox(event) {
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
|
@ -671,36 +669,17 @@ watch(
|
|||
line-height: 1.1;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #68cef8;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: rgba(#616161, 0.1);
|
||||
color: #616161;
|
||||
background-color: var(--grey-200);
|
||||
color: var(--grey-700);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
color: #fff;
|
||||
background: var(--grey-200);
|
||||
color: var(--grey-700);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
color: #fff;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-radius: $radius;
|
||||
|
||||
code {
|
||||
color: inherit;
|
||||
|
@ -711,7 +690,7 @@ watch(
|
|||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
|
@ -724,7 +703,7 @@ watch(
|
|||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
color: var(--code-variable);
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
|
@ -734,23 +713,23 @@ watch(
|
|||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
color: var(--code-literal);
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
color: var(--code-symbol);
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
color: var(--code-section);
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
color: var(--code-keyword);
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
|
|
26
src/components/input/editor/setLinkInEditor.ts
Normal file
26
src/components/input/editor/setLinkInEditor.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
|
||||
export async function setLinkInEditor(pos, editor) {
|
||||
const previousUrl = editor?.getAttributes('link').href || ''
|
||||
const url = await inputPrompt(pos, previousUrl)
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: url, target: '_blank'})
|
||||
.run()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
|
||||
<slot name="trigger" :isOpen="open" :toggle="toggle" :close="close"></slot>
|
||||
<div
|
||||
class="popup"
|
||||
:class="{
|
||||
|
@ -8,7 +8,7 @@
|
|||
}"
|
||||
ref="popup"
|
||||
>
|
||||
<slot name="content" :isOpen="open" :toggle="toggle"/>
|
||||
<slot name="content" :isOpen="open" :toggle="toggle" :close="close"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -53,6 +53,7 @@ onClickOutside(popup, () => {
|
|||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
z-index: 100;
|
||||
|
||||
&.is-open {
|
||||
opacity: 1;
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
||||
{{ getDisplayName(n.notification.doer) }}
|
||||
</span>
|
||||
<BaseButton @click="() => to(n, index)()">
|
||||
<BaseButton @click="() => to(n, index)()" class="has-text-left">
|
||||
{{ n.toText(userInfo) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
@ -223,6 +223,7 @@ async function markAllRead() {
|
|||
background: var(--primary);
|
||||
border-radius: 100%;
|
||||
margin: 0 .5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.read {
|
||||
background: transparent;
|
||||
|
|
|
@ -36,7 +36,7 @@ import Filters from '@/components/project/partials/filters.vue'
|
|||
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
|
@ -48,6 +48,9 @@ const value = computed({
|
|||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
if(props.modelValue === value) {
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
@ -59,7 +62,7 @@ watch(
|
|||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
// this.value also contains the page parameter which we don't want to include in filters
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
|
@ -61,6 +61,8 @@ import {
|
|||
import Loading from '@/components/misc/loading.vue'
|
||||
import {MILLISECONDS_A_DAY} from '@/constants/date'
|
||||
import {useWeekDayFromDate} from '@/helpers/time/formatDate'
|
||||
import dayjs from 'dayjs'
|
||||
import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
|
||||
|
||||
export interface GanttChartProps {
|
||||
isLoading: boolean,
|
||||
|
@ -81,8 +83,8 @@ const emit = defineEmits<{
|
|||
const {tasks, filters} = toRefs(props)
|
||||
|
||||
// setup dayjs for vue-ganttastic
|
||||
const dayjsLanguageLoading = ref(false)
|
||||
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
|
||||
// const dayjsLanguageLoading = ref(false)
|
||||
const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
|
||||
extendDayjs()
|
||||
|
||||
const ganttContainer = ref(null)
|
||||
|
|
|
@ -44,7 +44,7 @@ import flatPickr from 'vue-flatpickr-component'
|
|||
|
||||
import TaskService from '@/services/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
|
@ -55,7 +55,6 @@ const {
|
|||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const task = ref<ITask>()
|
||||
|
@ -102,9 +101,7 @@ const flatPickerConfig = computed(() => ({
|
|||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: {
|
||||
firstDayOfWeek: authStore.settings.weekStart,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
function deferDays(days: number) {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
:autocomplete-enabled="false"
|
||||
>
|
||||
<template #items="{items}">
|
||||
<assignee-list :assignees="items" :remove="removeAssignee"/>
|
||||
<assignee-list :assignees="items" :remove="removeAssignee" :disabled="disabled"/>
|
||||
</template>
|
||||
<template #searchResult="{option: user}">
|
||||
<user :avatar-size="24" :show-username="true" :user="user"/>
|
||||
|
|
|
@ -51,6 +51,7 @@ import Multiselect from '@/components/input/multiselect.vue'
|
|||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -132,7 +133,10 @@ async function createAndAddLabel(title: string) {
|
|||
return
|
||||
}
|
||||
|
||||
const newLabel = await labelStore.createLabel(new LabelModel({title}))
|
||||
const newLabel = await labelStore.createLabel(new LabelModel({
|
||||
title,
|
||||
hexColor: getRandomColorHex(),
|
||||
}))
|
||||
addLabel(newLabel, false)
|
||||
labels.value.push(newLabel)
|
||||
success({message: t('task.label.addCreateSuccess')})
|
||||
|
|
|
@ -57,10 +57,9 @@
|
|||
v-if="task.assignees.length > 0"
|
||||
:assignees="task.assignees"
|
||||
:avatar-size="24"
|
||||
class="ml-1"
|
||||
:inline="true"
|
||||
class="mr-1"
|
||||
/>
|
||||
<checklist-summary :task="task"/>
|
||||
<checklist-summary :task="task" class="checklist"/>
|
||||
<span class="icon" v-if="task.attachments.length > 0">
|
||||
<icon icon="paperclip"/>
|
||||
</span>
|
||||
|
@ -218,15 +217,20 @@ $task-background: var(--white);
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: .25rem;
|
||||
|
||||
:deep(.tag),
|
||||
:deep(.checklist-summary),
|
||||
.assignees,
|
||||
.icon,
|
||||
.priority-label {
|
||||
margin-top: .25rem;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
:deep(.checklist-summary) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.assignees {
|
||||
display: flex;
|
||||
|
||||
|
@ -292,25 +296,34 @@ $task-background: var(--white);
|
|||
.priority-label {
|
||||
background: hsl(220, 13%, 91%);
|
||||
}
|
||||
|
||||
.footer :deep(.checklist-summary) {
|
||||
color: hsl(216.9, 19.1%, 26.7%); // grey-700
|
||||
}
|
||||
}
|
||||
|
||||
&.has-light-text {
|
||||
--white: hsla(var(--white-h), var(--white-s), var(--white-l), var(--white-a)) !important;
|
||||
color: var(--white);
|
||||
|
||||
.task-id {
|
||||
color: var(--grey-200);
|
||||
color: hsl(220, 13%, 91%); // grey-200;
|
||||
}
|
||||
|
||||
.footer .icon,
|
||||
.due-date,
|
||||
.priority-label {
|
||||
background: var(--grey-800);
|
||||
background: hsl(215, 27.9%, 16.9%); // grey-800
|
||||
}
|
||||
|
||||
.footer {
|
||||
.icon svg {
|
||||
fill: var(--white);
|
||||
}
|
||||
|
||||
:deep(.checklist-summary) {
|
||||
color: hsl(220, 13%, 91%); // grey-200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{{ reminderText }}
|
||||
</SimpleButton>
|
||||
</template>
|
||||
<template #content="{isOpen, toggle}">
|
||||
<template #content="{isOpen, close}">
|
||||
<Card class="reminder-options-popup" :class="{'is-open': isOpen}" :padding="false">
|
||||
<div class="options" v-if="activeForm === null">
|
||||
<SimpleButton
|
||||
|
@ -17,7 +17,7 @@
|
|||
:key="k"
|
||||
class="option-button"
|
||||
:class="{'currently-active': p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo}"
|
||||
@click="setReminderFromPreset(p, toggle)"
|
||||
@click="setReminderFromPreset(p, close)"
|
||||
>
|
||||
{{ formatReminder(p) }}
|
||||
</SimpleButton>
|
||||
|
@ -40,20 +40,20 @@
|
|||
<ReminderPeriod
|
||||
v-if="activeForm === 'relative'"
|
||||
v-model="reminder"
|
||||
@update:modelValue="updateDataAndMaybeClose(toggle)"
|
||||
@update:modelValue="updateDataAndMaybeClose(close)"
|
||||
/>
|
||||
|
||||
<DatepickerInline
|
||||
v-if="activeForm === 'absolute'"
|
||||
v-model="reminderDate"
|
||||
@update:modelValue="setReminderDate(toggle)"
|
||||
@update:modelValue="setReminderDate(close)"
|
||||
/>
|
||||
|
||||
<x-button
|
||||
v-if="showFormSwitch !== null"
|
||||
class="reminder__close-button"
|
||||
:shadow="false"
|
||||
@click="toggle"
|
||||
@click="updateDataAndMaybeClose(close)"
|
||||
>
|
||||
{{ $t('misc.confirm') }}
|
||||
</x-button>
|
||||
|
@ -148,25 +148,26 @@ function updateData() {
|
|||
}
|
||||
}
|
||||
|
||||
function setReminderDate(toggle) {
|
||||
function setReminderDate(close) {
|
||||
reminder.value.reminder = reminderDate.value === null
|
||||
? null
|
||||
: new Date(reminderDate.value)
|
||||
reminder.value.relativeTo = null
|
||||
reminder.value.relativePeriod = 0
|
||||
updateDataAndMaybeClose(toggle)
|
||||
updateDataAndMaybeClose(close)
|
||||
}
|
||||
|
||||
function setReminderFromPreset(preset, toggle) {
|
||||
|
||||
function setReminderFromPreset(preset, close) {
|
||||
reminder.value = preset
|
||||
updateData()
|
||||
toggle()
|
||||
close()
|
||||
}
|
||||
|
||||
function updateDataAndMaybeClose(toggle) {
|
||||
function updateDataAndMaybeClose(close) {
|
||||
updateData()
|
||||
if (clearAfterUpdate) {
|
||||
toggle()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ import TaskReminderModel from '@/models/taskReminder'
|
|||
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES, type IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
|
@ -105,7 +106,7 @@ function updateData() {
|
|||
reminder.value.relativeTo = period.value.relativeTo
|
||||
reminder.value.reminder = null
|
||||
|
||||
emit('update:modelValue', reminder.value)
|
||||
useDebounceFn(() => emit('update:modelValue', reminder.value), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -288,6 +288,7 @@ async function markAsDone(checked: boolean) {
|
|||
title: t('task.undo'),
|
||||
callback: () => undoDone(checked),
|
||||
}])
|
||||
updateDueDate()
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* @param color
|
||||
* @returns {string}
|
||||
*/
|
||||
export function colorFromHex(color: string) {
|
||||
if (color.substring(0, 1) === '#') {
|
||||
export function colorFromHex(color: string): string {
|
||||
if (color !== '' && color.substring(0, 1) === '#') {
|
||||
color = color.substring(1, 7)
|
||||
}
|
||||
|
||||
|
|
15
src/helpers/flatpickrLanguage.ts
Normal file
15
src/helpers/flatpickrLanguage.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {useAuthStore} from '@/stores/auth'
|
||||
import FlatpickrLanguages from 'flatpickr/dist/l10n'
|
||||
import type { CustomLocale, key } from 'flatpickr/dist/types/locale'
|
||||
|
||||
export function getFlatpickrLanguage(): CustomLocale {
|
||||
const authStore = useAuthStore()
|
||||
const lang = authStore.settings.language
|
||||
const langPair = lang.split('-')
|
||||
let language = FlatpickrLanguages[lang === 'vi-vn' ? 'vn' : 'en']
|
||||
if (langPair.length > 0 && FlatpickrLanguages[langPair[0] as key] !== undefined) {
|
||||
language = FlatpickrLanguages[langPair[0] as key]
|
||||
}
|
||||
language.firstDayOfWeek = authStore.settings.weekStart ?? language.firstDayOfWeek
|
||||
return language
|
||||
}
|
39
src/helpers/inputPrompt.ts
Normal file
39
src/helpers/inputPrompt.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import {createRandomID} from '@/helpers/randomId'
|
||||
import tippy from 'tippy.js'
|
||||
import {nextTick} from 'vue'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
|
||||
export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const id = 'link-input-' + createRandomID()
|
||||
|
||||
const linkPopup = tippy('body', {
|
||||
getReferenceClientRect: () => pos,
|
||||
appendTo: () => document.body,
|
||||
content: `<div><input class="input" placeholder="URL" id="${id}" value="${oldValue}"/></div>`,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top-start',
|
||||
allowHTML: true,
|
||||
})
|
||||
|
||||
linkPopup[0].show()
|
||||
|
||||
nextTick(() => document.getElementById(id)?.focus())
|
||||
|
||||
document.getElementById(id)?.addEventListener('keydown', event => {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (hotkeyString !== 'Enter') {
|
||||
return
|
||||
}
|
||||
|
||||
const url = event.target.value
|
||||
|
||||
resolve(url)
|
||||
|
||||
linkPopup[0].hide()
|
||||
})
|
||||
|
||||
})
|
||||
}
|
|
@ -1,16 +1,23 @@
|
|||
import {createRandomID} from '@/helpers/randomId'
|
||||
import type {IProvider} from '@/types/IProvider'
|
||||
import {parseURL} from 'ufo'
|
||||
|
||||
export function getRedirectUrlFromCurrentFrontendPath(provider: IProvider): string {
|
||||
// We're not using the redirect url provided by the server to allow redirects when using the electron app.
|
||||
// The implications are not quite clear yet hence the logic to pass in another redirect url still exists.
|
||||
const url = parseURL(window.location.href)
|
||||
return `${url.protocol}//${url.host}/auth/openid/${provider.key}`
|
||||
}
|
||||
|
||||
export const redirectToProvider = (provider: IProvider) => {
|
||||
|
||||
// We're not using the redirect url provided by the server to allow redirects when using the electron app.
|
||||
// The implications are not quite clear yet hence the logic to pass in another redirect url still exists.
|
||||
const redirectUrl = `${window.location.href.replace('/login', '')}/auth/openid/`
|
||||
console.log({provider})
|
||||
|
||||
const redirectUrl = getRedirectUrlFromCurrentFrontendPath(provider)
|
||||
const state = createRandomID(24)
|
||||
localStorage.setItem('state', state)
|
||||
|
||||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}${provider.key}&response_type=code&scope=openid email profile&state=${state}`
|
||||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=openid email profile&state=${state}`
|
||||
}
|
||||
export const redirectToProviderOnLogout = (provider: IProvider) => {
|
||||
if (provider.logoutUrl.length > 0) {
|
||||
|
|
|
@ -15,34 +15,32 @@ interface dateFoundResult {
|
|||
const monthsRegexGroup = '(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
|
||||
|
||||
function matchesDateExpr(text: string, dateExpr: string): boolean {
|
||||
return text.match(new RegExp('(^| )' + dateExpr, 'g')) !== null
|
||||
return text.match(new RegExp('(^| )' + dateExpr, 'gi')) !== null
|
||||
}
|
||||
|
||||
export const parseDate = (text: string, now: Date = new Date()): dateParseResult => {
|
||||
const lowerText: string = text.toLowerCase()
|
||||
|
||||
if (matchesDateExpr(lowerText, 'today')) {
|
||||
if (matchesDateExpr(text, 'today')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'tomorrow')) {
|
||||
if (matchesDateExpr(text, 'tomorrow')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'next monday')) {
|
||||
if (matchesDateExpr(text, 'next monday')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextMonday')), 'next monday')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'this weekend')) {
|
||||
if (matchesDateExpr(text, 'this weekend')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('thisWeekend')), 'this weekend')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'later this week')) {
|
||||
if (matchesDateExpr(text, 'later this week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterThisWeek')), 'later this week')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'later next week')) {
|
||||
if (matchesDateExpr(text, 'later next week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterNextWeek')), 'later next week')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'next week')) {
|
||||
if (matchesDateExpr(text, 'next week')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'next month')) {
|
||||
if (matchesDateExpr(text, 'next month')) {
|
||||
const date: Date = new Date()
|
||||
date.setDate(1)
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
|
@ -52,7 +50,7 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
|
|||
|
||||
return addTimeToDate(text, date, 'next month')
|
||||
}
|
||||
if (matchesDateExpr(lowerText, 'end of month')) {
|
||||
if (matchesDateExpr(text, 'end of month')) {
|
||||
const curDate: Date = new Date()
|
||||
const date: Date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
|
||||
date.setHours(calculateNearestHours(date))
|
||||
|
@ -70,7 +68,7 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
|
|||
parsed = getDayFromText(text)
|
||||
if (parsed.date !== null) {
|
||||
const month = getMonthFromText(text, parsed.date)
|
||||
return addTimeToDate(text, month.date, parsed.foundText)
|
||||
return addTimeToDate(month.newText, month.date, parsed.foundText)
|
||||
}
|
||||
|
||||
parsed = getDateFromTextIn(text, now)
|
||||
|
@ -123,7 +121,7 @@ const addTimeToDate = (text: string, date: Date, previousMatch: string | null):
|
|||
|
||||
const replace = results !== null ? results[0] : previousMatch
|
||||
return {
|
||||
newText: replaceAll(text, replace, ''),
|
||||
newText: replaceAll(text, replace, '').trim(),
|
||||
date: date,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -762,7 +762,7 @@
|
|||
"empty": "Noch keine Beschreibung vorhanden."
|
||||
},
|
||||
"assignee": {
|
||||
"placeholder": "Tippe, um eine:n Benutzer:in zuzuweisen …",
|
||||
"placeholder": "Tippe, um jemanden zuzuweisen …",
|
||||
"selectPlaceholder": "Diese:n Benutzer:in zuweisen",
|
||||
"assignSuccess": "Der:die Benutzer:in wurde erfolgreich zugewiesen.",
|
||||
"unassignSuccess": "Benutzer:innenzuweisung aufgehoben."
|
||||
|
|
|
@ -160,7 +160,7 @@
|
|||
"expired": "Žeton je potekel pred {ago}.",
|
||||
"tokenCreatedSuccess": "Tu je vaš novi API žeton: {token}",
|
||||
"tokenCreatedNotSeeAgain": "Shranite ga na varno mesto, ker ga ne boste več videli!",
|
||||
"selectAll": "Select all",
|
||||
"selectAll": "Izberi vse",
|
||||
"delete": {
|
||||
"header": "Izbriši ta žeton",
|
||||
"text1": "Ali ste prepričani, da želite izbrisati žeton \"{token}\"?",
|
||||
|
|
|
@ -5,7 +5,6 @@ import type {ILabel} from '@/modelTypes/ILabel'
|
|||
import type {IUser} from '@/modelTypes/IUser'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
|
||||
export default class LabelModel extends AbstractModel<ILabel> implements ILabel {
|
||||
id = 0
|
||||
|
@ -24,15 +23,21 @@ export default class LabelModel extends AbstractModel<ILabel> implements ILabel
|
|||
constructor(data: Partial<ILabel> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
|
||||
if (this.hexColor === '') {
|
||||
this.hexColor = getRandomColorHex()
|
||||
}
|
||||
|
||||
if (this.hexColor.substring(0, 1) !== '#') {
|
||||
if (this.hexColor !== '' && !this.hexColor.startsWith('#') && !this.hexColor.startsWith('var(')) {
|
||||
this.hexColor = '#' + this.hexColor
|
||||
}
|
||||
this.textColor = colorIsDark(this.hexColor) ? '#4a4a4a' : '#fff'
|
||||
|
||||
if (this.hexColor === '') {
|
||||
this.hexColor = 'var(--grey-200)'
|
||||
this.textColor = 'var(--grey-800)'
|
||||
} else {
|
||||
this.textColor = colorIsDark(this.hexColor)
|
||||
// Fixed colors to avoid flipping in dark mode
|
||||
? 'hsl(215, 27.9%, 16.9%)' // grey-800
|
||||
: 'hsl(220, 13%, 91%)' // grey-200
|
||||
}
|
||||
|
||||
this.createdBy = new UserModel(this.createdBy)
|
||||
|
||||
this.created = new Date(this.created)
|
||||
|
|
|
@ -112,6 +112,16 @@ describe('Parse Task Text', () => {
|
|||
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
|
||||
})
|
||||
it('should recognize Tomorrow', () => {
|
||||
const result = parseTaskText('Lorem Ipsum Tomorrow')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
expect(result?.date?.getFullYear()).toBe(tomorrow.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
|
||||
})
|
||||
it('should recognize next monday', () => {
|
||||
const result = parseTaskText('Lorem Ipsum next monday')
|
||||
|
||||
|
@ -441,7 +451,7 @@ describe('Parse Task Text', () => {
|
|||
'06/08/2021': '2021-6-8',
|
||||
'6/7/21': '2021-6-7',
|
||||
'27/07/2021,': null,
|
||||
'2021/07/06,': '2021-7-6',
|
||||
'2021/07/06': '2021-7-6',
|
||||
'2021-07-06': '2021-7-6',
|
||||
'27 jan': '2022-1-27',
|
||||
'27/1': '2022-1-27',
|
||||
|
@ -449,39 +459,52 @@ describe('Parse Task Text', () => {
|
|||
'16/12': '2021-12-16',
|
||||
'01/27': '2022-1-27',
|
||||
'1/27': '2022-1-27',
|
||||
'Jan 27': '2022-1-27',
|
||||
'jan 27': '2022-1-27',
|
||||
'Jan 27': '2022-1-27',
|
||||
'feb 21': '2022-2-21',
|
||||
'Feb 21': '2022-2-21',
|
||||
'mar 21': '2022-3-21',
|
||||
'Mar 21': '2022-3-21',
|
||||
'apr 21': '2022-4-21',
|
||||
'Apr 21': '2022-4-21',
|
||||
'may 21': '2022-5-21',
|
||||
'May 21': '2022-5-21',
|
||||
'jun 21': '2022-6-21',
|
||||
'Jun 21': '2022-6-21',
|
||||
'jul 21': '2021-7-21',
|
||||
'Jul 21': '2021-7-21',
|
||||
'aug 21': '2021-8-21',
|
||||
'Aug 21': '2021-8-21',
|
||||
'sep 21': '2021-9-21',
|
||||
'Sep 21': '2021-9-21',
|
||||
'oct 21': '2021-10-21',
|
||||
'Oct 21': '2021-10-21',
|
||||
'nov 21': '2021-11-21',
|
||||
'Nov 21': '2021-11-21',
|
||||
'dec 21': '2021-12-21',
|
||||
'Dec 21': '2021-12-21',
|
||||
} as Record<string, string | null>
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should parse '${c}' as '${cases[c]}' with the date at the end`, () => {
|
||||
const {date} = getDateFromText(`Lorem Ipsum ${c}`, now)
|
||||
const {date, foundText} = getDateFromText(`Lorem Ipsum ${c}`, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
|
||||
expect(foundText.trim()).toBe(c)
|
||||
})
|
||||
it(`should parse '${c}' as '${cases[c]}' with the date at the beginning`, () => {
|
||||
const {date} = getDateFromText(`${c} Lorem Ipsum`, now)
|
||||
const {date, foundText} = getDateFromText(`${c} Lorem Ipsum`, now)
|
||||
if (date === null && cases[c] === null) {
|
||||
expect(date).toBeNull()
|
||||
return
|
||||
}
|
||||
|
||||
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
|
||||
expect(foundText.trim()).toBe(c)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -532,6 +555,20 @@ describe('Parse Task Text', () => {
|
|||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe(cases[c])
|
||||
})
|
||||
}
|
||||
|
||||
it('should replace the text in title case', () => {
|
||||
const {date, newText} = parseDate('Some task Mar 8th', now)
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe('2021-3-8 12:0')
|
||||
expect(newText).toBe('Some task')
|
||||
})
|
||||
|
||||
it('should replace the text in lowercase', () => {
|
||||
const {date, newText} = parseDate('Some task mar 8th', now)
|
||||
|
||||
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe('2021-3-8 12:0')
|
||||
expect(newText).toBe('Some task')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
const response = await filterService.get(filter.value)
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
await validateTitleField()
|
||||
}, {immediate: true})
|
||||
|
||||
async function createFilter() {
|
||||
|
|
|
@ -9,7 +9,11 @@ import UserSettingsService from '@/services/userSettings'
|
|||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {success} from '@/message'
|
||||
import {redirectToProvider, redirectToProviderOnLogout} from '@/helpers/redirectToProvider'
|
||||
import {
|
||||
getRedirectUrlFromCurrentFrontendPath,
|
||||
redirectToProvider,
|
||||
redirectToProviderOnLogout,
|
||||
} from '@/helpers/redirectToProvider'
|
||||
import {AUTH_TYPES, type IUser} from '@/modelTypes/IUser'
|
||||
import type {IUserSettings} from '@/modelTypes/IUserSettings'
|
||||
import router from '@/router'
|
||||
|
@ -17,6 +21,7 @@ import {useConfigStore} from '@/stores/config'
|
|||
import UserSettingsModel from '@/models/userSettings'
|
||||
import {MILLISECONDS_A_SECOND} from '@/constants/date'
|
||||
import {PrefixMode} from '@/modules/parseTaskText'
|
||||
import type {IProvider} from '@/types/IProvider'
|
||||
|
||||
function redirectToProviderIfNothingElseIsEnabled() {
|
||||
const {auth} = useConfigStore()
|
||||
|
@ -180,8 +185,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
const HTTP = HTTPFactory()
|
||||
setIsLoading(true)
|
||||
|
||||
const {auth} = useConfigStore()
|
||||
const fullProvider: IProvider = auth.openidConnect.providers.find((p: IProvider) => p.key === provider)
|
||||
|
||||
const data = {
|
||||
code: code,
|
||||
redirect_url: getRedirectUrlFromCurrentFrontendPath(fullProvider),
|
||||
}
|
||||
|
||||
// Delete an eventually preexisting old token
|
||||
|
|
|
@ -67,7 +67,7 @@ export const useLabelStore = defineStore('label', () => {
|
|||
}
|
||||
|
||||
function setLabel(label: ILabel) {
|
||||
labels.value[label.id] = label
|
||||
labels.value[label.id] = {...label}
|
||||
update(label)
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import {useBaseStore} from '@/stores/base'
|
|||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
|
||||
interface MatchedAssignee extends IUser {
|
||||
match: string,
|
||||
|
@ -337,7 +338,10 @@ export const useTaskStore = defineStore('task', () => {
|
|||
let label = validateLabel(Object.values(labelStore.labels), labelTitle)
|
||||
if (typeof label === 'undefined') {
|
||||
// label not found, create it
|
||||
const labelModel = new LabelModel({title: labelTitle})
|
||||
const labelModel = new LabelModel({
|
||||
title: labelTitle,
|
||||
hexColor: getRandomColorHex(),
|
||||
})
|
||||
label = await labelStore.createLabel(labelModel)
|
||||
}
|
||||
return label
|
||||
|
|
|
@ -256,6 +256,13 @@
|
|||
--card-border-color: var(--grey-200);
|
||||
--logo-text-color: hsl(180, 1%, 15%);
|
||||
|
||||
// Code colors
|
||||
--code-variable: #da2222;
|
||||
--code-literal: #fd8a09;
|
||||
--code-symbol: #0ead69;
|
||||
--code-section: #3a86ff;
|
||||
--code-keyword: #8338ec;
|
||||
|
||||
&.dark {
|
||||
@media screen {
|
||||
// Light mode colours reversed for dark mode
|
||||
|
@ -311,6 +318,13 @@
|
|||
--scheme-invert: var(--grey-900);
|
||||
--scheme-invert-bis: var(--grey-900);
|
||||
--scheme-invert-ter: var(--grey-800);
|
||||
|
||||
// Code colors
|
||||
--code-variable: #f98181;
|
||||
--code-literal: #fbbc88;
|
||||
--code-symbol: #b9f18d;
|
||||
--code-section: #faf594;
|
||||
--code-keyword: #70cff8;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -150,8 +150,8 @@ function deleteLabel(label: ILabel) {
|
|||
}
|
||||
|
||||
function editLabelSubmit() {
|
||||
return labelStore.updateLabel(labelEditLabel.value)
|
||||
}
|
||||
return labelStore.updateLabel(labelEditLabel.value)
|
||||
}
|
||||
|
||||
function editLabel(label: ILabel) {
|
||||
if (label.createdBy.id !== userInfo.value.id) {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from 'vue'
|
||||
import {computed, onBeforeMount, ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
|
@ -46,6 +46,7 @@ import LabelModel from '@/models/label'
|
|||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {success} from '@/message'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -55,6 +56,8 @@ useTitle(() => t('label.create.title'))
|
|||
const labelStore = useLabelStore()
|
||||
const label = ref(new LabelModel())
|
||||
|
||||
onBeforeMount(() => label.value.hexColor = getRandomColorHex())
|
||||
|
||||
const showError = ref(false)
|
||||
const loading = computed(() => labelStore.isLoading)
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ import {useI18n} from 'vue-i18n'
|
|||
import type {RouteLocationNormalized} from 'vue-router'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
|
@ -126,16 +126,13 @@ const flatPickerDateRange = computed<Date[]>({
|
|||
const initialDateRange = [filters.value.dateFrom, filters.value.dateTo]
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
const flatPickerConfig = computed<Options>(() => ({
|
||||
altFormat: t('date.altFormatShort'),
|
||||
altInput: true,
|
||||
defaultDate: initialDateRange,
|
||||
enableTime: false,
|
||||
mode: 'range',
|
||||
locale: {
|
||||
firstDayOfWeek: authStore.settings.weekStart,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
</script>
|
||||
|
||||
|
|
|
@ -179,7 +179,23 @@ watch(
|
|||
if (projectId < 0) {
|
||||
return
|
||||
}
|
||||
tasks.value = tasks.value.filter(t => typeof t.relatedTasks?.parenttask === 'undefined')
|
||||
const tasksById = {}
|
||||
tasks.value.forEach(t => tasksById[t.id] = true)
|
||||
|
||||
tasks.value = tasks.value.filter(t => {
|
||||
if (typeof t.relatedTasks?.parenttask === 'undefined') {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the task is a subtask, make sure the parent task is available in the current view as well
|
||||
for (const pt of t.relatedTasks.parenttask) {
|
||||
if(typeof tasksById[pt.id] === 'undefined') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -284,7 +300,7 @@ function prepareFiltersAndLoadTasks() {
|
|||
sortByParam.value = {}
|
||||
sortByParam.value[ALPHABETICAL_SORT] = 'asc'
|
||||
}
|
||||
|
||||
|
||||
loadTasks()
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -10,14 +10,12 @@
|
|||
{{ $t('project.delete.text1') }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong v-if="totalTasks !== null" class="has-text-white">
|
||||
{{
|
||||
totalTasks > 0 ? $t('project.delete.tasksToDelete', {count: totalTasks}) : $t('project.delete.noTasksToDelete')
|
||||
}}
|
||||
</strong>
|
||||
<Loading v-else class="is-loading-small" variant="default"/>
|
||||
<p class="has-text-weight-bold" v-if="totalTasks !== null">
|
||||
{{
|
||||
totalTasks > 0 ? $t('project.delete.tasksToDelete', {count: totalTasks}) : $t('project.delete.noTasksToDelete')
|
||||
}}
|
||||
</p>
|
||||
<Loading v-else class="is-loading-small" variant="default"/>
|
||||
|
||||
<p>
|
||||
{{ $t('misc.cannotBeUndone') }}
|
||||
|
|
|
@ -197,7 +197,7 @@ function validateSelectedEvents() {
|
|||
|
||||
<table
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth"
|
||||
v-if="webhooks"
|
||||
v-if="webhooks?.length > 0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -37,12 +37,17 @@
|
|||
{{ $t('task.attributes.assignees') }}
|
||||
</div>
|
||||
<edit-assignees
|
||||
:disabled="!canWrite"
|
||||
v-if="canWrite"
|
||||
:project-id="task.projectId"
|
||||
:task-id="task.id"
|
||||
:ref="e => setFieldRef('assignees', e)"
|
||||
v-model="task.assignees"
|
||||
/>
|
||||
<assignee-list
|
||||
v-else
|
||||
:assignees="task.assignees"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<CustomTransition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.priority">
|
||||
|
@ -513,6 +518,7 @@ import {useProjectStore} from '@/stores/projects'
|
|||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
|
||||
|
||||
const {
|
||||
taskId,
|
||||
|
|
|
@ -10,9 +10,9 @@ import {MILLISECONDS_A_DAY} from '@/constants/date'
|
|||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import type {IApiToken} from '@/modelTypes/IApiToken'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const service = new ApiTokenService()
|
||||
const tokens = ref<IApiToken[]>([])
|
||||
|
@ -32,18 +32,16 @@ const showDeleteModal = ref<boolean>(false)
|
|||
const tokenToDelete = ref<IApiToken>()
|
||||
|
||||
const {t} = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
locale: {
|
||||
firstDayOfWeek: authStore.settings.weekStart,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
minDate: now,
|
||||
}))
|
||||
|
||||
|
|
Reference in New Issue
Block a user