Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
285410a
fix: reset and dispose stores on component unmount to prevent memory …
WANZARGEN Jan 3, 2025
c3f6366
fix: correct task initialization logic in create mode handling
WANZARGEN Jan 3, 2025
c202eb4
fix: correct default field check logic in TaskFieldGenerator component
WANZARGEN Jan 3, 2025
a96881b
refactor: streamline category loading and initialization across compo…
WANZARGEN Jan 3, 2025
12ceaf2
fix: correct project_id assignment in task content form store
WANZARGEN Jan 3, 2025
cd4d78f
fix: prevent multiple task creation by disabling buttons during process
WANZARGEN Jan 3, 2025
a0bf5c7
fix: filter out deleted categories in dropdown items computation
WANZARGEN Jan 3, 2025
c839ae9
fix(asset-task-field): correct field value reference in readonly display
WANZARGEN Jan 3, 2025
f854e6d
feat(file-download-helper): add token to download URL for secure access
WANZARGEN Jan 3, 2025
9e304ee
feat(editor-content-transformer): enhance content transformation with…
WANZARGEN Jan 3, 2025
2e9e3f0
feat(AssetTaskField): fetch and display asset names on component mount
WANZARGEN Jan 3, 2025
46d11be
feat(task): replace scope with required_project in task interfaces
WANZARGEN Jan 3, 2025
ed524cc
fix: load categories on component mount to prevent undefined access
WANZARGEN Jan 3, 2025
782b434
fix: prevent unnecessary updates when selected category items are unc…
WANZARGEN Jan 3, 2025
3cadcad
feat(task-fields-configuration): set is_primary to true by default in…
WANZARGEN Jan 3, 2025
fe11d26
fix(ops-flow): update margin class for button alignment on landing page
WANZARGEN Jan 3, 2025
fd980d1
fix: preload categories on task content form initialization
WANZARGEN Jan 3, 2025
69c8a7d
feat(editor-content-transformer): update fileId format to use angle b…
WANZARGEN Jan 3, 2025
64e45ea
feat(file-manager): add project resource group handling and default i…
WANZARGEN Jan 4, 2025
cd5d914
feat(image): add error handling for image loading and enhance attributes
WANZARGEN Jan 4, 2025
40802c5
feat(editor): add resourceId support for file upload and transformation
WANZARGEN Jan 4, 2025
4148c18
Merge branch 'develop' into feature-opsflow-jira
WANZARGEN Jan 6, 2025
df4bd8d
fix: rename required_project to require_project in task type interfaces
WANZARGEN Jan 6, 2025
27f2d1f
fix(ops-flow): add color and status type fields to TaskStatusForm
WANZARGEN Jan 6, 2025
f0f294f
feat(editor): add content type support for text editor viewer and tra…
WANZARGEN Jan 6, 2025
518bb7f
Merge branch 'develop' into feature-opsflow-jira
WANZARGEN Jan 6, 2025
604bd88
test(file-manager): add debug log for file upload process
WANZARGEN Jan 6, 2025
c386eaf
feat(file-manager): update project_id parameter handling for uploads
WANZARGEN Jan 6, 2025
72b8270
fix: remove unnecessary debug log and simplify project assignment logic
WANZARGEN Jan 6, 2025
34adff9
feat(editor): update content type handling and add plain text support
WANZARGEN Jan 6, 2025
0ab2537
fix(editor): update text editor structure and styling for clarity
WANZARGEN Jan 6, 2025
9ef3dbe
feat(ops-flow): add new hero images and update TaskTypeForm functiona…
WANZARGEN Jan 6, 2025
abf7f36
docs: update korean translation for landing description in task manag…
WANZARGEN Jan 6, 2025
379416e
fix: correct file ID extraction and improve parameter handling in upl…
WANZARGEN Jan 6, 2025
97a6967
feat(ops-flow): update text styles and add zero-width space for descr…
WANZARGEN Jan 6, 2025
9c53775
feat(asset-task-field): add search text update and multi-select options
WANZARGEN Jan 6, 2025
af73caf
feat(asset-task-field): update asset handling and improve related ass…
WANZARGEN Jan 6, 2025
d9ac1da
feat(AssetTaskField): update query filters for asset cloud service ID
WANZARGEN Jan 6, 2025
5e6279b
feat(variable-models): enhance type safety with generics in resource …
WANZARGEN Jan 7, 2025
ccf6f10
feat(asset-task-field): integrate variable model handler for cloud se…
WANZARGEN Jan 7, 2025
7c14f92
Merge branch 'develop' into feature-opsflow-jira
WANZARGEN Jan 7, 2025
f3f48d6
feat(ops-flow): update label from 'All Categories' to 'All Tasks'
WANZARGEN Jan 7, 2025
7056107
feat(ops-flow): update BoardTaskTable layout and remove description c…
WANZARGEN Jan 7, 2025
39eaef1
feat(timezone): add duration calculation and display in task table
WANZARGEN Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 51 additions & 53 deletions apps/web/src/common/components/editor/TextEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,32 @@ import type { AnyExtension } from '@tiptap/vue-2';
import { Editor, EditorContent } from '@tiptap/vue-2';
import { Markdown } from 'tiptap-markdown';

