Compare commits

..

17 Commits

Author SHA1 Message Date
1c5db5bfa5 fix(deps): update dependency @kyvg/vue3-notification to v3.1.1
Some checks failed
continuous-integration/drone/pr Build is failing
2023-12-15 15:16:43 +00:00
2541733c71
fix(tasks): prevent endless references
All checks were successful
continuous-integration/drone/push Build is passing
This would lead to failing attempts when updating the task later on (for example marking it as favorite)
2023-12-13 19:27:35 +01:00
4d6fd9ecc4
fix(tasks): favorited sub tasks are not shown in favorites pseudo list
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-13 19:22:28 +01:00
818f31c220
fix(tasks): update sub task relations in list view after they were created
All checks were successful
continuous-integration/drone/push Build is passing
Resolves #3853
2023-12-13 19:15:48 +01:00
34e4862c88
fix(kanban): make sure kanban cards always have text color matching their background
All checks were successful
continuous-integration/drone/push Build is passing
Resolves https://github.com/go-vikunja/frontend/issues/135
2023-12-13 18:54:48 +01:00
0d074113f1 chore(deps): update dev-dependencies (#3846)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3846
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-13 17:45:31 +00:00
0730955403
fix(sw): remove debug option via env as it would not be replaced correctly in prod builds
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-13 18:37:56 +01:00
75035ec1f8
fix(navigation): show filter settings dropdown
All checks were successful
continuous-integration/drone/push Build is passing
Resolves #3851
2023-12-13 18:30:10 +01:00
923fc4eaa0 fix(editor): keep editor open when emptying content from the outside (#3852)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3852
2023-12-11 23:13:30 +00:00
ea7dab68ae
fix(editor): add workaround for checklist tiptap bug
All checks were successful
continuous-integration/drone/pr Build is passing
2023-12-11 23:58:46 +01:00
8b9e5e54af
fix(test): use correct file input
Some checks failed
continuous-integration/drone/pr Build is failing
2023-12-11 23:23:25 +01:00
a4a2b95dc7
chore: cleanup
Some checks failed
continuous-integration/drone/pr Build is failing
2023-12-11 22:37:28 +01:00
9fdb6a8d24
feat(task): add more tests
Some checks reported errors
continuous-integration/drone/pr Build was killed
2023-12-11 22:35:32 +01:00
fc6b707405
fix(task): use empty description helper everywhere 2023-12-11 22:35:09 +01:00
9efe860f26
fix(editor): keep editor open when emptying content from the outside 2023-12-11 21:58:39 +01:00
af13d68c48
fix(editor): show editor if there is no content initially 2023-12-11 21:55:47 +01:00
3cb1e7dede
chore: debug 2023-12-11 21:01:38 +01:00
15 changed files with 586 additions and 414 deletions

View File

@ -6,7 +6,6 @@
# (2) Comment in and adjust the values as needed. # (2) Comment in and adjust the values as needed.
# VITE_IS_ONLINE=true # VITE_IS_ONLINE=true
# VITE_WORKBOX_DEBUG=false
# SENTRY_AUTH_TOKEN=YOUR_TOKEN # SENTRY_AUTH_TOKEN=YOUR_TOKEN
# SENTRY_ORG=vikunja # SENTRY_ORG=vikunja
# SENTRY_PROJECT=frontend-oss # SENTRY_PROJECT=frontend-oss

View File

@ -5,6 +5,19 @@ import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects' import {prepareProjects} from './prepareProjects'
function createSingleTaskInBucket(count = 1, attrs = {}) {
const projects = ProjectFactory.create(1)
const buckets = BucketFactory.create(2, {
project_id: projects[0].id,
})
const tasks = TaskFactory.create(count, {
project_id: projects[0].id,
bucket_id: buckets[0].id,
...attrs,
})
return tasks[0]
}
describe('Project View Kanban', () => { describe('Project View Kanban', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
prepareProjects() prepareProjects()
@ -207,15 +220,7 @@ describe('Project View Kanban', () => {
}) })
it('Should remove a task from the board when deleting it', () => { it('Should remove a task from the board when deleting it', () => {
const projects = ProjectFactory.create(1) const task = createSingleTaskInBucket(5)
const buckets = BucketFactory.create(2, {
project_id: projects[0].id,
})
const tasks = TaskFactory.create(5, {
project_id: 1,
bucket_id: buckets[0].id,
})
const task = tasks[0]
cy.visit('/projects/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task') cy.get('.kanban .bucket .tasks .task')
@ -238,4 +243,43 @@ describe('Project View Kanban', () => {
cy.get('.kanban .bucket .tasks') cy.get('.kanban .bucket .tasks')
.should('not.contain', task.title) .should('not.contain', task.title)
}) })
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: 'Lorem Ipsum',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('exist')
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: '',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: '<p></p>',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
}) })

View File

@ -36,7 +36,7 @@ function uploadAttachmentAndVerify(taskId: number) {
cy.get('.task-view .action-buttons .button') cy.get('.task-view .action-buttons .button')
.contains('Add Attachments') .contains('Add Attachments')
.click() .click()
cy.get('input[type=file]', {timeout: 1000}) cy.get('input[type=file]#files', {timeout: 1000})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose .selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.wait('@uploadAttachment') cy.wait('@uploadAttachment')
@ -112,10 +112,50 @@ describe('Task', () => {
.should('contain', 'Favorites') .should('contain', 'Favorites')
}) })
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: 'Lorem Ipsum',
})
cy.visit('/projects/1/list')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
.should('exist')
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: '',
})
cy.visit('/projects/1/list')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
.should('not.exist')
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: '<p></p>',
})
cy.visit('/projects/1/list')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
.should('not.exist')
})
describe('Task Detail View', () => { describe('Task Detail View', () => {
beforeEach(() => { beforeEach(() => {
TaskCommentFactory.truncate() TaskCommentFactory.truncate()
LabelTaskFactory.truncate() LabelTaskFactory.truncate()
TaskAttachmentFactory.truncate()
}) })
it('Shows all task details', () => { it('Shows all task details', () => {
@ -213,6 +253,45 @@ describe('Task', () => {
.should('exist') .should('exist')
}) })
it('Shows an empty editor when the description of a task is empty', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: '',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
.should('have.attr', 'data-placeholder')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.should('not.exist')
})
it('Shows a preview editor when the description of a task is not empty', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: 'Lorem Ipsum dolor sit amet',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
.should('not.have.attr', 'data-placeholder')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.should('exist')
})
it('Shows a preview editor when the description of a task contains html', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: '<p>Lorem Ipsum dolor sit amet</p>',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
.should('not.have.attr', 'data-placeholder')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.should('exist')
})
it('Can add a new comment', () => { it('Can add a new comment', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
@ -692,7 +771,7 @@ describe('Task', () => {
.should('exist') .should('exist')
}) })
it('Can check items off a checklist', () => { it.only('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
description: ` description: `
@ -761,7 +840,7 @@ describe('Task', () => {
.should('exist') .should('exist')
}) })
it.only('Should render an image from attachment', async () => { it('Should render an image from attachment', async () => {
TaskAttachmentFactory.truncate() TaskAttachmentFactory.truncate()

1
env.d.ts vendored
View File

@ -25,7 +25,6 @@ interface ImportMetaEnv {
readonly SENTRY_ORG?: string readonly SENTRY_ORG?: string
readonly SENTRY_PROJECT?: string readonly SENTRY_PROJECT?: string
readonly VITE_WORKBOX_DEBUG?: boolean
readonly VITE_IS_ONLINE: boolean readonly VITE_IS_ONLINE: boolean
} }

View File

@ -52,7 +52,7 @@
"@github/hotkey": "2.3.1", "@github/hotkey": "2.3.1",
"@infectoone/vue-ganttastic": "2.2.0", "@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "1.5.0", "@intlify/unplugin-vue-i18n": "1.5.0",
"@kyvg/vue3-notification": "3.1.0", "@kyvg/vue3-notification": "3.1.1",
"@sentry/tracing": "7.85.0", "@sentry/tracing": "7.85.0",
"@sentry/vue": "7.85.0", "@sentry/vue": "7.85.0",
"@tiptap/core": "2.1.13", "@tiptap/core": "2.1.13",
@ -136,23 +136,23 @@
"@types/is-touch-device": "1.0.2", "@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9", "@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2", "@types/marked": "5.0.2",
"@types/node": "20.10.3", "@types/node": "20.10.4",
"@types/postcss-preset-env": "7.7.0", "@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.7", "@types/sortablejs": "1.15.7",
"@typescript-eslint/eslint-plugin": "6.13.2", "@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.13.2", "@typescript-eslint/parser": "6.14.0",
"@vitejs/plugin-legacy": "5.2.0", "@vitejs/plugin-legacy": "5.2.0",
"@vitejs/plugin-vue": "4.5.1", "@vitejs/plugin-vue": "4.5.2",
"@vue/eslint-config-typescript": "12.0.0", "@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "2.4.3", "@vue/test-utils": "2.4.3",
"@vue/tsconfig": "0.4.0", "@vue/tsconfig": "0.4.0",
"autoprefixer": "10.4.16", "autoprefixer": "10.4.16",
"browserslist": "4.22.2", "browserslist": "4.22.2",
"caniuse-lite": "1.0.30001566", "caniuse-lite": "1.0.30001570",
"css-has-pseudo": "6.0.0", "css-has-pseudo": "6.0.0",
"csstype": "3.1.2", "csstype": "3.1.3",
"cypress": "13.6.1", "cypress": "13.6.1",
"esbuild": "0.19.8", "esbuild": "0.19.9",
"eslint": "8.55.0", "eslint": "8.55.0",
"eslint-plugin-vue": "9.19.2", "eslint-plugin-vue": "9.19.2",
"happy-dom": "12.10.3", "happy-dom": "12.10.3",
@ -162,17 +162,17 @@
"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.3.0", "postcss-preset-env": "9.3.0",
"rollup": "4.6.1", "rollup": "4.9.0",
"rollup-plugin-visualizer": "5.10.0", "rollup-plugin-visualizer": "5.11.0",
"sass": "1.69.5", "sass": "1.69.5",
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.3",
"typescript": "5.3.2", "typescript": "5.3.3",
"vite": "5.0.6", "vite": "5.0.8",
"vite-plugin-inject-preload": "1.3.3", "vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.17.3", "vite-plugin-pwa": "0.17.4",
"vite-plugin-sentry": "1.3.0", "vite-plugin-sentry": "1.3.0",
"vite-svg-loader": "5.1.0", "vite-svg-loader": "5.1.0",
"vitest": "1.0.1", "vitest": "1.0.4",
"vue-tsc": "1.8.25", "vue-tsc": "1.8.25",
"wait-on": "7.2.0", "wait-on": "7.2.0",
"workbox-cli": "7.0.0" "workbox-cli": "7.0.0"

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,6 @@
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton> </BaseButton>
<ProjectSettingsDropdown <ProjectSettingsDropdown
v-if="project.id > 0"
class="menu-list-dropdown" class="menu-list-dropdown"
:project="project" :project="project"
:level="level" :level="level"
@ -58,7 +57,6 @@
</BaseButton> </BaseButton>
</template> </template>
</ProjectSettingsDropdown> </ProjectSettingsDropdown>
<span class="list-setting-spacer" v-else></span>
</div> </div>
<ProjectsNavigation <ProjectsNavigation
v-if="canNestDeeper && childProjectsOpen && canCollapse" v-if="canNestDeeper && childProjectsOpen && canCollapse"

View File

@ -118,7 +118,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue' import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {refDebounced} from '@vueuse/core'
import EditorToolbar from './EditorToolbar.vue' import EditorToolbar from './EditorToolbar.vue'
@ -173,6 +172,7 @@ import {Placeholder} from '@tiptap/extension-placeholder'
import {eventToHotkeyString} from '@github/hotkey' import {eventToHotkeyString} from '@github/hotkey'
import {mergeAttributes} from '@tiptap/core' import {mergeAttributes} from '@tiptap/core'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const tiptapInstanceRef = ref<HTMLInputElement | null>(null) const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
@ -200,7 +200,9 @@ const CustomTableCell = TableCell.extend({
}) })
type CacheKey = `${ITask['id']}-${IAttachment['id']}` type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{ [key: CacheKey]: string }>({}) const loadedAttachments = ref<{
[key: CacheKey]: string
}>({})
const CustomImage = Image.extend({ const CustomImage = Image.extend({
addAttributes() { addAttributes() {
@ -272,7 +274,6 @@ const {
showSave = false, showSave = false,
placeholder = '', placeholder = '',
editShortcut = '', editShortcut = '',
initialMode = 'edit',
} = defineProps<{ } = defineProps<{
modelValue: string, modelValue: string,
uploadCallback?: UploadCallback, uploadCallback?: UploadCallback,
@ -281,13 +282,11 @@ const {
showSave?: boolean, showSave?: boolean,
placeholder?: string, placeholder?: string,
editShortcut?: string, editShortcut?: string,
initialMode?: Mode,
}>() }>()
const emit = defineEmits(['update:modelValue', 'save']) const emit = defineEmits(['update:modelValue', 'save'])
const inputHTML = ref('') const internalMode = ref<Mode>('edit')
const internalMode = ref<Mode>(initialMode)
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled) const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const editor = useEditor({ const editor = useEditor({
@ -359,14 +358,28 @@ const editor = useEditor({
TaskItem.configure({ TaskItem.configure({
nested: true, nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => { onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (isEditEnabled) { if (!isEditEnabled) {
node.attrs.checked = checked return false
inputHTML.value = editor.value?.getHTML()
bubbleSave()
return true
} }
return false // The following is a workaround for this bug:
// https://github.com/ueberdosis/tiptap/issues/4521
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
})
return true
}, },
}), }),
@ -376,52 +389,55 @@ const editor = useEditor({
BubbleMenu, BubbleMenu,
], ],
onUpdate: () => { onUpdate: () => {
inputHTML.value = editor.value!.getHTML() bubbleNow()
}, },
}) })
watch(
() => modelValue,
value => {
inputHTML.value = value
if (!editor?.value) return
if (editor.value.getHTML() === value) {
return
}
editor.value.commands.setContent(value, false)
},
)
const debouncedInputHTML = refDebounced(inputHTML, 1000)
watch(debouncedInputHTML, () => bubbleNow())
function bubbleNow() {
emit('update:modelValue', inputHTML.value)
}
function bubbleSave() {
bubbleNow()
emit('save', inputHTML.value)
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEdit() {
internalMode.value = 'edit'
editor.value?.commands.focus()
}
watch( watch(
() => isEditing.value, () => isEditing.value,
() => { () => {
editor.value?.setEditable(isEditing.value) editor.value?.setEditable(isEditing.value)
}, },
{immediate: true},
) )
watch(
() => modelValue,
value => {
if (!editor?.value) return
if (editor.value.getHTML() === value) {
return
}
setModeAndValue(value)
},
{immediate: true},
)
function bubbleNow() {
if (editor.value?.getHTML() === modelValue) {
return
}
emit('update:modelValue', editor.value?.getHTML())
}
function bubbleSave() {
bubbleNow()
emit('save', editor.value?.getHTML())
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEdit(focus: boolean = true) {
internalMode.value = 'edit'
if (focus) {
editor.value?.commands.focus()
}
}
onBeforeUnmount(() => editor.value?.destroy()) onBeforeUnmount(() => editor.value?.destroy())
const uploadInputRef = ref<HTMLInputElement | null>(null) const uploadInputRef = ref<HTMLInputElement | null>(null)
@ -491,15 +507,17 @@ function setLink() {
.run() .run()
} }
onMounted(() => { onMounted(async () => {
internalMode.value = initialMode
nextTick(() => {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
})
if (editShortcut !== '') { if (editShortcut !== '') {
document.addEventListener('keydown', setFocusToEditor) document.addEventListener('keydown', setFocusToEditor)
} }
await nextTick()
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
setModeAndValue(modelValue)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -512,6 +530,11 @@ onBeforeUnmount(() => {
} }
}) })
function setModeAndValue(value: string) {
internalMode.value = isEditorContentEmpty(value) ? 'edit' : 'preview'
editor.value?.commands.setContent(value, false)
}
function handleImagePaste(event) { function handleImagePaste(event) {
if (event?.clipboardData?.items?.length === 0) { if (event?.clipboardData?.items?.length === 0) {
return return
@ -521,7 +544,6 @@ function handleImagePaste(event) {
const image = event.clipboardData.items[0] const image = event.clipboardData.items[0]
if (image.kind === 'file' && image.type.startsWith('image/')) { if (image.kind === 'file' && image.type.startsWith('image/')) {
console.log('img', image.getAsFile())
uploadAndInsertFiles([image.getAsFile()]) uploadAndInsertFiles([image.getAsFile()])
} }
} }
@ -538,7 +560,7 @@ function setFocusToEditor(event) {
} }
event.preventDefault() event.preventDefault()
if (initialMode === 'preview' && isEditEnabled && !isEditing.value) { if (!isEditing.value && isEditEnabled) {
internalMode.value = 'edit' internalMode.value = 'edit'
} }

View File

@ -149,13 +149,15 @@ async function addTask() {
await Promise.all(newTasks) await Promise.all(newTasks)
const taskRelationService = new TaskRelationService() const taskRelationService = new TaskRelationService()
const allParentTasks = tasksToCreate.filter(t => t.parent !== null).map(t => t.parent)
const relations = tasksToCreate.map(async t => { const relations = tasksToCreate.map(async t => {
const createdTask = createdTasks[t.title] const createdTask = createdTasks[t.title]
if (typeof createdTask === 'undefined') { if (typeof createdTask === 'undefined') {
return return
} }
if (t.parent === null) { const isParent = allParentTasks.includes(t.title)
if (t.parent === null && !isParent) {
emit('taskAdded', createdTask) emit('taskAdded', createdTask)
return return
} }
@ -171,10 +173,19 @@ async function addTask() {
relationKind: RELATION_KIND.PARENTTASK, relationKind: RELATION_KIND.PARENTTASK,
})) }))
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask] createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [{
...createdParentTask,
relatedTasks: {}, // To avoid endless references
}]
// we're only emitting here so that the relation shows up in the project // we're only emitting here so that the relation shows up in the project
emit('taskAdded', createdTask) emit('taskAdded', createdTask)
createdParentTask.relatedTasks[RELATION_KIND.SUBTASK] = [{
...createdTask,
relatedTasks: {}, // To avoid endless references
}]
emit('taskAdded', createdParentTask)
return rel return rel
}) })
await Promise.all(relations) await Promise.all(relations)

View File

@ -26,7 +26,6 @@
v-model="description" v-model="description"
@update:model-value="saveWithDelay" @update:model-value="saveWithDelay"
@save="save" @save="save"
:initial-mode="isEditorContentEmpty(description) ? 'edit' : 'preview'"
/> />
</div> </div>
</template> </template>
@ -39,7 +38,6 @@ import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string> type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>

View File

@ -5,6 +5,7 @@
'is-loading': loadingInternal || loading, 'is-loading': loadingInternal || loading,
'draggable': !(loadingInternal || loading), 'draggable': !(loadingInternal || loading),
'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color), 'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color),
'has-custom-background-color': color !== TASK_DEFAULT_COLOR ? color : undefined,
}" }"
:style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : undefined}" :style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : undefined}"
@click.exact="openTaskDetail()" @click.exact="openTaskDetail()"
@ -48,7 +49,10 @@
</progress> </progress>
<div class="footer"> <div class="footer">
<labels :labels="task.labels"/> <labels :labels="task.labels"/>
<priority-label :priority="task.priority" :done="task.done" class="is-inline-flex is-align-items-center"/> <priority-label
:priority="task.priority"
:done="task.done"
class="is-inline-flex is-align-items-center"/>
<assignee-list <assignee-list
v-if="task.assignees.length > 0" v-if="task.assignees.length > 0"
:assignees="task.assignees" :assignees="task.assignees"
@ -60,7 +64,7 @@
<span class="icon" v-if="task.attachments.length > 0"> <span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/> <icon icon="paperclip"/>
</span> </span>
<span v-if="task.description" class="icon"> <span v-if="!isEditorContentEmpty(task.description)" class="icon">
<icon icon="align-left"/> <icon icon="align-left"/>
</span> </span>
<span class="icon" v-if="task.repeatAfter.amount > 0"> <span class="icon" v-if="task.repeatAfter.amount > 0">
@ -91,6 +95,7 @@ import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue' import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop' import {playPopSound} from '@/helpers/playPop'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const router = useRouter() const router = useRouter()
@ -113,7 +118,7 @@ async function toggleTaskDone(task: ITask) {
...task, ...task,
done: !task.done, done: !task.done,
}) })
if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) { if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound() playPopSound()
} }
@ -279,6 +284,16 @@ $task-background: var(--white);
width: auto; width: auto;
} }
&.has-custom-background-color {
color: hsl(215, 27.9%, 16.9%); // copied from grey-800 to avoid different values in dark mode
.footer .icon,
.due-date,
.priority-label {
background: hsl(220, 13%, 91%);
}
}
&.has-light-text { &.has-light-text {
color: var(--white); color: var(--white);

View File

@ -93,7 +93,7 @@
<span class="project-task-icon" v-if="task.attachments.length > 0"> <span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/> <icon icon="paperclip"/>
</span> </span>
<span class="project-task-icon" v-if="task.description"> <span class="project-task-icon" v-if="!isEditorContentEmpty(task.description)">
<icon icon="align-left"/> <icon icon="align-left"/>
</span> </span>
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0"> <span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
@ -184,6 +184,7 @@ import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useIntervalFn} from '@vueuse/core' import {useIntervalFn} from '@vueuse/core'
import {playPopSound} from '@/helpers/playPop' import {playPopSound} from '@/helpers/playPop'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const { const {
theTask, theTask,

View File

@ -11,7 +11,6 @@ const workboxVersion = 'v7.0.0'
importScripts(`${fullBaseUrl}workbox-${workboxVersion}/workbox-sw.js`) importScripts(`${fullBaseUrl}workbox-${workboxVersion}/workbox-sw.js`)
workbox.setConfig({ workbox.setConfig({
modulePathPrefix: `${fullBaseUrl}workbox-${workboxVersion}`, modulePathPrefix: `${fullBaseUrl}workbox-${workboxVersion}`,
debug: Boolean(import.meta.env.VITE_WORKBOX_DEBUG),
}) })
import { precacheAndRoute } from 'workbox-precaching' import { precacheAndRoute } from 'workbox-precaching'

View File

@ -175,7 +175,11 @@ const tasks = ref<ITask[]>([])
watch( watch(
allTasks, allTasks,
() => { () => {
tasks.value = [...allTasks.value].filter(t => typeof t.relatedTasks?.parenttask === 'undefined') tasks.value = [...allTasks.value]
if (projectId < 0) {
return
}
tasks.value = tasks.value.filter(t => typeof t.relatedTasks?.parenttask === 'undefined')
}, },
) )
@ -241,9 +245,9 @@ function updateTaskList(task: ITask) {
loadTasks() loadTasks()
} }
else { else {
tasks.value = [ allTasks.value = [
task, task,
...tasks.value, ...allTasks.value,
] ]
} }

View File

@ -6,7 +6,8 @@
'is-modal': isModal, 'is-modal': isModal,
}" }"
> >
<div class="task-view"> <!-- Removing everything until the task is loaded to prevent empty initialization of other components -->
<div class="task-view" v-if="visible">
<Heading <Heading
:task="task" :task="task"
@update:task="Object.assign(task, $event)" @update:task="Object.assign(task, $event)"
@ -605,7 +606,8 @@ watch(
} }
try { try {
Object.assign(task.value, await taskService.get({id})) const loaded = await taskService.get({id})
Object.assign(task.value, loaded)
attachmentStore.set(task.value.attachments) attachmentStore.set(task.value.attachments)
taskColor.value = task.value.hexColor taskColor.value = task.value.hexColor
setActiveFields() setActiveFields()