Compare commits

..

1 Commits

Author SHA1 Message Date
6fba3cd429 chore(deps): update dev-dependencies
Some checks failed
continuous-integration/drone/pr Build is failing
2023-12-11 17:20:49 +00:00
8 changed files with 57 additions and 204 deletions

View File

@ -5,19 +5,6 @@ import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
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', () => {
createFakeUserAndLogin()
prepareProjects()
@ -220,7 +207,15 @@ describe('Project View Kanban', () => {
})
it('Should remove a task from the board when deleting it', () => {
const task = createSingleTaskInBucket(5)
const projects = ProjectFactory.create(1)
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.get('.kanban .bucket .tasks .task')
@ -243,43 +238,4 @@ describe('Project View Kanban', () => {
cy.get('.kanban .bucket .tasks')
.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')
.contains('Add Attachments')
.click()
cy.get('input[type=file]#files', {timeout: 1000})
cy.get('input[type=file]', {timeout: 1000})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.wait('@uploadAttachment')
@ -112,50 +112,10 @@ describe('Task', () => {
.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', () => {
beforeEach(() => {
TaskCommentFactory.truncate()
LabelTaskFactory.truncate()
TaskAttachmentFactory.truncate()
})
it('Shows all task details', () => {
@ -253,45 +213,6 @@ describe('Task', () => {
.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', () => {
const tasks = TaskFactory.create(1, {
id: 1,
@ -771,7 +692,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: `
@ -840,7 +761,7 @@ describe('Task', () => {
.should('exist')
})
it('Should render an image from attachment', async () => {
it.only('Should render an image from attachment', async () => {
TaskAttachmentFactory.truncate()

View File

@ -167,7 +167,7 @@
"sass": "1.69.5",
"start-server-and-test": "2.0.3",
"typescript": "5.3.3",
"vite": "5.0.8",
"vite": "5.0.7",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.17.4",
"vite-plugin-sentry": "1.3.0",

View File

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

View File

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

View File

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

View File

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

View File

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