import { PTextarea } from '@cloudforet/mirinae';

import { createImageExtension } from '@/common/components/editor/extensions/image';
import { getAttachmentIds, setAttachmentsToContents } from '@/common/components/editor/extensions/image/helper';
import type { Attachment, ImageUploader } from '@/common/components/editor/extensions/image/type';
import type { ImageUploader } from '@/common/components/editor/extensions/image/type';
import MenuBar from '@/common/components/editor/MenuBar.vue';
import type { TextEditorContentsType } from '@/common/components/editor/type';

import { loadMonospaceFonts } from '@/styles/fonts';

interface Props {
value?: string;
imageUploader?: ImageUploader;
attachments?: Attachment[];
invalid?: boolean;
placeholder?: string;
contentType?: 'html'|'markdown';
contentsType?: TextEditorContentsType;
showUndoRedoButtons?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
value: '',
imageUploader: undefined,
attachments: () => [],
invalid: false,
placeholder: '',
contentType: 'html',
contentsType: 'html',
showUndoRedoButtons: true,
});
const emit = defineEmits<{(e: 'update:value', value: string): void;
(e: 'update:attachment-ids', attachmentIds: string[]): void;
}>();

loadMonospaceFonts();
Expand Down Expand Up @@ -68,13 +67,13 @@ const getExtensions = (): AnyExtension[] => {
];

