Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update file upload logkcs & space connector restClient #5393

Merged
merged 40 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2e39763
refactor: separate mention functionality from comment input component…
WANZARGEN Dec 23, 2024
979b115
feat: enhancements and fixes for validation, task field management (#…
WANZARGEN Dec 26, 2024
37104a5
feat(comment): add optional fields and mention source to comment mode…
WANZARGEN Dec 26, 2024
4de719c
feat(event): add status change interface and update event handling lo…
WANZARGEN Dec 26, 2024
3b5d1aa
feat: add conditional rendering for badge and fix field ID reference
WANZARGEN Dec 26, 2024
1cae173
feat(go-back): update goBack logic to use ref for path tracking
WANZARGEN Dec 26, 2024
cd128c2
feat(ops-flow): add landing description and improve layout for landin…
WANZARGEN Dec 30, 2024
e021a00
feat(routes): restructure board route to include nested children comp…
WANZARGEN Dec 30, 2024
2e43db9
fix: update icons and add workspaceId prop to task components (#5364)
WANZARGEN Dec 31, 2024
003e7cd
feat(associated-tasks): add workspace link and loading skeleton to ta…
WANZARGEN Dec 31, 2024
bf3c280
feat(project-link-button): add noRoleIfNotExists prop and translations
WANZARGEN Dec 31, 2024
c9ccdcf
fix: correct layout padding in AdminTaskCategoryDetailPageTaskTypeTab…
WANZARGEN Dec 31, 2024
bf3e524
feat(task-management): add getters for associated tasks and categories
WANZARGEN Dec 31, 2024
33c1d07
feat(comment-store): refactor comment management to use reactive comm…
WANZARGEN Dec 31, 2024
d22e3a0
refactor: use new opsflowTaskData composable
WANZARGEN Dec 31, 2024
271bbe6
refactor: move task-category-store imports to a new location
WANZARGEN Dec 31, 2024
e4b7998
fix: resolve category handling issues in task content form and store
WANZARGEN Dec 31, 2024
cba66ee
refactor: update task detail handling to use getters for improved cla…
WANZARGEN Dec 31, 2024
c980c2c
feat: add task status handling and improve error management in forms
WANZARGEN Dec 31, 2024
eaa92a5
feat(task): add scope field to task and comment models for categoriza…
WANZARGEN Dec 31, 2024
9ffea8c
feat(task-field): add project task field with match pattern option
WANZARGEN Dec 31, 2024
c4c339c
feat(task-fields): update task field components and validation logic
WANZARGEN Dec 31, 2024
9bb3f28
feat: restructure user-select-dropdown components for better layout
WANZARGEN Jan 1, 2025
c65af7f
fix: resolve readonly state for default fields in TaskFieldGenerator.vue
WANZARGEN Jan 1, 2025
675a832
docs: add documentation and stories for useContextMenuStyle hook
WANZARGEN Jan 2, 2025
bdb5af1
feat(AssetTaskField): implement data selector with cloud service options
WANZARGEN Jan 2, 2025
3be0161
fix: remove unused type import in dynamic-layout item component
WANZARGEN Jan 2, 2025
a453301
feat: add advanced menu display composable for dynamic menu rendering
WANZARGEN Jan 2, 2025
72d7bbc
feat(asset-inventory): add task tab to CloudServiceDetailTabs component
WANZARGEN Jan 2, 2025
bd720a0
fix(task-field-metadata): integrate task field metadata store and upd…
WANZARGEN Jan 2, 2025
63bd53b
feat(space-connector): add restClient for simplified API method access
WANZARGEN Jan 2, 2025
6c31377
feat(task-content-form): replace file model with file IDs for uploads
WANZARGEN Jan 2, 2025
6bd0caa
feat(image): simplify attachment handling by removing unnecessary dat…
WANZARGEN Jan 2, 2025
0446fbb
refactor(space-connector): update rest client to use serviceApiV2 ins…
WANZARGEN Jan 3, 2025
eefba2c
feat(file-attachments): enhance file attachment handling with resourc…
WANZARGEN Jan 3, 2025
e3662b6
refactor: simplify restClient type and initialization in SpaceConnector
WANZARGEN Jan 3, 2025
5da19fd
feat(file-manager): simplify file upload process and enhance resource…
WANZARGEN Jan 3, 2025
59c0dcc
feat(editor-content-transformer): add content transformation for edit…
WANZARGEN Jan 3, 2025
3a90840
feat(task-content-form): add create-minimal mode and improve task han…
WANZARGEN Jan 3, 2025
8f660f9
fix: resolve layout issues in OpsFlowLandingPage component
WANZARGEN Jan 3, 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
14 changes: 6 additions & 8 deletions apps/web/src/common/components/editor/TextEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ import { Editor, EditorContent } from '@tiptap/vue-2';
import { Markdown } from 'tiptap-markdown';

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

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

interface Props {
value?: string;
imageUploader?: ImageUploader<any>;
attachments?: Attachment<any>[];
imageUploader?: ImageUploader;
attachments?: Attachment[];
invalid?: boolean;
placeholder?: string;
contentType?: 'html'|'markdown';
Expand All @@ -40,15 +40,13 @@ const props = withDefaults(defineProps<Props>(), {
showUndoRedoButtons: true,
});
const emit = defineEmits<{(e: 'update:value', value: string): void;
(e: 'update:attachments', attachments: Attachment<any>[]): void;
(e: 'update:attachment-ids', attachmentIds: string[]): void;
}>();

loadMonospaceFonts();

const editor = shallowRef<null|Editor>(null);

const imgFileDataMap = new Map();

const getExtensions = (): AnyExtension[] => {
const extensions: AnyExtension[] = [
StarterKit.configure({
Expand Down Expand Up @@ -82,7 +80,7 @@ const getExtensions = (): AnyExtension[] => {

// add image extension if imageUploader is provided
if (props.imageUploader) {
extensions.push(createImageExtension(props.imageUploader, imgFileDataMap));
extensions.push(createImageExtension(props.imageUploader));
}
return extensions;
};
Expand All @@ -100,7 +98,7 @@ onMounted(() => {
content = editor.value.storage.markdown.getMarkdown() ?? '';
}
emit('update:value', content);
emit('update:attachments', getAttachments<any>(editor.value, imgFileDataMap));
emit('update:attachment-ids', getAttachmentIds(editor.value));
},
});
});
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/common/components/editor/extensions/image/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ import type { Attachment } from '@/common/components/editor/extensions/image/typ
// such as <p></p>
export const emptyHtmlRegExp = /<[^/>][^>]*><\/[^>]+>/;

export const getAttachments = <FileData>(editor: Editor, imgFileDataMap: Map<string, FileData>): Attachment<FileData>[] => {
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 downloadUrl = imageElement.getAttribute('src');
if (fileId && downloadUrl) {
results.push({ fileId, downloadUrl, data: imgFileDataMap.get(fileId) });
const src = imageElement.getAttribute('src');
if (fileId && src) {
results.push(fileId);
}

return results;
}, [] as Attachment<FileData>[]);
}, [] as string[]);
};

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

