Skip to content

Commit

Permalink
feat: add task category field validation and bind package & category …
Browse files Browse the repository at this point in the history
…api (#5096)

* feat(ops-flow): add task field metadata and enhance task fields UI

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-fields-configuration): refactor task field management and add new components

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-fields-configuration): enhance task field generator with foldable options and new layout

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-fields): add functionality to dynamically add custom fields

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-fields-configuration): implement task fields management component

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-field): add dropdown task field options and validation logic

Signed-off-by: Wanjin Noh <[email protected]>

* feat(editor): add placeholder extension to text editor component

Signed-off-by: Wanjin Noh <[email protected]>

* feat: add validation event emission for task field options generator

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-management): add task management template functionality and translations

Signed-off-by: Wanjin Noh <[email protected]>

* feat(landing-panel): implement initial data loading and update functionality

Signed-off-by: Wanjin Noh <[email protected]>

* feat: enhance action menu button and add package management APIs

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-category): enhance task status options and add color constants

Signed-off-by: Wanjin Noh <[email protected]>

* fix: update import path for useDomainConfigStore in components and store

Signed-off-by: Wanjin Noh <[email protected]>

* feat(task-status): add default status menu and isDefault prop handling

Signed-off-by: Wanjin Noh <[email protected]>

* feat(ops-flow): conditionally render status type field in TaskStatusForm

Signed-off-by: Wanjin Noh <[email protected]>

---------

Signed-off-by: Wanjin Noh <[email protected]>
  • Loading branch information