// add extensions based on content type
if (props.contentType === 'html') {
if (props.contentsType === 'html') {
extensions.push(Color);
extensions.push(TextAlign.configure({
types: ['heading', 'paragraph'],
}));
}
if (props.contentType === 'markdown') {
if (props.contentsType === 'markdown') {
extensions.push(Markdown);
}

Expand All @@ -87,18 +86,17 @@ const getExtensions = (): AnyExtension[] => {

onMounted(() => {
editor.value = new Editor({
content: setAttachmentsToContents(props.value, props.attachments),
content: props.value,
extensions: getExtensions(),
onUpdate: () => {
let content = '';
if (!editor.value) return;
if (props.contentType === 'html') {
if (props.contentsType === 'html') {
content = editor.value?.getHTML() ?? '';
} else {
content = editor.value.storage.markdown.getMarkdown() ?? '';
}
emit('update:value', content);
emit('update:attachment-ids', getAttachmentIds(editor.value));
},
});
});
Expand All @@ -107,42 +105,50 @@ onBeforeUnmount(() => {
if (editor.value) editor.value.destroy();
});

watch([() => props.value, () => props.attachments], ([value, attachments], prev) => {
watch(() => props.value, (value) => {
if (!editor.value) return;
let isSame;
if (props.contentType === 'html') {
isSame = editor.value.getHTML() === value;

let contents: string;
if (props.contentsType === 'html') {
contents = editor.value?.getHTML() ?? '';
} else {
isSame = editor.value.storage.markdown.getMarkdown() === value;
contents = editor.value.storage.markdown.getMarkdown() ?? '';
}
if (isSame) return;
let newContents = value;
if (attachments !== prev[1]) newContents = setAttachmentsToContents(value, attachments);
editor.value.commands.setContent(newContents, false);
if (contents === value) return; // prevent infinite loop.

editor.value.commands.setContent(value, false);
});
</script>

<template>
<div v-if="editor"
class="text-editor"
:class="{invalid: props.invalid}"
>
<menu-bar :editor="editor"
:use-color="props.contentType === 'html'"
:use-text-align="props.contentType === 'html'"
:use-image="!!props.imageUploader"
:show-undo-redo-buttons="props.showUndoRedoButtons"
/>
<editor-content class="editor-content"
:editor="editor"
<div class="text-editor">
<p-textarea v-if="props.contentsType === 'plain'"
:value="props.value"
:placeholder="props.placeholder"
:invalid="props.invalid"
@update:value="emit('update:value', $event)"
/>
<div v-else-if="editor"
class="editor"
:class="{invalid: props.invalid}"
>
<menu-bar :editor="editor"
:use-color="props.contentsType === 'html'"
:use-text-align="props.contentsType === 'html'"
:use-image="!!props.imageUploader"
:show-undo-redo-buttons="props.showUndoRedoButtons"
/>
<editor-content class="editor-content"
:editor="editor"
/>
</div>
</div>
</template>

<style lang="postcss">
@import './text-editor-nodes.pcss';
.text-editor {
> .editor-content {
> .editor .editor-content {
.ProseMirror {
@mixin all-nodes-style;
min-height: inherit;
Expand All @@ -166,26 +172,18 @@ watch([() => props.value, () => props.attachments], ([value, attachments], prev)
.text-editor {
@apply bg-white border border-gray-200 rounded-lg;
min-height: 356px;
> .editor-content {
> .editor {
min-height: inherit;
padding: 0.75rem 1rem 1.125rem 1rem;
}
&:focus-within {
@apply border-secondary;
}
&.invalid {
@apply border-alert;
}
>.suggestion-list {
@apply absolute;
z-index: 10;
> .editor-content {
min-height: inherit;
padding: 0.75rem 1rem 1.125rem 1rem;
}
&:focus-within {
@apply border-secondary;
}
&.invalid {
@apply border-alert;
}
}
}
</style>

<style lang="postcss">
.mention {
@apply bg-violet-150 text-violet-600 rounded-md;
padding: 0 2px;
}
</style>
39 changes: 28 additions & 11 deletions apps/web/src/common/components/editor/TextEditorViewer.vue
Original file line number Diff line number Diff line change
@@ -1,45 +1,62 @@
<script setup lang="ts">
import { computed, toRef } from 'vue';
import {
computed, watch, nextTick, ref, toRef,
} from 'vue';

import DOMPurify from 'dompurify';

import { useMarkdown } from '@cloudforet/mirinae';

import { setAttachmentsToContents } from '@/common/components/editor/extensions/image/helper';
import type { Attachment } from '@/common/components/editor/extensions/image/type';
import type { TextEditorContentsType } from '@/common/components/editor/type';

import { loadMonospaceFonts } from '@/styles/fonts';

interface Props {
contents?: string;
attachments?: Attachment[];
showInBox?: boolean
contentType?: 'html'|'markdown';
contentsType?: TextEditorContentsType;
}
const props = withDefaults(defineProps<Props>(), {
contents: '',
attachments: () => [],
showInBox: false,
contentType: 'html',
contentsType: 'plain',
});

loadMonospaceFonts();

const { sanitizedHtml } = useMarkdown({
value: toRef(props, 'contents'),
inlineCodeClass: 'inline-code',
});
const refinedContents = computed(() => {
if (props.contentType === 'markdown') {
if (props.contentsType === 'markdown') {
return sanitizedHtml.value;
} if (props.contentsType === 'html') {
return DOMPurify.sanitize(props.contents);
}
const sanitized = DOMPurify.sanitize(props.contents);
return setAttachmentsToContents(sanitized, props.attachments);
// plain
return props.contents;
});

const htmlContainer = ref<null|HTMLElement>(null);
const addErrorHandlers = (container: HTMLElement) => {
container.querySelectorAll('img').forEach((img) => {
img.onerror = () => {
img.setAttribute('error', 'true');
};
});
};
watch([refinedContents, htmlContainer], async ([, container]) => {
if (!container) return;
await nextTick();
addErrorHandlers(container);
});
</script>

<template>
<!-- eslint-disable-next-line vue/no-v-html-->
<div class="text-editor-contents"
<div ref="htmlContainer"
class="text-editor-contents"
:class="{'contents-box': props.showInBox}"
v-html="refinedContents"
/>
Expand Down
45 changes: 0 additions & 45 deletions apps/web/src/common/components/editor/extensions/image/helper.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,2 @@
import type { Editor } from '@tiptap/vue-2';

import type { Attachment } from '@/common/components/editor/extensions/image/type';

// such as <p></p>
export const emptyHtmlRegExp = /<[^/>][^>]*><\/[^>]+>/;

export const getAttachmentIds = (editor: Editor): string[] => {
const contentsEl = editor.contentComponent?.$el;
if (!contentsEl) return [];
const imageElements = contentsEl.getElementsByTagName('img');
return Array.from(imageElements)
.reduce((results, imageElement) => {
const fileId = imageElement.getAttribute('file-id');
const src = imageElement.getAttribute('src');
if (fileId && src) {
results.push(fileId);
}

return results;
}, [] as string[]);
};

export const setAttachmentsToContents = (contents: string, attachments: Attachment[]): string => {
if (attachments.length === 0) return contents;

const contentsEl = document.createElement('div');
contentsEl.innerHTML = contents.trim();

const attachmentsMap = {};
attachments.forEach(({ fileId, downloadUrl }) => {
attachmentsMap[fileId] = downloadUrl;
});

const imageElements = contentsEl.getElementsByTagName('img');
Array.from(imageElements)
.forEach((imageElement) => {
const fileId = imageElement.getAttribute('file-id');
if (fileId && attachmentsMap[fileId]) {
imageElement.setAttribute('src', attachmentsMap[fileId]);
}
});

const newContents = contentsEl.innerHTML;
contentsEl.remove();
return newContents;
};
33 changes: 20 additions & 13 deletions apps/web/src/common/components/editor/extensions/image/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node, nodeInputRule } from '@tiptap/core';
import { Node, nodeInputRule, mergeAttributes } from '@tiptap/core';

import type { ImageUploader } from '@/common/components/editor/extensions/image/type';

Expand All @@ -19,16 +19,19 @@ export const createImageExtension = (uploadFn: ImageUploader) => Node.create({
inline: true,
group: 'inline',
draggable: true,
addAttributes: () => ({
src: {},
alt: { default: null },
title: { default: null },
'file-id': {},
'data-loading': { default: false }, // for loading spinner
style: { default: null }, // for loading spinner
width: { default: 'auto' },
height: { default: 'auto' },
}),
addAttributes() {
return {
src: {},
alt: { default: null },
title: { default: null },
'file-id': {},
'data-loading': { default: false }, // for loading spinner
style: { default: null }, // for loading spinner
width: { default: 'auto' },
height: { default: 'auto' },
error: { default: null },
};
},
parseHTML: () => [
{
tag: 'img[src]',
Expand All @@ -43,12 +46,16 @@ export const createImageExtension = (uploadFn: ImageUploader) => Node.create({
style: element.getAttribute('style'),
width: element.getAttribute('width'),
height: element.getAttribute('height'),
error: element.getAttribute('error'),
};
},
},
],
renderHTML: ({ HTMLAttributes }) => ['img', HTMLAttributes],

renderHTML({ HTMLAttributes }) {
return ['img', mergeAttributes(HTMLAttributes, {
onerror: "this.setAttribute('error', 'true')",
})];
},
// eslint-disable-next-line no-unused-vars
addCommands(this: any) {
return (attrs) => (state, dispatch) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ export const dropImagePlugin = (upload: ImageUploader) => new Plugin({

// upload and replace the loading node with the uploaded image node
upload(image).then(({ downloadUrl, fileId }) => {
const node = schema.nodes.image.create({
const imageNode = schema.nodes.image.create({
src: downloadUrl,
'file-id': fileId,
});
const loadingPos = view.state.selection.anchor - 1; // get the position of the loading node
const transaction = view.state.tr.setNodeMarkup(loadingPos, schema.nodes.image, node.attrs);
const transaction = view.state.tr.setNodeMarkup(loadingPos, schema.nodes.image, imageNode.attrs);
view.dispatch(transaction);
});
}
Expand Down Expand Up @@ -88,12 +88,12 @@ export const dropImagePlugin = (upload: ImageUploader) => new Plugin({

// upload and replace the loading node with the uploaded image node
const { downloadUrl, fileId } = await upload(image);
const node = schema.nodes.image.create({
const imageNode = schema.nodes.image.create({
src: downloadUrl,
'file-id': fileId,
});
const loadingPos = view.state.selection.anchor - 1; // get the position of the loading node
const transaction = view.state.tr.setNodeMarkup(loadingPos, schema.nodes.image, node.attrs);
const transaction = view.state.tr.setNodeMarkup(loadingPos, schema.nodes.image, imageNode.attrs);
view.dispatch(transaction);
} else {
reader.onload = (readerEvent) => {
Expand Down
Loading