const contentsEl = document.createElement('div');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { dropImagePlugin } from './plugins/drop-image';
*/
const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;

export const createImageExtension = (uploadFn: ImageUploader<any>, imgFileDataMap: Map<string, any>) => Node.create({
export const createImageExtension = (uploadFn: ImageUploader) => Node.create({
name: 'image',
inline: true,
group: 'inline',
Expand Down Expand Up @@ -74,6 +74,6 @@ export const createImageExtension = (uploadFn: ImageUploader<any>, imgFileDataMa
];
},
addProseMirrorPlugins() {
return [dropImagePlugin(uploadFn, imgFileDataMap)];
return [dropImagePlugin(uploadFn)];
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const LOADING_IMAGE_NODE = {
'data-loading': true,
style: 'max-width: 2rem; max-height: 2rem;',
};
export const dropImagePlugin = (upload: ImageUploader<any>, imgFileDataMap: Map<string, any>) => new Plugin({
export const dropImagePlugin = (upload: ImageUploader) => new Plugin({
props: {
handleDOMEvents: {
paste: (view, _event: Event) => {
Expand All @@ -29,8 +29,7 @@ export const dropImagePlugin = (upload: ImageUploader<any>, imgFileDataMap: Map<
view.dispatch(loadingTransaction);

// upload and replace the loading node with the uploaded image node
upload(image).then(({ downloadUrl, fileId, data }) => {
if (data) imgFileDataMap.set(fileId, data);
upload(image).then(({ downloadUrl, fileId }) => {
const node = schema.nodes.image.create({
src: downloadUrl,
'file-id': fileId,
Expand Down Expand Up @@ -88,8 +87,7 @@ export const dropImagePlugin = (upload: ImageUploader<any>, imgFileDataMap: Map<
view.dispatch(loadingTransaction);

// upload and replace the loading node with the uploaded image node
const { downloadUrl, fileId, data } = await upload(image);
if (data) imgFileDataMap.set(fileId, data);
const { downloadUrl, fileId } = await upload(image);
const node = schema.nodes.image.create({
src: downloadUrl,
'file-id': fileId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export interface Attachment<Data = never> {
export interface Attachment {
downloadUrl: string;
fileId: string;
data?: Data;
}

export type ImageUploader<Data = never> = (image: File) => Promise<Attachment<Data>>;
export type ImageUploader = (image: File) => Promise<Attachment>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';

import { SpaceConnector } from '@cloudforet/core-lib/space-connector';

export const useEditorContentTransformer = (op?: {
value: ComputedRef<string>|Ref<string>;
contentType?: 'html'|'markdown';
}) => {
const { value, contentType = 'html' } = op || {};
const baseUri = SpaceConnector.restClient.getUri();

const replaceImageUrl = (url: string): string => {
const pattern = new RegExp(`${baseUri}/files/[^/]+/(file-[^?]+)\\?token=.*`);
const match = url.match(pattern);
if (match && match[1]) {
return `{${match[1]}}`; // Extract only the fileId and return it in the format {fileId}.
}
return url;
};

const transformHtmlContent = (content: string): string => {
const imagePattern = /<img\s+[^>]*src="([^"]+)"[^>]*>/g;
return content.replace(imagePattern, (match, url) => {
const newUrl = replaceImageUrl(url);
return match.replace(url, newUrl);
});
};
const transformMarkdownContent = (content: string): string => {
const markdownImagePattern = /!\[([^\]]*)\]\(([^)]+)\)/g;
return content.replace(markdownImagePattern, (match, altText, url) => {
const newUrl = replaceImageUrl(url);
return `![${altText}](${newUrl})`;
});
};

const transformEditorContent = (content: string): string => {
if (contentType === 'markdown') {
return transformMarkdownContent(content);
}
return transformHtmlContent(content);
};


const transformedEditorContent = computed(() => transformEditorContent(value ? value.value : ''));

return {
transformedEditorContent,
transformEditorContent,
};
};
34 changes: 16 additions & 18 deletions apps/web/src/common/composables/file-attachments/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
import { computedAsync } from '@vueuse/core';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';

import type { ResourceGroupType } from '@/schema/_common/type';
import type { FileModel } from '@/schema/file-manager/model';

import { getUploadedFile } from '@/lib/file-manager';
import { useAppContextStore } from '@/store/app-context/app-context-store';

import { getFileDownloadUrl } from '@/lib/file-manager';

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

export const useFileAttachments = (files: ComputedRef<FileModel[]>| Ref<FileModel[]>) => {
const noImage = `${window.location.origin}/images/no-image.png`;
export const useFileAttachments = (files: ComputedRef<FileModel[]>|Ref<FileModel[]>) => {
const appContextStore = useAppContextStore();
const appContextGetters = appContextStore.getters;
const resourceGroup = computed<Extract<ResourceGroupType, 'DOMAIN'|'WORKSPACE'>>(() => {
if (appContextGetters.isAdminMode) return 'DOMAIN';
return 'WORKSPACE';
});

const attachments = computedAsync<Attachment<FileModel>[]>(async (): Promise<Attachment<FileModel>[]> => {
const attachments = computed<Attachment[]>(() => {
if (files.value.length === 0) return [];

const results = await Promise.allSettled(files.value.map((file) => {
if (file.download_url) return Promise.resolve(file);
return getUploadedFile(file.file_id);
return files.value.map((file, idx) => ({
downloadUrl: getFileDownloadUrl(file.file_id, resourceGroup.value),
fileId: files.value[idx].file_id,
}));
return results.map((result, idx) => {
if (result.status === 'fulfilled') {
return {
downloadUrl: result.value.download_url ?? noImage,
fileId: result.value.file_id,
data: result.value,
};
}
return { downloadUrl: noImage, fileId: files.value[idx].file_id, data: files.value[idx] };
});
});

return {
Expand Down
11 changes: 1 addition & 10 deletions apps/web/src/common/composables/file-uploader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,20 @@ import { computed } from 'vue';
import type { ResourceGroupType } from '@/schema/_common/type';

import { useAppContextStore } from '@/store/app-context/app-context-store';
import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store';
import { useDomainStore } from '@/store/domain/domain-store';

import { uploadFileAndGetFileInfo } from '@/lib/file-manager';


export const useFileUploader = () => {
const appContextStore = useAppContextStore();
const appContextGetters = appContextStore.getters;
const userWorkspaceStore = useUserWorkspaceStore();
const workspaceGetters = userWorkspaceStore.getters;
const domainStore = useDomainStore();
const resourceGroup = computed<Extract<ResourceGroupType, 'DOMAIN'|'WORKSPACE'>>(() => {
if (appContextGetters.isAdminMode) return 'DOMAIN';
return 'WORKSPACE';
});
const domainIdOrWorkspaceId = computed<string>(() => {
if (appContextGetters.isAdminMode) return domainStore.state.domainId;
return workspaceGetters.currentWorkspaceId as string;
});
return {
fileUploader(file: File) {
return uploadFileAndGetFileInfo(file, resourceGroup.value, domainIdOrWorkspaceId.value);
return uploadFileAndGetFileInfo(file, resourceGroup.value);
},
};
};
12 changes: 7 additions & 5 deletions apps/web/src/common/composables/go-back/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { ref } from 'vue';
import type { Location } from 'vue-router';

// CAUTION: Do not change to useRouter() because useRouter is only available in script setup
import { SpaceRouter } from '@/router';

export const useGoBack = (mainRoute: Location) => {
let pathFrom;
const pathFrom = ref<Location|undefined>();
const setPathFrom = (path: Location) => {
pathFrom = path;
pathFrom.value = path;
};

const handleClickBackButton = () => {
if (!pathFrom?.name) { // in case of direct access from the other site, go to the main page
if (!pathFrom.value?.name) { // in case of direct access from the other site, go to the main page
SpaceRouter.router.push(mainRoute).catch(() => {});
} else if (pathFrom.name === mainRoute.name) { // in case of access from the service main page in the same site, go to the previous page
SpaceRouter.router.push(pathFrom).catch(() => {});
} else if (pathFrom.value.name === mainRoute.name) { // in case of access from the service main page in the same site, go to the previous page
SpaceRouter.router.push(pathFrom.value).catch(() => {});
} else { // in case of access from the other page in the same site, go to the main page
SpaceRouter.router.push(mainRoute).catch(() => {});
}
};

return {
pathFrom,
setPathFrom,
handleClickBackButton,
goBack: handleClickBackButton,
Expand Down
61 changes: 38 additions & 23 deletions apps/web/src/common/modules/project/ProjectLinkButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import { computed } from 'vue';
import type { Location } from 'vue-router';

import { PTextButton, PI, PLink } from '@cloudforet/mirinae';
import {
PTextButton, PI, PLink, PSkeleton,
} from '@cloudforet/mirinae';

import { useProjectReferenceStore } from '@/store/reference/project-reference-store';

Expand All @@ -14,43 +16,56 @@ const props = defineProps<{
projectId: string;
to?: Location;
useClickEvent?: boolean;
noRoleIfNotExists?: boolean;
}>();
const emit = defineEmits<{(event: 'click'): void;
}>();
const projectReferenceStore = useProjectReferenceStore();
const { getProperRouteLocation } = useProperRouteLocation();
const hasProjectReferenceLoaded = computed<boolean>(() => !!projectReferenceStore.getters.projectItems);
const projectPageLocation = computed<Location>(() => (getProperRouteLocation({
name: PROJECT_ROUTE.DETAIL._NAME,
params: {
id: props.projectId,
},
})));
const getProjectName = (projectId: string): string|undefined => projectReferenceStore.getters.projectItems[projectId]?.label;
</script>

<template>
<span>
<p-link v-if="!props.useClickEvent"
action-icon="internal-link"
new-tab
:to="props.to || projectPageLocation"
>
{{ projectReferenceStore.getters.projectItems[props.projectId]?.label || props.projectId }}
</p-link>
<p-text-button v-else
class="text-gray-900"
size="md"
@click="emit('click')"
>
<span>
{{ projectReferenceStore.getters.projectItems[props.projectId]?.label || props.projectId }}
<p-i name="ic_arrow-right-up"
class="link-mark"
height="0.875rem"
width="0.875rem"
color="inherit"
/>
</span>
</p-text-button>
<template v-if="hasProjectReferenceLoaded">
<template v-if="props.noRoleIfNotExists ? !!getProjectName(props.projectId) : true">
<p-link v-if="!props.useClickEvent"
action-icon="internal-link"
new-tab
:to="props.to || projectPageLocation"
>
{{ getProjectName(props.projectId) || props.projectId }}
</p-link>
<p-text-button v-else
class="text-gray-900"
size="md"
@click="emit('click')"
>
<span>
{{ getProjectName(props.projectId) || props.projectId }}
<p-i name="ic_arrow-right-up"
class="link-mark"
height="0.875rem"
width="0.875rem"
color="inherit"
/>
</span>
</p-text-button>
</template>
<template v-else>
{{ $t('COMMON.PROJECT_LINK_BUTTON.NO_ROLE') }}
</template>
</template>
<p-skeleton v-else
width="100%"
/>
</span>
</template>

Loading
Loading