145 lines
3.3 KiB
Vue
145 lines
3.3 KiB
Vue
<template>
|
|
<div>
|
|
<h3>
|
|
<span class="icon is-grey">
|
|
<icon icon="align-left" />
|
|
</span>
|
|
{{ $t('task.attributes.description') }}
|
|
<CustomTransition name="fade">
|
|
<span
|
|
v-if="loading && saving"
|
|
class="is-small is-inline-flex"
|
|
>
|
|
<span class="loader is-inline-block mr-2" />
|
|
{{ $t('misc.saving') }}
|
|
</span>
|
|
<span
|
|
v-else-if="!loading && saved"
|
|
class="is-small has-text-success"
|
|
>
|
|
<icon icon="check" />
|
|
{{ $t('misc.saved') }}
|
|
</span>
|
|
</CustomTransition>
|
|
</h3>
|
|
<Editor
|
|
v-model="description"
|
|
class="tiptap__task-description"
|
|
:is-edit-enabled="canWrite"
|
|
:upload-callback="uploadCallback"
|
|
:placeholder="$t('task.description.placeholder')"
|
|
:show-save="true"
|
|
edit-shortcut="e"
|
|
:enable-discard-shortcut="true"
|
|
@update:modelValue="saveWithDelay"
|
|
@save="save"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {ref, computed, watch, onBeforeUnmount} from 'vue'
|
|
|
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
|
import Editor from '@/components/input/AsyncEditor'
|
|
|
|
import type {ITask} from '@/modelTypes/ITask'
|
|
import {useTaskStore} from '@/stores/tasks'
|
|
|
|
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
|
|
|
|
const {
|
|
modelValue,
|
|
attachmentUpload,
|
|
canWrite,
|
|
} = defineProps<{
|
|
modelValue: ITask,
|
|
attachmentUpload: AttachmentUploadFunction,
|
|
canWrite: boolean,
|
|
}>()
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const description = ref<string>('')
|
|
const saved = ref(false)
|
|
|
|
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
|
|
const saving = ref(false)
|
|
|
|
const taskStore = useTaskStore()
|
|
const loading = computed(() => taskStore.isLoading)
|
|
|
|
watch(
|
|
() => modelValue.description,
|
|
value => {
|
|
description.value = value
|
|
},
|
|
{immediate: true},
|
|
)
|
|
|
|
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
async function saveWithDelay() {
|
|
if (changeTimeout.value !== null) {
|
|
clearTimeout(changeTimeout.value)
|
|
}
|
|
|
|
changeTimeout.value = setTimeout(async () => {
|
|
await save()
|
|
}, 5000)
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
if (changeTimeout.value !== null) {
|
|
clearTimeout(changeTimeout.value)
|
|
}
|
|
})
|
|
|
|
async function save() {
|
|
if (changeTimeout.value !== null) {
|
|
clearTimeout(changeTimeout.value)
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
// FIXME: don't update state from internal.
|
|
const updated = await taskStore.update({
|
|
...modelValue,
|
|
description: description.value,
|
|
})
|
|
emit('update:modelValue', updated)
|
|
|
|
saved.value = true
|
|
setTimeout(() => {
|
|
saved.value = false
|
|
}, 2000)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
|
|
|
|
const uploadPromises: Promise<string>[] = []
|
|
|
|
files.forEach((file: File) => {
|
|
const promise = new Promise<string>((resolve) => {
|
|
attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
|
|
})
|
|
|
|
uploadPromises.push(promise)
|
|
})
|
|
|
|
return await Promise.all(uploadPromises)
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.tiptap__task-description {
|
|
// The exact amount of pixels we need to make the description icon align with the buttons and the form inside the editor.
|
|
// The icon is not exactly the same length on all sides so we need to hack our way around it.
|
|
margin-left: 4px;
|
|
}
|
|
</style>
|