WANZARGEN authored Nov 27, 2024
1 parent 751dca7 commit fc50870
Show file tree
Hide file tree
Showing 58 changed files with 2,052 additions and 1,171 deletions.
5 changes: 3 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@tiptap/core": "^2.0.3",
"@tiptap/extension-color": "^2.0.3",
"@tiptap/extension-link": "^2.0.3",
"@tiptap/extension-placeholder": "^2.10.2",
"@tiptap/extension-text-align": "^2.0.3",
"@tiptap/extension-text-style": "^2.0.3",
"@tiptap/extension-underline": "^2.0.3",
Expand Down Expand Up @@ -69,6 +70,7 @@
"prosemirror-state": "^1.4.3",
"prosemirror-transform": "^1.7.3",
"prosemirror-view": "^1.31.5",
"qrcode": "^1.5.4",
"uuid": "^8.3.2",
"v-click-outside": "^3.0.1",
"v-tooltip": "^2.0.3",
Expand All @@ -81,8 +83,7 @@
"vue-selecto": "^1.6.4",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"webfontloader": "^1.6.28",
"qrcode": "^1.5.4"
"webfontloader": "^1.6.28"
},
"devDependencies": {
"@babel/preset-env": "^7.21.4",
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/common/components/buttons/ActionMenuButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { ref, computed } from 'vue';
import { PIconButton, PContextMenu, useContextMenuStyle } from '@cloudforet/mirinae';
import type { MenuItem } from '@cloudforet/mirinae/types/controls/context-menu/type';
interface ActionMenuItem extends MenuItem {
name: string;
}
const props = withDefaults(defineProps<{
menu?: MenuItem[];
menu?: ActionMenuItem[];
}>(), {
menu: undefined,
});
Expand All @@ -27,7 +30,7 @@ useContextMenuStyle({
position: 'right',
menuWidth: '192px',
});
const menu = computed<MenuItem[]>(() => props.menu ?? [
const menu = computed<ActionMenuItem[]>(() => props.menu ?? [
{ name: 'edit', icon: 'ic_edit', label: 'Edit' },
{ name: 'delete', icon: 'ic_delete', label: 'Delete' },
]);
Expand All @@ -39,7 +42,7 @@ const hideMenu = () => {
};
onClickOutside(containerRef, hideMenu);
const handleSelectMenu = (item: MenuItem) => {
const handleSelectMenu = (item: ActionMenuItem) => {
emit(item.name);
hideMenu();
};
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/common/components/editor/TextEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { Color } from '@tiptap/extension-color';
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';
import TextAlign from '@tiptap/extension-text-align';
import TextStyle from '@tiptap/extension-text-style';
import Underline from '@tiptap/extension-underline';
Expand All @@ -23,6 +24,7 @@ interface Props {
imageUploader?: ImageUploader;
attachments?: Attachment[];
invalid?: boolean;
placeholder?: string;
}
const props = withDefaults(defineProps<Props>(), {
value: '',
Expand All @@ -32,6 +34,7 @@ const props = withDefaults(defineProps<Props>(), {
}),
attachments: () => [],
invalid: false,
placeholder: '',
});
const emit = defineEmits<{(e: 'update:value', value: string): void;
(e: 'update:attachments', attachments: Attachment[]): void;
Expand Down Expand Up @@ -59,6 +62,9 @@ onMounted(() => {
},
},
}),
Placeholder.configure({
placeholder: props.placeholder,
}),
Underline,
Link,
TextStyle,
Expand Down Expand Up @@ -111,6 +117,15 @@ watch([() => props.value, () => props.attachments], ([value, attachments], prev)
&:focus {
@apply outline-none;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
@apply text-gray-400;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/lib/site-initializer/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@ const getAfterCallApiMap = () => ({
const allReferenceStore = useAllReferenceStore();
allReferenceStore.sync('workspace', data);
},
'/identity/workspace/add-package': (data) => {
useAllReferenceStore(pinia);
const allReferenceStore = useAllReferenceStore();
allReferenceStore.sync('workspace', data);
},
'/identity/workspace/remove-package': (data) => {
useAllReferenceStore(pinia);
const allReferenceStore = useAllReferenceStore();
allReferenceStore.sync('workspace', data);
},
});

const getSessionTimeoutCallback = (store) => () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/schema/identity/package/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Tags } from '@/schema/_common/model';
export interface PackageModel {
package_id: string;
name: string;
description: string;
description?: string;
order: number;
is_default: boolean;
tags: Tags;
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/schema/identity/workspace/model.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { Tags } from '@/schema/_common/model';
import type { PackageModel } from '@/schema/identity/package/model';

export type WorkspaceState = 'ENABLED' | 'DISABLED' | 'DORMANT';

export interface WorkspaceModel {
workspace_id: string;
name: string;
state: WorkspaceState;
packages: PackageModel[];
packages: string[];
tags: Tags;
is_managed?: boolean;
reference_id?: string;
Expand Down
42 changes: 40 additions & 2 deletions apps/web/src/schema/opsflow/_types/task-field-type.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
export type TaskFieldType = |'GLOBAL'
|'TEXT'|'PARAGRAPH'|'LABELS'|'DROPDOWN'|'DATE'
|'USER'|'ASSET'|'PROJECT'|'PROVIDER'|'SERVICE_ACCOUNT';

export type TaskFieldSelectionType = 'SINGLE'|'MULTI';
export interface TaskField {

/* options */
export type TaskFieldEnum = {
key: string;
name: string;
};
interface TextTaskFieldOptions {
example: string;
}
export interface ParagraphTaskFieldOptions {
example: string;
}
export interface DropdownTaskFieldOptions {
enums: TaskFieldEnum[];
}
interface OtherTaskFieldOptions {
[key: string]: never;
}
export type TaskFieldOptions = TextTaskFieldOptions | ParagraphTaskFieldOptions | DropdownTaskFieldOptions | OtherTaskFieldOptions;

/* task field */
interface BaseTaskField {
field_id: string;
name: string;
description?: string;
field_type: TaskFieldType;
selection_type?: TaskFieldSelectionType;
is_required?: boolean;
is_primary?: boolean; // whether to display field during task creation
options?: string[]; // for dropdown field type
}
interface TextTaskField extends BaseTaskField {
field_type: 'TEXT';
options: TextTaskFieldOptions;
}
interface ParagraphTaskField extends BaseTaskField {
field_type: 'PARAGRAPH';
options: ParagraphTaskFieldOptions;
}
interface DropdownTaskField extends BaseTaskField {
field_type: 'DROPDOWN';
options: DropdownTaskFieldOptions;
}
interface OtherTaskField extends BaseTaskField {
field_type: Exclude<TaskFieldType, 'TEXT'|'PARAGRAPH'|'DROPDOWN'>;
options: object;
}
export type TaskField = TextTaskField | ParagraphTaskField | DropdownTaskField | OtherTaskField;
8 changes: 6 additions & 2 deletions apps/web/src/schema/opsflow/task-category/api-verbs/create.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { Tags } from '@/schema/_common/model';
import type { TaskField } from '@/schema/opsflow/_types/task-field-type';
import type { TaskStatusOptions } from '@/schema/opsflow/task/type';
import type { TaskStatusOption, TaskStatusType } from '@/schema/opsflow/task/type';

interface TaskStatusOptionWithOptionalId extends Omit<TaskStatusOption, 'status_id'> {
status_id?: string;
}

export interface TaskCategoryCreateParameters {
name: string;
description?: string;
status_options: TaskStatusOptions;
status_options: Record<TaskStatusType, TaskStatusOptionWithOptionalId[]>;
fields?: TaskField[];
tags?: Tags;
package_id: string;
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/schema/opsflow/task/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TASK_STATUS_COLOR_NAMES = ['gray200', 'violet200', 'blue200', 'peacock200', 'green200', 'yellow200', 'coral200', 'red200'] as const;
7 changes: 5 additions & 2 deletions apps/web/src/schema/opsflow/task/type.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { TASK_STATUS_COLOR_NAMES } from '@/schema/opsflow/task/constant';

export type TaskStatusType = 'TODO'|'IN_PROGRESS'|'COMPLETED';
type TaskStatusColorName = typeof TASK_STATUS_COLOR_NAMES[number];
export interface TaskStatusOption {
status_id: string;
name: string;
color: string;
is_default: boolean;
color: TaskStatusColorName;
is_default?: boolean;
}
export type TaskStatusOptions = Record<TaskStatusType, TaskStatusOption[]>;
export type TaskPriority = 'LOW'|'MEDIUM'|'HIGH';
48 changes: 46 additions & 2 deletions apps/web/src/services/ops-flow/components/EnableLandingPanel.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,56 @@
<script setup lang="ts">
import { reactive } from 'vue';
import type { Ref } from 'vue';
import { reactive, toRef } from 'vue';
import { APIError } from '@cloudforet/core-lib/space-connector/error';
import {
PPaneLayout, PHeading, PToggleButton,
} from '@cloudforet/mirinae';
import { useDomainConfigStore } from '@/store/domain/domain-config-store';
import ErrorHandler from '@/common/composables/error/errorHandler';
interface DomainConfigLandingData {
enabled: boolean;
}
const domainConfigStore = useDomainConfigStore();
const domainConfigStoreGetters = domainConfigStore.getters;
const landingData = toRef(domainConfigStoreGetters, 'TASK_LANDING') as unknown as Ref<DomainConfigLandingData|undefined>;
const state = reactive({
enableLandingPage: false,
});
const setInitialLandingData = async () => {
if (landingData.value?.enabled) {
state.enableLandingPage = landingData.value.enabled;
return;
}
try {
const res = await domainConfigStore.get<DomainConfigLandingData>('TASK_LANDING');
state.enableLandingPage = res.data.enabled ?? false;
} catch (e) {
if (e instanceof APIError && e.status === 404) return;
ErrorHandler.handleError(e);
}
};
const updateLandingData = async (enabled: boolean) => {
const prev = state.enableLandingPage;
state.enableLandingPage = enabled;
try {
await domainConfigStore.set<DomainConfigLandingData>('TASK_LANDING', { enabled });
} catch (e) {
ErrorHandler.handleError(e);
state.enableLandingPage = prev;
}
};
setInitialLandingData();
</script>

<template>
Expand All @@ -20,7 +63,8 @@ const state = reactive({
랜딩페이지를 사용하면 어떻게 되는지 Description
</p>
<p-toggle-button show-state-text
:value.sync="state.enableLandingPage"
:value="state.enableLandingPage"
@update:value="updateLandingData"
/>
</p-pane-layout>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ref, computed } from 'vue';
import { PButtonModal } from '@cloudforet/mirinae';
import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
import ErrorHandler from '@/common/composables/error/errorHandler';
import AssociatedCategories from '@/services/ops-flow/components/AssociatedCategories.vue';
Expand All @@ -22,6 +24,7 @@ const handleConfirm = async () => {
throw new Error('[Console Error] Cannot delete package without a target package');
}
await packageStore.delete(taskManagementPageStore.state.targetPackageId);
showSuccessMessage('Successfully deleted the package', '');
} catch (e) {
ErrorHandler.handleRequestError(e, 'Failed to delete package');
} finally {
Expand Down
33 changes: 26 additions & 7 deletions apps/web/src/services/ops-flow/components/PackageForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { PackageModel } from '@/schema/identity/package/model';
import { useWorkspaceReferenceStore } from '@/store/reference/workspace-reference-store';
import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
import ErrorHandler from '@/common/composables/error/errorHandler';
import { useFormValidator } from '@/common/composables/form-validator';
Expand Down Expand Up @@ -62,8 +64,8 @@ const {
setForm, isAllValid,
resetValidations,
} = useFormValidator({
name: '',
description: '',
name: '' as string,
description: '' as string|undefined,
categories: categoryValidator,
workspaces: workspaceValidator,
}, {
Expand All @@ -73,9 +75,6 @@ const {
if (packageStore.getters.packages.some((p) => p.package_id !== taskManagementPageState.targetPackageId && p.name === value)) return 'Name already exists';
return true;
},
description(value: string) {
return value.length > 0 ? true : 'Description is required';
},
});
/* modal */
Expand All @@ -96,10 +95,20 @@ const handleConfirm = async () => {
tags: {},
});
// bind workspaces and categories
await Promise.all([
const errorMessages: string[] = [];
const responses = await Promise.allSettled([
applyPackageToWorkspaces(updatedPackage.package_id),
applyPackageToCategories(updatedPackage.package_id),
]);
responses.forEach((response) => {
if (response.status === 'rejected') {
errorMessages.push(response.reason.message);
}
});
if (errorMessages.length) {
throw new Error(errorMessages.join('\n'));
}
showSuccessMessage('Successfully updated the package', '');
} catch (e) {
ErrorHandler.handleRequestError(e, 'Failed to update package');
}
Expand All @@ -111,10 +120,20 @@ const handleConfirm = async () => {
tags: {},
});
// bind workspaces and categories
await Promise.all([
const errorMessages: string[] = [];
const responses = await Promise.allSettled([
applyPackageToWorkspaces(createdPackage.package_id),
applyPackageToCategories(createdPackage.package_id),
]);
responses.forEach((response) => {
if (response.status === 'rejected') {
errorMessages.push(response.reason.message);
}
});
if (errorMessages.length) {
throw new Error(errorMessages.join('\n'));
}
showSuccessMessage('Successfully added the package', '');
} catch (e) {
ErrorHandler.handleRequestError(e, 'Failed to create package');
}
Expand Down
Loading

0 comments on commit fc50870

Please sign in to comment.