import {
- toRef, ref, computed, watch, nextTick,
+ ref, watch, nextTick,
} from 'vue';
import {
POverlayLayout, PFieldGroup, PTextInput, PTextarea, PSelectDropdown, PButton,
} from '@cloudforet/mirinae';
-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';
@@ -18,14 +14,13 @@ import { useFormValidator } from '@/common/composables/form-validator';
import { useCategoryField } from '@/services/ops-flow/composables/use-category-field';
import { useWorkspaceField } from '@/services/ops-flow/composables/use-workspace-field';
+import { usePackageStore } from '@/services/ops-flow/stores/admin/package-store';
import { useTaskManagementPageStore } from '@/services/ops-flow/stores/admin/task-management-page-store';
-const workspaceReferenceStore = useWorkspaceReferenceStore();
const taskManagementPageStore = useTaskManagementPageStore();
const taskManagementPageState = taskManagementPageStore.state;
const taskManagementPageGetters = taskManagementPageStore.getters;
-const packageStore = taskManagementPageStore.packageStore;
-const taskCategoryStore = taskManagementPageStore.taskCategoryStore;
+const packageStore = usePackageStore();
@@ -37,22 +32,17 @@ const {
handleUpdateSelectedWorkspaces,
setInitialWorkspaces,
applyPackageToWorkspaces,
-} = useWorkspaceField({
- workspaceReferenceMap: toRef(workspaceReferenceStore.state, 'items'),
-});
+} = useWorkspaceField();
/* category */
const {
selectedCategoryItems,
categoryMenuItemsHandler,
categoryValidator,
- handleUpdateSelectedCategories,
- setInitialCategories,
+ setSelectedCategoryItems,
+ setInitialCategoriesByPackageId,
applyPackageToCategories,
-} = useCategoryField({
- defaultPackage: computed(() => packageStore.getters.packages.find((p) => p.is_default)),
- taskCategoryStore,
-});
+} = useCategoryField();
/* form */
const {
@@ -82,6 +72,9 @@ const loading = ref(false);
const handleCancelOrClose = () => {
taskManagementPageStore.closePackageForm();
};
+const handleClosed = () => {
+ taskManagementPageStore.resetTargetPackageId();
+};
const handleConfirm = async () => {
if (!isAllValid.value) return;
@@ -151,7 +144,7 @@ watch([() => taskManagementPageState.visiblePackageForm, () => taskManagementPag
description: '',
});
setInitialWorkspaces();
- setInitialCategories();
+ await setInitialCategoriesByPackageId();
resetValidations();
return;
}
@@ -161,7 +154,7 @@ watch([() => taskManagementPageState.visiblePackageForm, () => taskManagementPag
description: targetPackage.description,
});
setInitialWorkspaces(targetPackage.package_id);
- setInitialCategories(targetPackage.package_id);
+ await setInitialCategoriesByPackageId(targetPackage.package_id);
}
});
@@ -171,6 +164,7 @@ watch([() => taskManagementPageState.visiblePackageForm, () => taskManagementPag
@@ -234,7 +228,7 @@ watch([() => taskManagementPageState.visiblePackageForm, () => taskManagementPag
show-clear-selection
is-filterable
init-selected-with-handler
- @update:selected="handleUpdateSelectedCategories"
+ @update:selected="setSelectedCategoryItems"
/>
diff --git a/apps/web/src/services/ops-flow/components/PackagePanel.vue b/apps/web/src/services/ops-flow/components/PackagePanel.vue
index 0d77601d7a..f79c0185c0 100644
--- a/apps/web/src/services/ops-flow/components/PackagePanel.vue
+++ b/apps/web/src/services/ops-flow/components/PackagePanel.vue
@@ -9,11 +9,12 @@ import type { DataTableField } from '@cloudforet/mirinae/types/data-display/tabl
import ActionMenuButton from '@/common/components/buttons/ActionMenuButton.vue';
+import { usePackageStore } from '@/services/ops-flow/stores/admin/package-store';
import { useTaskManagementPageStore } from '@/services/ops-flow/stores/admin/task-management-page-store';
const taskManagementPageStore = useTaskManagementPageStore();
-const packageStore = taskManagementPageStore.packageStore;
+const packageStore = usePackageStore();
const state = reactive({
packageFields: computed(() => [
@@ -75,7 +76,7 @@ const state = reactive({
계약에 따라 고객이 이용할 수 있는 기능의 묶음입니다. 전체 서비스의 범위를 나타내며, 하위 카테고를 생성할 수 있습니다.
-
diff --git a/apps/web/src/services/ops-flow/components/PackageSetDefaultModal.vue b/apps/web/src/services/ops-flow/components/PackageSetDefaultModal.vue
index 8e779560c2..6ca4f81c5c 100644
--- a/apps/web/src/services/ops-flow/components/PackageSetDefaultModal.vue
+++ b/apps/web/src/services/ops-flow/components/PackageSetDefaultModal.vue
@@ -7,10 +7,12 @@ import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
import ErrorHandler from '@/common/composables/error/errorHandler';
+import { usePackageStore } from '@/services/ops-flow/stores/admin/package-store';
import { useTaskManagementPageStore } from '@/services/ops-flow/stores/admin/task-management-page-store';
const taskManagementPageStore = useTaskManagementPageStore();
-const packageStore = taskManagementPageStore.packageStore;
+const packageStore = usePackageStore();
+
const loading = ref(false);
const name = computed(() => taskManagementPageStore.getters.targetPackage?.name ?? '');
const handleConfirm = async () => {
@@ -31,6 +33,9 @@ const handleConfirm = async () => {
const handleCloseOrCancel = () => {
taskManagementPageStore.closeSetDefaultPackageModal();
};
+const handleClosed = () => {
+ taskManagementPageStore.resetTargetPackageId();
+};
@@ -40,6 +45,7 @@ const handleCloseOrCancel = () => {
@confirm="handleConfirm"
@close="handleCloseOrCancel"
@cancel="handleCloseOrCancel"
+ @closed="handleClosed"
>
Set {{ name }} as the default package.
diff --git a/apps/web/src/services/ops-flow/components/TaskCategoryDeleteModal.vue b/apps/web/src/services/ops-flow/components/TaskCategoryDeleteModal.vue
index 241e074e2d..925c44c55c 100644
--- a/apps/web/src/services/ops-flow/components/TaskCategoryDeleteModal.vue
+++ b/apps/web/src/services/ops-flow/components/TaskCategoryDeleteModal.vue
@@ -4,10 +4,11 @@ import { ref } from 'vue';
import DeleteModal from '@/common/components/modals/DeleteModal.vue';
import ErrorHandler from '@/common/composables/error/errorHandler';
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
import { useTaskManagementPageStore } from '@/services/ops-flow/stores/admin/task-management-page-store';
const taskManagementPageStore = useTaskManagementPageStore();
-const taskCategoryStore = taskManagementPageStore.taskCategoryStore;
+const taskCategoryStore = useTaskCategoryStore();
const loading = ref(false);
const handleConfirm = async () => {
@@ -27,6 +28,9 @@ const handleConfirm = async () => {
const handleCloseOrCancel = () => {
taskManagementPageStore.closeDeleteCategoryModal();
};
+const handleClosed = () => {
+ taskManagementPageStore.resetTargetCategoryId();
+};
@@ -37,5 +41,6 @@ const handleCloseOrCancel = () => {
@confirm="handleConfirm"
@close="handleCloseOrCancel"
@cancel="handleCloseOrCancel"
+ @closed="handleClosed"
/>
diff --git a/apps/web/src/services/ops-flow/components/TaskCategoryForm.vue b/apps/web/src/services/ops-flow/components/TaskCategoryForm.vue
index 0903620910..53a6ff5964 100644
--- a/apps/web/src/services/ops-flow/components/TaskCategoryForm.vue
+++ b/apps/web/src/services/ops-flow/components/TaskCategoryForm.vue
@@ -10,12 +10,13 @@ import {
import ErrorHandler from '@/common/composables/error/errorHandler';
import { useFormValidator } from '@/common/composables/form-validator';
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
import { useTaskManagementPageStore } from '@/services/ops-flow/stores/admin/task-management-page-store';
const taskManagementPageStore = useTaskManagementPageStore();
const taskManagementPageState = taskManagementPageStore.state;
const taskManagementPageGetters = taskManagementPageStore.getters;
-const taskCategoryStore = taskManagementPageStore.taskCategoryStore;
+const taskCategoryStore = useTaskCategoryStore();
const {
forms: { name, description },
@@ -41,6 +42,9 @@ const handleCancelOrClose = () => {
initForm();
taskManagementPageStore.closeCategoryForm();
};
+const handleClosed = () => {
+ taskManagementPageStore.resetTargetCategoryId();
+};
const handleConfirm = async () => {
if (!isAllValid.value) return;
@@ -103,6 +107,7 @@ watch([() => taskManagementPageState.visibleCategoryForm, () => taskManagementPa
diff --git a/apps/web/src/services/ops-flow/components/TaskCategoryPanel.vue b/apps/web/src/services/ops-flow/components/TaskCategoryPanel.vue
index e84144d69d..2c689ad644 100644
--- a/apps/web/src/services/ops-flow/components/TaskCategoryPanel.vue
+++ b/apps/web/src/services/ops-flow/components/TaskCategoryPanel.vue
@@ -11,14 +11,16 @@ import { makeAdminRouteName } from '@/router/helpers/route-helper';
import ActionMenuButton from '@/common/components/buttons/ActionMenuButton.vue';
import { OPS_FLOW_ROUTE } from '@/services/ops-flow/routes/route-constant';
+import { usePackageStore } from '@/services/ops-flow/stores/admin/package-store';
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
import { useTaskManagementPageStore } from '@/services/ops-flow/stores/admin/task-management-page-store';
import {
useTaskManagementTemplateStore,
} from '@/services/ops-flow/task-management-templates/stores/use-task-management-template-store';
const taskManagementPageStore = useTaskManagementPageStore();
-const taskCategoryStore = taskManagementPageStore.taskCategoryStore;
-const packageStore = taskManagementPageStore.packageStore;
+const taskCategoryStore = useTaskCategoryStore();
+const packageStore = usePackageStore();
const taskManagementTemplateStore = useTaskManagementTemplateStore();
@@ -66,7 +68,7 @@ const state = reactive({
티켓의 유형을 그룹화한 것입니다. 고객은 티켓을 제출할 때 적절한 카테고리를 선택하여 담당자가 티켓을 효율적으로 관리할 수 있게 돕습니다.
-
diff --git a/apps/web/src/services/ops-flow/components/TaskContentBaseForm.vue b/apps/web/src/services/ops-flow/components/TaskContentBaseForm.vue
new file mode 100644
index 0000000000..5eb55bf993
--- /dev/null
+++ b/apps/web/src/services/ops-flow/components/TaskContentBaseForm.vue
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/components/TaskCreateProgressTab.vue b/apps/web/src/services/ops-flow/components/TaskCreateProgressTab.vue
new file mode 100644
index 0000000000..440e85577d
--- /dev/null
+++ b/apps/web/src/services/ops-flow/components/TaskCreateProgressTab.vue
@@ -0,0 +1,7 @@
+
+
+
+ progress
+
diff --git a/apps/web/src/services/ops-flow/components/TaskStatusDeleteModal.vue b/apps/web/src/services/ops-flow/components/TaskStatusDeleteModal.vue
index 298be3ce2b..a6103098bd 100644
--- a/apps/web/src/services/ops-flow/components/TaskStatusDeleteModal.vue
+++ b/apps/web/src/services/ops-flow/components/TaskStatusDeleteModal.vue
@@ -1,21 +1,61 @@
@@ -26,5 +66,6 @@ const handleCloseOrCancel = () => {
@confirm="handleConfirm"
@close="handleCloseOrCancel"
@cancel="handleCloseOrCancel"
+ @closed="handleClosed"
/>
diff --git a/apps/web/src/services/ops-flow/components/TaskStatusDraggableItem.vue b/apps/web/src/services/ops-flow/components/TaskStatusDraggableItem.vue
index 16cc8a0359..f7725907a5 100644
--- a/apps/web/src/services/ops-flow/components/TaskStatusDraggableItem.vue
+++ b/apps/web/src/services/ops-flow/components/TaskStatusDraggableItem.vue
@@ -39,10 +39,13 @@ const defaultStatusMenu = computed
diff --git a/apps/web/src/services/ops-flow/components/TaskStatusForm.vue b/apps/web/src/services/ops-flow/components/TaskStatusForm.vue
index df80bd41b1..356e5989c7 100644
--- a/apps/web/src/services/ops-flow/components/TaskStatusForm.vue
+++ b/apps/web/src/services/ops-flow/components/TaskStatusForm.vue
@@ -3,6 +3,7 @@ import {
ref, computed, onBeforeMount, nextTick, watch,
} from 'vue';
+import { cloneDeep } from 'lodash';
import {
POverlayLayout, PFieldGroup, PTextInput, PSelectDropdown, PButton, PI,
@@ -10,17 +11,23 @@ import {
import type { SelectDropdownMenuItem } from '@cloudforet/mirinae/types/controls/dropdown/select-dropdown/type';
import { TASK_STATUS_COLOR_NAMES } from '@/schema/opsflow/task/constant';
+import type {
+ TaskStatusColorName,
+ TaskStatusOption, TaskStatusOptions,
+ TaskStatusType,
+} from '@/schema/opsflow/task/type';
+
+import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
import ErrorHandler from '@/common/composables/error/errorHandler';
import { useFormValidator } from '@/common/composables/form-validator';
import { useTaskCategoryPageStore } from '@/services/ops-flow/stores/admin/task-category-page-store';
-
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
const taskCategoryPageStore = useTaskCategoryPageStore();
const taskCategoryPageState = taskCategoryPageStore.state;
-const taskCategoryPageGetters = taskCategoryPageStore.getters;
-
+const taskCategoryStore = useTaskCategoryStore();
/* status type */
const statusIdAndNames = computed<[id: string, name: string][]>(() => Object.values(taskCategoryPageStore.getters.statusOptions).flat().map((p) => [p.status_id, p.name]));
@@ -41,12 +48,12 @@ const {
} = useFormValidator({
name: '',
statusType: statusTypeItems.value[0] as SelectDropdownMenuItem,
- color: '',
+ color: TASK_STATUS_COLOR_NAMES[0] as TaskStatusColorName,
}, {
name(value: string) {
if (!value.trim().length) return 'Name is required';
if (value.length > 50) return 'Name should be less than 50 characters';
- const statusId = taskCategoryPageGetters.targetStatusOption?.data.status_id;
+ const statusId = taskCategoryPageStore.getters.targetStatusOption?.data.status_id;
if (statusIdAndNames.value.some((p) => p[1] === value && p[0] !== statusId)) return 'Name already exists';
return true;
},
@@ -58,38 +65,87 @@ const handleCancelOrClose = () => {
taskCategoryPageStore.closeStatusForm();
};
+const updateStatusOptions = async (categoryId: string, allStatusOptions: TaskStatusOptions, targetStatusOption: {
+ type: TaskStatusType;
+ data: TaskStatusOption;
+ }) => {
+ try {
+ const newStatusOptions = cloneDeep(allStatusOptions);
+ const { type, data } = targetStatusOption;
+ if (statusType.value.name !== type) {
+ newStatusOptions[statusType.value.name].push({
+ ...data,
+ name: name.value,
+ color: color.value,
+ });
+ newStatusOptions[type] = newStatusOptions[type].filter((p) => p.status_id !== data.status_id);
+ } else {
+ const target = newStatusOptions[type].find((p) => p.status_id === data.status_id);
+ if (!target) throw new Error('[Console Error] Failed to find target status option');
+ target.name = name.value;
+ target.color = color.value;
+ }
+
+ await taskCategoryStore.update({
+ category_id: categoryId,
+ status_options: newStatusOptions,
+ force: true,
+ });
+ showSuccessMessage('Task status option updated successfully', '');
+ } catch (e) {
+ ErrorHandler.handleRequestError(e, 'Failed to update task status option');
+ }
+};
+
+const createStatusOption = async (categoryId: string, allStatusOptions: TaskStatusOptions) => {
+ try {
+ const newStatusOptions = cloneDeep(allStatusOptions);
+ newStatusOptions[statusType.value.name].push({
+ name: name.value,
+ color: color.value,
+ });
+
+ await taskCategoryStore.update({
+ category_id: categoryId,
+ status_options: newStatusOptions,
+ force: true,
+ });
+ showSuccessMessage('Task status option created successfully', '');
+ } catch (e) {
+ ErrorHandler.handleRequestError(e, 'Failed to create task status option');
+ }
+};
+
const handleConfirm = async () => {
if (!isAllValid.value) return;
- if (!taskCategoryPageState.currentCategoryId) return;
try {
+ if (!taskCategoryPageState.currentCategoryId) {
+ throw new Error('Category ID is required');
+ }
loading.value = true;
- if (taskCategoryPageGetters.targetStatusOption) {
- // await taskCategoryStore.update({
- //
- // });
+ if (taskCategoryPageStore.getters.targetStatusOption) {
+ await updateStatusOptions(taskCategoryPageState.currentCategoryId, taskCategoryPageStore.getters.statusOptions, taskCategoryPageStore.getters.targetStatusOption);
} else {
- // await taskCategoryStore.create({
- // });
+ await createStatusOption(taskCategoryPageState.currentCategoryId, taskCategoryPageStore.getters.statusOptions);
}
taskCategoryPageStore.closeStatusForm();
} catch (e) {
- ErrorHandler.handleRequestError(e, 'Failed to save category');
- // TODO: handle error
+ ErrorHandler.handleError(e);
} finally {
loading.value = false;
}
};
onBeforeMount(() => {
- if (taskCategoryPageGetters.targetStatusOption) {
- const { type, data } = taskCategoryPageGetters.targetStatusOption;
+ if (taskCategoryPageStore.getters.targetStatusOption) {
+ const { type, data } = taskCategoryPageStore.getters.targetStatusOption;
setForm('name', data.name);
setForm('statusType', statusTypeItems.value.find((p) => p.name === type) ?? statusTypeItems.value[0]);
setForm('color', data.color ?? TASK_STATUS_COLOR_NAMES[0]);
}
});
-watch([() => taskCategoryPageState.visibleStatusForm, () => taskCategoryPageGetters.targetStatusOption], async ([visible, target], [prevVisible]) => {
+watch([() => taskCategoryPageState.visibleStatusForm, () => taskCategoryPageStore.getters.targetStatusOption], async ([visible, target], [prevVisible]) => {
if (!visible) {
if (!prevVisible) return; // prevent initial call
await nextTick(); // wait for closing animation
@@ -112,7 +168,7 @@ watch([() => taskCategoryPageState.visibleStatusForm, () => taskCategoryPageGett
-
@@ -131,7 +187,7 @@ watch([() => taskCategoryPageState.visibleStatusForm, () => taskCategoryPageGett
/>
-
+import { ref, computed } from 'vue';
+
+import { cloneDeep } from 'lodash';
+
+import { PButtonModal } from '@cloudforet/mirinae';
+
+import type { TaskStatusOption, TaskStatusOptions, TaskStatusType } from '@/schema/opsflow/task/type';
+
+import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
+
+import ErrorHandler from '@/common/composables/error/errorHandler';
+
+import { useTaskCategoryPageStore } from '@/services/ops-flow/stores/admin/task-category-page-store';
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
+
+const taskCategoryPageStore = useTaskCategoryPageStore();
+const taskCategoryStore = useTaskCategoryStore();
+
+const loading = ref(false);
+const name = computed(() => taskCategoryPageStore.getters.targetStatusOption?.data?.name ?? '');
+
+const setAsDefaultStatus = async (categoryId: string, allStatusOptions: TaskStatusOptions, targetStatusOption: {
+ type: TaskStatusType;
+ data: TaskStatusOption;
+ }) => {
+ try {
+ const newStatusOptions = cloneDeep(allStatusOptions);
+ const { type, data } = targetStatusOption;
+ const prevDefault = newStatusOptions[type].find((p) => p.is_default);
+ if (prevDefault) prevDefault.is_default = false;
+ const newDefault = newStatusOptions[type].find((p) => p.status_id === data.status_id);
+ if (!newDefault) throw new Error('Status not found');
+ newDefault.is_default = true;
+
+ await taskCategoryStore.update({
+ category_id: categoryId,
+ status_options: newStatusOptions,
+ force: true,
+ });
+ showSuccessMessage('Task status set as default successfully', '');
+ } catch (e) {
+ ErrorHandler.handleRequestError(e, 'Failed to set task status as default');
+ }
+};
+const handleConfirm = async () => {
+ loading.value = true;
+ try {
+ if (!taskCategoryPageStore.state.currentCategoryId) {
+ throw new Error('Category ID is required');
+ }
+ if (!taskCategoryPageStore.getters.targetStatusOption) {
+ throw new Error('[Console Error] Cannot set default status without a target status');
+ }
+ await setAsDefaultStatus(taskCategoryPageStore.state.currentCategoryId, taskCategoryPageStore.getters.statusOptions, taskCategoryPageStore.getters.targetStatusOption);
+ taskCategoryPageStore.closeSetDefaultStatusModal();
+ } catch (e) {
+ ErrorHandler.handleError(e);
+ } finally {
+ loading.value = false;
+ }
+};
+const handleCloseOrCancel = () => {
+ taskCategoryPageStore.closeSetDefaultStatusModal();
+};
+const handleClosed = () => {
+ taskCategoryPageStore.resetTargetStatus();
+};
+
+
+
+
+
+ Set {{ name }} as the default status.
+
+
+ This will make it the primary status for related operations.
+
+
+
diff --git a/apps/web/src/services/ops-flow/components/TaskStatusTree.vue b/apps/web/src/services/ops-flow/components/TaskStatusTree.vue
index 7f17438002..04d7e60a0d 100644
--- a/apps/web/src/services/ops-flow/components/TaskStatusTree.vue
+++ b/apps/web/src/services/ops-flow/components/TaskStatusTree.vue
@@ -3,12 +3,16 @@ import { computed } from 'vue';
import type { TaskStatusOption, TaskStatusType } from '@/schema/opsflow/task/type';
+import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
+
+import ErrorHandler from '@/common/composables/error/errorHandler';
+
import TaskStatusList from '@/services/ops-flow/components/TaskStatusList.vue';
import { useTaskCategoryPageStore } from '@/services/ops-flow/stores/admin/task-category-page-store';
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
const taskCategoryPageStore = useTaskCategoryPageStore();
-const taskCategoryPageGetters = taskCategoryPageStore.getters;
-const taskCategoryStore = taskCategoryPageStore.taskCategoryStore;
+const taskCategoryStore = useTaskCategoryStore();
const taskStatusTree = computed<{
key: TaskStatusType,
@@ -19,15 +23,22 @@ const taskStatusTree = computed<{
{ key: 'COMPLETED', name: 'Completed' },
]);
-const handleUpdateItems = (statusType: TaskStatusType, items: TaskStatusOption[]) => {
- if (!taskCategoryPageStore.state.currentCategoryId) return;
- taskCategoryStore.update({
- category_id: taskCategoryPageStore.state.currentCategoryId,
- status_options: {
- ...taskCategoryPageGetters.statusOptions,
- [statusType]: items,
- },
- });
+const handleUpdateItems = async (statusType: TaskStatusType, items: TaskStatusOption[]) => {
+ try {
+ if (!taskCategoryPageStore.state.currentCategoryId) {
+ throw new Error('Category ID is required');
+ }
+ await taskCategoryStore.update({
+ category_id: taskCategoryPageStore.state.currentCategoryId,
+ status_options: {
+ ...taskCategoryPageStore.getters.statusOptions,
+ [statusType]: items,
+ },
+ });
+ showSuccessMessage('Task status options updated successfully', '');
+ } catch (e) {
+ ErrorHandler.handleRequestError(e, 'Failed to update task status options');
+ }
};
@@ -37,7 +48,7 @@ const handleUpdateItems = (statusType: TaskStatusType, items: TaskStatusOption[]
diff --git a/apps/web/src/services/ops-flow/components/TaskTypeDeleteModal.vue b/apps/web/src/services/ops-flow/components/TaskTypeDeleteModal.vue
new file mode 100644
index 0000000000..cf038823b2
--- /dev/null
+++ b/apps/web/src/services/ops-flow/components/TaskTypeDeleteModal.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/components/TaskTypeForm.vue b/apps/web/src/services/ops-flow/components/TaskTypeForm.vue
index a249ce14cc..11e27f1aaa 100644
--- a/apps/web/src/services/ops-flow/components/TaskTypeForm.vue
+++ b/apps/web/src/services/ops-flow/components/TaskTypeForm.vue
@@ -1,20 +1,24 @@
-
+
+
+
+ Board
+
+
+
+
+ Create Ticket
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/pages/TaskCreatePage.vue b/apps/web/src/services/ops-flow/pages/TaskCreatePage.vue
index 033f5317a0..9ff2b0ddd8 100644
--- a/apps/web/src/services/ops-flow/pages/TaskCreatePage.vue
+++ b/apps/web/src/services/ops-flow/pages/TaskCreatePage.vue
@@ -1,8 +1,152 @@
+
+
-
+
+
+
+
+
+
+ {{ headerTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue b/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
index d1f461941e..58d9bb0566 100644
--- a/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
+++ b/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
@@ -1,7 +1,193 @@
+
+
-
+
+
+
+
+
+
+ {{ headerTitle }}
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPage.vue b/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPage.vue
index bc72a30161..990852d20a 100644
--- a/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPage.vue
+++ b/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPage.vue
@@ -20,7 +20,7 @@ export default defineComponent({
/* eslint-disable import/first */
// eslint-disable-next-line import/no-duplicates,import/order
// eslint-disable-next-line import/no-duplicates
-import { computed, onBeforeMount } from 'vue';
+import { computed, onBeforeMount, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router/composables';
import { PHeading, PTab, PSkeleton } from '@cloudforet/mirinae';
@@ -33,9 +33,12 @@ import { useGoBack } from '@/common/composables/go-back';
import TaskStatusDeleteModal from '@/services/ops-flow/components/TaskStatusDeleteModal.vue';
import TaskStatusForm from '@/services/ops-flow/components/TaskStatusForm.vue';
+import TaskStatusSetDefaultModal from '@/services/ops-flow/components/TaskStatusSetDefaultModal.vue';
+import TaskTypeDeleteModal from '@/services/ops-flow/components/TaskTypeDeleteModal.vue';
import TaskTypeForm from '@/services/ops-flow/components/TaskTypeForm.vue';
import { OPS_FLOW_ROUTE } from '@/services/ops-flow/routes/route-constant';
import { useTaskCategoryPageStore } from '@/services/ops-flow/stores/admin/task-category-page-store';
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
const props = defineProps<{
taskCategoryId: string;
@@ -44,13 +47,12 @@ const router = useRouter();
const route = useRoute();
const taskCategoryPageStore = useTaskCategoryPageStore();
-const taskCategoryPageGetters = taskCategoryPageStore.getters;
-const taskCategoryStore = taskCategoryPageStore.taskCategoryStore;
+const taskCategoryStore = useTaskCategoryStore();
/* header and back button */
-const loading = computed(() => taskCategoryStore.state.loading);
-const headerTitle = computed(() => taskCategoryPageGetters.currentCategory?.name ?? 'No Category');
+const loading = computed(() => taskCategoryStore.getters.loading);
+const headerTitle = computed(() => taskCategoryPageStore.getters.currentCategory?.name ?? 'No Category');
const {
setPathFrom,
handleClickBackButton,
@@ -97,6 +99,9 @@ const handleUpdateActiveTab = (tab: string) => {
onBeforeMount(() => {
taskCategoryPageStore.setCurrentCategoryId(props.taskCategoryId);
});
+onUnmounted(() => {
+ taskCategoryPageStore.$dispose();
+});
/* expose */
defineExpose({ setPathFrom });
@@ -122,9 +127,14 @@ defineExpose({ setPathFrom });
>
-
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPageTaskTypeTab.vue b/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPageTaskTypeTab.vue
index 148fe1aa23..5c7579f6b9 100644
--- a/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPageTaskTypeTab.vue
+++ b/apps/web/src/services/ops-flow/pages/admin/AdminTaskCategoryDetailPageTaskTypeTab.vue
@@ -10,7 +10,9 @@ import ActionMenuButton from '@/common/components/buttons/ActionMenuButton.vue';
import { useTaskCategoryPageStore } from '@/services/ops-flow/stores/admin/task-category-page-store';
+
const taskCategoryPageStore = useTaskCategoryPageStore();
+const taskCategoryPageGetters = taskCategoryPageStore.getters;
const taskTypeFields = computed(() => [
{
@@ -39,7 +41,9 @@ const taskTypeFields = computed(() => [
-
+
(() => [
티켓 양식으로 티켓에 포함된 필드와 데이터를 결정합니다. 여러 티켓 양식을 만들 수 있습니다.
예를 들어 서비스별로 서로 다른 양식을 만들 수 있습니다. 그런 경우에는 최종 사용자가 적절한 양식을 선택하여 요청을 제출합니다.
-
diff --git a/apps/web/src/services/ops-flow/routes/routes.ts b/apps/web/src/services/ops-flow/routes/routes.ts
index ba3b319466..a1ece4a86c 100644
--- a/apps/web/src/services/ops-flow/routes/routes.ts
+++ b/apps/web/src/services/ops-flow/routes/routes.ts
@@ -40,21 +40,20 @@ const opsFlowRoutes: RouteConfig = {
meta: {
menuId: MENU_ID.BOARD,
translationId: MENU_INFO_MAP[MENU_ID.BOARD].translationId,
+ lsbVisible: true,
},
component: BoardPage as any,
- children: [
- {
- path: ':taskId',
- name: OPS_FLOW_ROUTE.BOARD.TASK_DETAIL._NAME,
- props: true,
- component: TaskDetailPage as any,
- },
- {
- path: 'task-create',
- name: OPS_FLOW_ROUTE.BOARD.TASK_CREATE._NAME,
- component: TaskCreatePage as any,
- },
- ],
+ },
+ {
+ path: 'board/task-create',
+ name: OPS_FLOW_ROUTE.BOARD.TASK_CREATE._NAME,
+ component: TaskCreatePage as any,
+ },
+ {
+ path: 'board/:taskId',
+ name: OPS_FLOW_ROUTE.BOARD.TASK_DETAIL._NAME,
+ props: true,
+ component: TaskDetailPage as any,
},
],
};
diff --git a/apps/web/src/services/ops-flow/stores/admin/package-store.ts b/apps/web/src/services/ops-flow/stores/admin/package-store.ts
index 08bda782dc..fcedaedd8f 100644
--- a/apps/web/src/services/ops-flow/stores/admin/package-store.ts
+++ b/apps/web/src/services/ops-flow/stores/admin/package-store.ts
@@ -1,6 +1,5 @@
import { asyncComputed } from '@vueuse/core';
-import type { Ref, UnwrapRef } from 'vue';
-import { reactive } from 'vue';
+import { watch, reactive, computed } from 'vue';
import { defineStore } from 'pinia';
@@ -15,6 +14,8 @@ import type { PackageSetDefaultParameters } from '@/schema/identity/package/api-
import type { PackageUpdateParameters } from '@/schema/identity/package/api-verbs/update';
import type { PackageModel } from '@/schema/identity/package/model';
+import { useAppContextStore } from '@/store/app-context/app-context-store';
+
import ErrorHandler from '@/common/composables/error/errorHandler';
@@ -22,23 +23,22 @@ interface UsePackageStoreState {
loading: boolean;
items?: PackageModel[];
}
-interface UsePackageStoreGetters {
- packages: Ref>
-}
export const usePackageStore = defineStore('package', () => {
const state = reactive({
loading: false,
items: undefined,
});
- const getters = reactive({
+ const getters = {
+ loading: computed(() => state.loading),
packages: asyncComputed(async () => {
if (state.items === undefined) {
await actions.list();
}
return state.items ?? [];
}, [], { lazy: true }),
- }) as UnwrapRef;
+ defaultPackage: computed(() => getters.packages.find((p) => p.is_default)),
+ };
const fetchList = getCancellableFetcher>(SpaceConnector.clientV2.identity.package.list);
const actions = {
@@ -47,7 +47,7 @@ export const usePackageStore = defineStore('package', () => {
try {
const result = await fetchList({});
if (result.status === 'succeed') {
- state.items = result.response.results;
+ state.items = result.response.results ?? [];
}
} catch (e) {
ErrorHandler.handleError(e);
@@ -57,28 +57,42 @@ export const usePackageStore = defineStore('package', () => {
},
async create(param: PackageCreateParameters) {
const response = await SpaceConnector.clientV2.identity.package.create(param);
+ state.items?.push(response);
return response;
},
async update(param: PackageUpdateParameters) {
const response = await SpaceConnector.clientV2.identity.package.update(param);
+ state.items = state.items?.map((item) => (item.package_id === response.package_id ? response : item));
return response;
},
async setDefaultPackage(packageId: string) {
const prevDefaultPackage = getters.packages.find((p) => p.is_default);
if (prevDefaultPackage?.package_id === packageId) return prevDefaultPackage;
- const response = await SpaceConnector.clientV2.identity.package.setDefaultPackage({
+ const response = await SpaceConnector.clientV2.identity.package.setDefault({
package_id: packageId,
});
+ const prev = state.items?.find((p) => p.is_default);
+ if (prev) prev.is_default = false;
+ const current = state.items?.find((p) => p.package_id === packageId);
+ if (current) current.is_default = true;
return response;
},
async delete(packageId: string) {
await SpaceConnector.clientV2.identity.package.delete({
package_id: packageId,
});
+ state.items = state.items?.filter((item) => item.package_id !== packageId);
},
};
+ const disposeSelf = () => {
+ const store = usePackageStore();
+ store.$dispose();
+ };
+ const appContextStore = useAppContextStore();
+ watch(() => appContextStore.getters.isAdminMode, () => {
+ disposeSelf();
+ });
return {
- state,
getters,
...actions,
};
diff --git a/apps/web/src/services/ops-flow/stores/admin/task-category-page-store.ts b/apps/web/src/services/ops-flow/stores/admin/task-category-page-store.ts
index f5cf4c33ad..b7d8fbd519 100644
--- a/apps/web/src/services/ops-flow/stores/admin/task-category-page-store.ts
+++ b/apps/web/src/services/ops-flow/stores/admin/task-category-page-store.ts
@@ -1,5 +1,7 @@
-import type { ComputedRef, UnwrapRef } from 'vue';
-import { reactive, computed } from 'vue';
+import { asyncComputed } from '@vueuse/core';
+import {
+ reactive, computed,
+} from 'vue';
import { defineStore } from 'pinia';
@@ -7,18 +9,21 @@ import type { TaskCategoryModel } from '@/schema/opsflow/task-category/model';
import type { TaskTypeModel } from '@/schema/opsflow/task-type/model';
import type { TaskStatusOption, TaskStatusOptions, TaskStatusType } from '@/schema/opsflow/task/type';
+import ErrorHandler from '@/common/composables/error/errorHandler';
+
import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
-import { useTaskTypeStore } from '@/services/ops-flow/stores/admin/task-type-store';
+import { useTaskTypeStore } from '@/services/ops-flow/stores/task-type-store';
interface UseTaskCategoryPageStoreState {
currentCategoryId?: string;
// status
visibleStatusForm: boolean;
targetStatus: {
+ statusId: string;
type: TaskStatusType;
- index: number;
}|undefined;
visibleStatusDeleteModal: boolean;
+ visibleSetDefaultStatusModal: boolean;
// task type
visibleTaskTypeForm: boolean;
targetTaskTypeId?: string;
@@ -26,19 +31,19 @@ interface UseTaskCategoryPageStoreState {
}
interface UseTaskCategoryPageStoreGetters {
- currentCategory: ComputedRef;
+ currentCategory: TaskCategoryModel|undefined;
// status
- statusOptions: ComputedRef;
- targetStatusOption: ComputedRef<{
- type: TaskStatusType;
- data: TaskStatusOption;
- }|undefined>;
+ statusOptions: TaskStatusOptions;
+ targetStatusOption: {
+ type: TaskStatusType;
+ data: TaskStatusOption;
+ }|undefined;
// task type
- taskTypes: ComputedRef;
- targetTaskType: ComputedRef;
+ taskTypes: TaskTypeModel[]|undefined;
+ targetTaskType: TaskTypeModel|undefined;
}
-export const useTaskCategoryPageStore = defineStore('task-management-category-page', () => {
+export const useTaskCategoryPageStore = defineStore('task-category-page', () => {
const taskCategoryStore = useTaskCategoryStore();
const taskTypeStore = useTaskTypeStore();
const state = reactive({
@@ -47,13 +52,13 @@ export const useTaskCategoryPageStore = defineStore('task-management-category-pa
visibleStatusForm: false,
targetStatus: undefined,
visibleStatusDeleteModal: false,
+ visibleSetDefaultStatusModal: false,
// task type
visibleTaskTypeForm: false,
targetTaskTypeId: undefined,
visibleTaskTypeDeleteModal: false,
});
-
- const getters = reactive({
+ const getters: UseTaskCategoryPageStoreGetters = {
currentCategory: computed(() => taskCategoryStore.getters.taskCategories.find((c) => c.category_id === state.currentCategoryId)),
// status
statusOptions: computed(() => {
@@ -72,26 +77,27 @@ export const useTaskCategoryPageStore = defineStore('task-management-category-pa
data: TaskStatusOption;
}|undefined>(() => {
if (!state.targetStatus) return undefined;
- const { index, type } = state.targetStatus;
+ const { statusId, type } = state.targetStatus;
const statusOptions = getters.statusOptions;
if (!statusOptions) return undefined;
+ const data = statusOptions[type].find((status) => status.status_id === statusId);
+ if (!data) return undefined;
return {
type,
- data: statusOptions[type][index],
+ data,
};
}),
// task type
- taskTypes: computed(() => {
+ taskTypes: asyncComputed(async () => {
if (!state.currentCategoryId) return undefined;
- const allTaskTypes = taskTypeStore.getters.taskTypes;
- return allTaskTypes.filter((taskType) => taskType.category_id === state.currentCategoryId);
- }),
+ if (!taskTypeStore.state.itemsByCategoryId[state.currentCategoryId]) await taskTypeStore.listByCategoryId(state.currentCategoryId);
+ return taskTypeStore.state.itemsByCategoryId[state.currentCategoryId];
+ }, undefined, { lazy: true, onError: ErrorHandler.handleError }),
targetTaskType: computed(() => {
if (!state.targetTaskTypeId) return undefined;
- return taskTypeStore.getters.taskTypes.find((taskType) => taskType.task_type_id === state.targetTaskTypeId);
+ return getters.taskTypes?.find((taskType) => taskType.task_type_id === state.targetTaskTypeId);
}),
- }) as UnwrapRef;
-
+ } as unknown as UseTaskCategoryPageStoreGetters;
const actions = {
setCurrentCategoryId(categoryId: string) {
state.currentCategoryId = categoryId;
@@ -101,29 +107,53 @@ export const useTaskCategoryPageStore = defineStore('task-management-category-pa
state.targetStatus = undefined;
state.visibleStatusForm = true;
},
- openEditStatusForm(index: number, statusType: TaskStatusType) {
+ openEditStatusForm(statusId: string, statusType: TaskStatusType) {
state.targetStatus = {
- index,
+ statusId,
type: statusType,
};
state.visibleStatusForm = true;
},
closeStatusForm() {
state.visibleStatusForm = false;
- state.targetStatus = undefined;
+ // do not reset targetStatus here and handle it after the modal is closed
},
- openDeleteStatusModal(index: number, statusType: TaskStatusType) {
+ openDeleteStatusModal(statusId: string, statusType: TaskStatusType) {
state.targetStatus = {
- index,
+ statusId,
type: statusType,
};
state.visibleStatusDeleteModal = true;
},
closeDeleteStatusModal() {
state.visibleStatusDeleteModal = false;
+ // do not reset targetStatus here and handle it after the modal is closed
+ },
+ openSetDefaultStatusModal(statusId: string, statusType: TaskStatusType) {
+ state.targetStatus = {
+ statusId,
+ type: statusType,
+ };
+ state.visibleSetDefaultStatusModal = true;
+ },
+ closeSetDefaultStatusModal() {
+ state.visibleSetDefaultStatusModal = false;
+ // do not reset targetStatus here and handle it after the modal is closed
+ },
+ resetTargetStatus() {
state.targetStatus = undefined;
},
// task type
+ async listTaskTypes() {
+ try {
+ if (!state.currentCategoryId) throw new Error('currentCategoryId is not set');
+ const taskTypes = await taskTypeStore.listByCategoryId(state.currentCategoryId, true);
+ return taskTypes;
+ } catch (e) {
+ ErrorHandler.handleError(e);
+ return [];
+ }
+ },
openAddTaskTypeForm() {
state.targetTaskTypeId = undefined;
state.visibleTaskTypeForm = true;
@@ -134,7 +164,7 @@ export const useTaskCategoryPageStore = defineStore('task-management-category-pa
},
closeTaskTypeForm() {
state.visibleTaskTypeForm = false;
- state.targetTaskTypeId = undefined;
+ // do not reset targetTaskTypeId here and handle it after the modal is closed
},
openDeleteTaskTypeModal(taskTypeId: string) {
state.targetTaskTypeId = taskTypeId;
@@ -142,6 +172,9 @@ export const useTaskCategoryPageStore = defineStore('task-management-category-pa
},
closeDeleteTaskTypeModal() {
state.visibleTaskTypeDeleteModal = false;
+ // do not reset targetTaskTypeId here and handle it after the modal is closed
+ },
+ resetTargetTaskTypeId() {
state.targetTaskTypeId = undefined;
},
};
@@ -149,6 +182,5 @@ export const useTaskCategoryPageStore = defineStore('task-management-category-pa
state,
getters,
...actions,
- taskCategoryStore,
};
});
diff --git a/apps/web/src/services/ops-flow/stores/admin/task-category-store.ts b/apps/web/src/services/ops-flow/stores/admin/task-category-store.ts
index e8fd80cf34..1daece9994 100644
--- a/apps/web/src/services/ops-flow/stores/admin/task-category-store.ts
+++ b/apps/web/src/services/ops-flow/stores/admin/task-category-store.ts
@@ -1,6 +1,4 @@
-import { asyncComputed } from '@vueuse/core';
-import type { Ref } from 'vue';
-import { reactive } from 'vue';
+import { reactive, computed, watch } from 'vue';
import { defineStore } from 'pinia';
@@ -9,11 +7,14 @@ import { getCancellableFetcher } from '@cloudforet/core-lib/space-connector/canc
import type { ListResponse } from '@/schema/_common/api-verbs/list';
import type { TaskCategoryCreateParameters } from '@/schema/opsflow/task-category/api-verbs/create';
+import type { TaskCategoryDeleteParameters } from '@/schema/opsflow/task-category/api-verbs/delete';
import type { TaskCategoryGetParameters } from '@/schema/opsflow/task-category/api-verbs/get';
import type { TaskCategoryListParameters } from '@/schema/opsflow/task-category/api-verbs/list';
import type { TaskCategoryUpdateParameters } from '@/schema/opsflow/task-category/api-verbs/update';
import type { TaskCategoryModel } from '@/schema/opsflow/task-category/model';
+import { useAppContextStore } from '@/store/app-context/app-context-store';
+
import ErrorHandler from '@/common/composables/error/errorHandler';
@@ -21,9 +22,6 @@ interface UseTaskCategoryStoreState {
loading: boolean;
items?: TaskCategoryModel[];
}
-interface UseTaskCategoryStoreGetters {
- taskCategories: Ref>
-}
const DEFAULT_STATUS_OPTIONS: TaskCategoryCreateParameters['status_options'] = {
TODO: [
{ name: 'To Do', color: 'blue200', is_default: true },
@@ -39,56 +37,84 @@ export const useTaskCategoryStore = defineStore('task-category', () => {
const state = reactive({
loading: false,
items: undefined,
- }) as UseTaskCategoryStoreState;
+ });
- const getters = reactive({
- taskCategories: asyncComputed(async () => {
+ const getters = {
+ loading: computed(() => state.loading),
+ taskCategories: computed(() => {
if (state.items === undefined) {
- await actions.list();
+ actions.list();
}
return state.items ?? [];
- }, [], { lazy: true }),
- });
+ }),
+ };
- const fetchList = getCancellableFetcher>(SpaceConnector.client.opsFlow.taskCategory.list);
+ const fetchList = getCancellableFetcher>(SpaceConnector.clientV2.opsflow.taskCategory.list);
const actions = {
- async list() {
- state.loading = true;
+ async list(paramsOrForce?: TaskCategoryListParameters|true): Promise {
+ const force = paramsOrForce === true;
+ if (!force && state.items) return state.items;
+ const params = force ? undefined : paramsOrForce;
try {
- const result = await fetchList({});
+ state.loading = true;
+ const result = await fetchList(params ?? {});
if (result.status === 'succeed') {
- state.items = result.response.results;
+ state.loading = false;
+ if (params) {
+ return result.response.results ?? [];
+ }
+ state.items = result.response.results ?? [];
+ return result.response.results ?? [];
}
+ return undefined;
} catch (e) {
ErrorHandler.handleError(e);
- } finally {
state.loading = false;
+ return undefined;
}
},
async create(params: Omit) {
- const response = await SpaceConnector.client.opsFlow.taskCategory.create({
+ const response = await SpaceConnector.clientV2.opsflow.taskCategory.create({
...params,
status_options: DEFAULT_STATUS_OPTIONS,
});
+ state.items?.push(response);
return response;
},
async update(params: TaskCategoryUpdateParameters) {
- const response = await SpaceConnector.client.opsFlow.taskCategory.update(params);
+ const response = await SpaceConnector.clientV2.opsflow.taskCategory.update(params);
+ const item = state.items?.find((c) => c.category_id === response.category_id);
+ if (item) {
+ Object.assign(item, response);
+ }
return response;
},
async get(categoryId: string) {
- const response = await SpaceConnector.client.opsFlow.taskCategory.update({
+ const category = state.items?.find((item) => item.category_id === categoryId);
+ if (category) return category;
+ const response = await SpaceConnector.clientV2.opsflow.taskCategory.get({
category_id: categoryId,
});
return response;
},
async delete(categoryId: string) {
- await SpaceConnector.client.opsFlow.taskCategory.delete({
+ await SpaceConnector.clientV2.opsflow.taskCategory.delete({
category_id: categoryId,
});
+ state.items = state.items?.filter((item) => item.category_id !== categoryId);
},
};
+
+ const disposeSelf = () => {
+ const store = useTaskCategoryStore();
+ store.$reset();
+ store.$dispose();
+ };
+ const appContextStore = useAppContextStore();
+ watch([() => appContextStore.getters.isAdminMode, () => appContextStore.getters.workspaceId], () => {
+ disposeSelf();
+ });
return {
state,
getters,
diff --git a/apps/web/src/services/ops-flow/stores/admin/task-management-page-store.ts b/apps/web/src/services/ops-flow/stores/admin/task-management-page-store.ts
index b758e4d568..65ad0408f5 100644
--- a/apps/web/src/services/ops-flow/stores/admin/task-management-page-store.ts
+++ b/apps/web/src/services/ops-flow/stores/admin/task-management-page-store.ts
@@ -1,4 +1,4 @@
-import type { ComputedRef, DeepReadonly } from 'vue';
+import type { DeepReadonly } from 'vue';
import { reactive, computed } from 'vue';
import { defineStore } from 'pinia';
@@ -11,7 +11,6 @@ import { useWorkspaceReferenceStore } from '@/store/reference/workspace-referenc
import { usePackageStore } from '@/services/ops-flow/stores/admin/package-store';
import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
-// import { useTaskTypeStore } from '@/services/ops-flow/stores/task-type-store';
interface UseTaskManagementPageStoreState {
// support package
@@ -25,19 +24,10 @@ interface UseTaskManagementPageStoreState {
visibleDeleteCategoryModal: boolean;
}
-interface UseTaskManagementPageStoreGetters {
- targetPackage: ComputedRef;
- associatedCategoriesToPackage: ComputedRef>;
- associatedWorkspacesToPackage: ComputedRef;
- targetCategory: ComputedRef|undefined>;
- defaultPackage: ComputedRef;
-}
-
export const useTaskManagementPageStore = defineStore('task-management-page', () => {
const packageStore = usePackageStore();
const taskCategoryStore = useTaskCategoryStore();
const workspaceReferenceStore = useWorkspaceReferenceStore();
- // const taskTypeStore = useTaskTypeStore();
const state = reactive({
// support package
visiblePackageForm: false,
@@ -49,7 +39,7 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
targetCategoryId: undefined,
visibleDeleteCategoryModal: false,
});
- const getters = reactive({
+ const getters = {
targetPackage: computed(() => packageStore.getters.packages.find((p) => p.package_id === state.targetPackageId)),
associatedCategoriesToPackage: computed>(() => taskCategoryStore.getters.taskCategories.filter((c) => c.package_id === state.targetPackageId)),
associatedWorkspacesToPackage: computed(() => {
@@ -60,7 +50,7 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
}),
targetCategory: computed|undefined>(() => taskCategoryStore.getters.taskCategories.find((c) => c.category_id === state.targetCategoryId)),
defaultPackage: computed(() => packageStore.getters.packages.find((p) => p.is_default)),
- });
+ };
const actions = {
// support package
openAddPackageForm() {
@@ -73,7 +63,7 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
},
closePackageForm() {
state.visiblePackageForm = false;
- state.targetPackageId = undefined;
+ // do not reset targetPackageId here and handle it after the modal is closed
},
openDeletePackageModal(packageId: string) {
state.targetPackageId = packageId;
@@ -81,7 +71,7 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
},
closeDeletePackageModal() {
state.visibleDeletePackageModal = false;
- state.targetPackageId = undefined;
+ // do not reset targetPackageId here and handle it after the modal is closed
},
openSetDefaultPackageModal(packageId: string) {
state.targetPackageId = packageId;
@@ -89,6 +79,9 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
},
closeSetDefaultPackageModal() {
state.visibleSetDefaultPackageModal = false;
+ // do not reset targetPackageId here and handle it after the modal is closed
+ },
+ resetTargetPackageId() {
state.targetPackageId = undefined;
},
// category
@@ -102,7 +95,7 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
},
closeCategoryForm() {
state.visibleCategoryForm = false;
- state.targetCategoryId = undefined;
+ // do not reset targetCategoryId here and handle it after the modal is closed
},
openDeleteCategoryModal(categoryId: string) {
state.targetCategoryId = categoryId;
@@ -110,12 +103,13 @@ export const useTaskManagementPageStore = defineStore('task-management-page', ()
},
closeDeleteCategoryModal() {
state.visibleDeleteCategoryModal = false;
+ // do not reset targetCategoryId here and handle it after the modal is closed
+ },
+ resetTargetCategoryId() {
state.targetCategoryId = undefined;
},
};
return {
- packageStore,
- taskCategoryStore,
state,
getters,
...actions,
diff --git a/apps/web/src/services/ops-flow/stores/admin/task-type-store.ts b/apps/web/src/services/ops-flow/stores/admin/task-type-store.ts
deleted file mode 100644
index e961e337bc..0000000000
--- a/apps/web/src/services/ops-flow/stores/admin/task-type-store.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { asyncComputed } from '@vueuse/core';
-import type { Ref, UnwrapRef } from 'vue';
-import { reactive } from 'vue';
-
-import { defineStore } from 'pinia';
-
-import type { TaskTypeModel } from '@/schema/opsflow/task-type/model';
-
-import getRandomId from '@/lib/random-id-generator';
-
-interface UseTaskTypeStoreState {
- items?: TaskTypeModel[];
-}
-
-interface UseTaskTypeStoreGetters {
- taskTypes: Ref>
-}
-
-export const useTaskTypeStore = defineStore('task-type', () => {
- const state = reactive({
- items: undefined,
- }) as UseTaskTypeStoreState;
-
- const getters = reactive({
- taskTypes: asyncComputed(async () => {
- if (!state.items) {
- await actions.list();
- }
- return state.items ?? [];
- }, [], { lazy: true }),
- }) as UnwrapRef;
-
- const actions = {
- async list() {
- return new Promise((resolve) => {
- setTimeout(() => {
- state.items = [
- {
- task_type_id: 'task_type_1',
- name: 'Account',
- description: '계정과 관련된 문의사항',
- assignee_pool: ['wanjin@mz.co.kr'],
- fields: [
- {
- field_id: getRandomId(),
- name: '제목',
- field_type: 'TEXT',
- is_required: true,
- is_primary: true,
- },
- {
- field_id: getRandomId(),
- name: '설명',
- field_type: 'PARAGRAPH',
- is_required: false,
- is_primary: true,
- },
- ],
- category_id: 'category_1',
- tags: {},
- domain_id: '1',
- created_at: '2021-09-01T00:00:00',
- updated_at: '2021-09-01T00:00:00',
- },
- {
- task_type_id: 'task_type_2',
- name: 'Invoice',
- description: '청구서와 관련된 문의사항',
- assignee_pool: ['bokjang@mz.co.kr'],
- fields: [
- {
- field_id: getRandomId(),
- name: '제목',
- field_type: 'TEXT',
- is_required: true,
- is_primary: true,
- },
- {
- field_id: getRandomId(),
- name: '설명',
- field_type: 'PARAGRAPH',
- is_required: true,
- is_primary: true,
- },
- {
- field_id: getRandomId(),
- name: '서비스',
- field_type: 'DROPDOWN',
- options: ['서비스1', '서비스2', '서비스3'],
- is_required: false,
- },
- {
- field_id: getRandomId(),
- name: '연결된 자산',
- field_type: 'ASSET',
- is_required: false,
- },
- {
- field_id: getRandomId(),
- name: '담당자',
- field_type: 'USER',
- is_required: false,
- },
- ],
- category_id: 'category_1',
- tags: {},
- domain_id: '1',
- created_at: '2021-09-01T00:00:00',
- updated_at: '2021-09-01T00:00:00',
- },
- {
- task_type_id: 'task_type_3',
- name: 'Billing',
- description: '빌링과 관련된 문의사항',
- fields: [
- {
- field_id: getRandomId(),
- name: '제목',
- field_type: 'TEXT',
- is_required: true,
- is_primary: true,
- },
- {
- field_id: getRandomId(),
- name: '설명',
- field_type: 'PARAGRAPH',
- is_required: false,
- is_primary: false,
- },
- ],
- category_id: 'category_2',
- tags: {},
- domain_id: '1',
- created_at: '2021-09-01T00:00:00',
- updated_at: '2021-09-01T00:00:00',
- },
- ];
- resolve(state.items);
- }, 1000);
- });
- },
- };
- return {
- state,
- getters,
- ...actions,
- };
-});
diff --git a/apps/web/src/services/ops-flow/stores/board-page-store.ts b/apps/web/src/services/ops-flow/stores/board-page-store.ts
new file mode 100644
index 0000000000..35c3708ac7
--- /dev/null
+++ b/apps/web/src/services/ops-flow/stores/board-page-store.ts
@@ -0,0 +1,32 @@
+import type { ComputedRef } from 'vue';
+import { reactive, computed } from 'vue';
+
+import { defineStore } from 'pinia';
+
+import type { TaskCategoryModel } from '@/schema/opsflow/task-category/model';
+
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
+
+interface UseBoardPageStoreGetters {
+ currentCategory: ComputedRef;
+}
+export const useBoardPageStore = defineStore('board-page', () => {
+ const taskCategoryStore = useTaskCategoryStore();
+
+ const state = reactive({
+ currentCategoryId: '',
+ });
+ const getters: UseBoardPageStoreGetters = {
+ currentCategory: computed(() => taskCategoryStore.getters.taskCategories.find((c) => c.category_id === state.currentCategoryId)),
+ };
+ const actions = {
+ setCurrentCategoryId(categoryId: string) {
+ state.currentCategoryId = categoryId;
+ },
+ };
+ return {
+ state,
+ getters,
+ ...actions,
+ };
+});
diff --git a/apps/web/src/services/ops-flow/stores/task-content-form-store.ts b/apps/web/src/services/ops-flow/stores/task-content-form-store.ts
new file mode 100644
index 0000000000..7e63de624e
--- /dev/null
+++ b/apps/web/src/services/ops-flow/stores/task-content-form-store.ts
@@ -0,0 +1,166 @@
+import { reactive, computed } from 'vue';
+
+import { isEmpty } from 'lodash';
+import { defineStore } from 'pinia';
+
+import type { TaskField } from '@/schema/opsflow/_types/task-field-type';
+import type { TaskCategoryModel } from '@/schema/opsflow/task-category/model';
+import type { TaskTypeModel } from '@/schema/opsflow/task-type/model';
+
+import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';
+
+import ErrorHandler from '@/common/composables/error/errorHandler';
+
+import { useTaskCategoryStore } from '@/services/ops-flow/stores/admin/task-category-store';
+import { useTaskStore } from '@/services/ops-flow/stores/task-store';
+import { useTaskTypeStore } from '@/services/ops-flow/stores/task-type-store';
+import {
+ useTaskFieldMetadataStore,
+} from '@/services/ops-flow/task-fields-configuration/stores/use-task-field-metadata-store';
+import type { DefaultTaskFieldId } from '@/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type';
+
+interface UseTaskCreatePageStoreState {
+ // base form
+ currentCategoryId?: string;
+ currentTaskTypeId?: string;
+ statusId?: string;
+ assignee?: string;
+ isBaseFormValid: boolean;
+ // default field form
+ defaultData: Partial>;
+ defaultDataValidationMap: Record;
+ // task type field form
+ data: Record;
+ dataValidationMap: Record;
+ // overall
+ mode: 'create-minimal'|'create'|'edit'|'view'; // create-minimal: create task without status and assignee
+ hasUnsavedChanges: boolean;
+ createTaskLoading: boolean;
+}
+interface UseTaskCreatePageStoreGetters {
+ currentCategory: TaskCategoryModel|undefined;
+ currentTaskType: TaskTypeModel|undefined;
+ currentFields: TaskField[];
+ isDefaultFieldValid: boolean;
+ isFieldValid: boolean;
+ isAllValid: boolean;
+}
+export const useTaskContentFormStore = defineStore('task-content-form', () => {
+ const taskCategoryStore = useTaskCategoryStore();
+ const taskTypeStore = useTaskTypeStore();
+ const taskStore = useTaskStore();
+ const taskFieldMetadataStore = useTaskFieldMetadataStore();
+
+ const state = reactive({
+ // base form
+ currentCategoryId: undefined,
+ currentTaskTypeId: undefined,
+ statusId: undefined,
+ assignee: undefined,
+ isBaseFormValid: false,
+ // default field form
+ defaultData: {},
+ defaultDataValidationMap: {},
+ // task type field form
+ data: {},
+ dataValidationMap: {},
+ // overall
+ mode: 'create',
+ hasUnsavedChanges: false,
+ createTaskLoading: false,
+ });
+ const getters = {
+ // base form
+ currentCategory: computed(() => taskCategoryStore.getters.taskCategories.find((c) => c.category_id === state.currentCategoryId)),
+ currentTaskType: computed(() => {
+ if (!state.currentCategoryId || !state.currentTaskTypeId) return undefined;
+ if (!taskTypeStore.state.itemsByCategoryId[state.currentCategoryId]) {
+ taskTypeStore.listByCategoryId(state.currentCategoryId);
+ }
+ return taskTypeStore.state.itemsByCategoryId[state.currentCategoryId]?.find((t) => t.task_type_id === state.currentTaskTypeId);
+ }),
+ // task type field form
+ currentFields: computed(() => {
+ const taskType = getters.currentTaskType;
+ return taskType ? taskType.fields : [];
+ }),
+ isDefaultFieldValid: computed(() => taskFieldMetadataStore.getters.defaultFields.every((field) => state.defaultDataValidationMap[field.field_id])),
+ isFieldValid: computed(() => getters.currentFields?.every((field) => state.dataValidationMap[field.field_id]) ?? true),
+ // overall
+ isAllValid: computed(() => state.isBaseFormValid && getters.isDefaultFieldValid && getters.isFieldValid),
+ } as unknown as UseTaskCreatePageStoreGetters; // HACK: to avoid type error
+ const actions = {
+ setCurrentCategoryId(categoryId?: string) {
+ if (state.currentCategoryId === categoryId) return;
+ state.currentCategoryId = categoryId;
+ state.currentTaskTypeId = undefined;
+ },
+ setCurrentTaskTypeId(taskTypeId?: string) {
+ if (state.currentTaskTypeId === taskTypeId) return;
+ state.currentTaskTypeId = taskTypeId;
+ state.data = {};
+ state.dataValidationMap = {};
+ },
+ setStatusId(statusId?: string) {
+ state.statusId = statusId;
+ },
+ setAssignee(assignee?: string) {
+ state.assignee = assignee;
+ },
+ setIsBaseFormValid(isValid: boolean) {
+ state.isBaseFormValid = isValid;
+ },
+ // default field form
+ setDefaultFieldData(fieldId: DefaultTaskFieldId, value: any) {
+ state.defaultData[fieldId] = value;
+ state.hasUnsavedChanges = true;
+ },
+ setDefaultFieldValidation(fieldId: DefaultTaskFieldId, isValid: boolean) {
+ state.defaultDataValidationMap = { ...state.defaultDataValidationMap, [fieldId]: isValid };
+ },
+ // task type field form
+ setFieldData(fieldId: string, value: any) {
+ state.data[fieldId] = value;
+ state.hasUnsavedChanges = true;
+ },
+ setFieldValidation(fieldId: string, isValid: boolean) {
+ state.dataValidationMap = { ...state.dataValidationMap, [fieldId]: isValid };
+ },
+ // overall
+ resetFieldsForm() {
+ state.defaultData = {};
+ state.defaultDataValidationMap = {};
+ state.data = {};
+ state.dataValidationMap = {};
+ },
+ setMode(mode: 'create-minimal'|'create'|'edit'|'view') {
+ state.mode = mode;
+ },
+ async createTask() {
+ try {
+ state.createTaskLoading = true;
+ await taskStore.create({
+ task_type_id: state.currentTaskTypeId as string,
+ name: state.defaultData.title,
+ status_id: state.statusId as string,
+ description: state.defaultData.description || undefined,
+ assignee: state.assignee || undefined,
+ data: isEmpty(state.data) ? undefined : state.data,
+ project_id: state.defaultData.project?.[0],
+ });
+ showSuccessMessage('Task created successfully', '');
+ state.createTaskLoading = false;
+ return true;
+ } catch (e) {
+ ErrorHandler.handleRequestError(e, 'Failed to create task');
+ state.createTaskLoading = false;
+ return false;
+ }
+ },
+ };
+ return {
+ state,
+ getters,
+ ...actions,
+ };
+});
diff --git a/apps/web/src/services/ops-flow/stores/task-store.ts b/apps/web/src/services/ops-flow/stores/task-store.ts
new file mode 100644
index 0000000000..ac21d812df
--- /dev/null
+++ b/apps/web/src/services/ops-flow/stores/task-store.ts
@@ -0,0 +1,79 @@
+import { reactive, watch } from 'vue';
+
+import { defineStore } from 'pinia';
+
+import { SpaceConnector } from '@cloudforet/core-lib/space-connector';
+import { getCancellableFetcher } from '@cloudforet/core-lib/space-connector/cancellable-fetcher';
+
+import type { ListResponse } from '@/schema/_common/api-verbs/list';
+import type { TaskCreateParameters } from '@/schema/opsflow/task/api-verbs/create';
+import type { TaskDeleteParameters } from '@/schema/opsflow/task/api-verbs/delete';
+import type { TaskGetParameters } from '@/schema/opsflow/task/api-verbs/get';
+import type { TaskListParameters } from '@/schema/opsflow/task/api-verbs/list';
+import type { TaskUpdateParameters } from '@/schema/opsflow/task/api-verbs/update';
+import type { TaskModel } from '@/schema/opsflow/task/model';
+
+import { useAppContextStore } from '@/store/app-context/app-context-store';
+
+interface UseTaskTypeStoreState {
+ itemsByTaskId: Record;
+}
+
+export const useTaskStore = defineStore('task', () => {
+ const state = reactive({
+ itemsByTaskId: {},
+ });
+
+ const fetchList = getCancellableFetcher>(SpaceConnector.clientV2.opsflow.task.list);
+ const actions = {
+ /*
+ * @return {TaskModel[]|undefined} It returns undefined if the request is canceled.
+ */
+ async list(params: TaskListParameters = {}): Promise {
+ const result = await fetchList(params);
+ if (result.status === 'succeed') {
+ return result.response.results ?? [];
+ }
+ return undefined;
+ },
+ async create(params: TaskCreateParameters) {
+ const response = await SpaceConnector.clientV2.opsflow.task.create(params);
+ const taskId = response.task_type_id;
+ state.itemsByTaskId[taskId] = response;
+ return response;
+ },
+ async update(params: TaskUpdateParameters) {
+ const response = await SpaceConnector.clientV2.opsflow.task.update(params);
+ const taskId = response.task_type_id;
+ state.itemsByTaskId[taskId] = response;
+ return response;
+ },
+ async get(taskId: string) {
+ const response = await SpaceConnector.clientV2.opsflow.task.get({
+ task_id: taskId,
+ });
+ state.itemsByTaskId[taskId] = response;
+ return response;
+ },
+ async delete(taskId: string) {
+ await SpaceConnector.clientV2.opsflow.task.delete({
+ task_id: taskId,
+ });
+ delete state.itemsByTaskId[taskId];
+ },
+ };
+
+ const disposeSelf = () => {
+ const store = useTaskStore();
+ store.$reset();
+ store.$dispose();
+ };
+ const appContextStore = useAppContextStore();
+ watch([() => appContextStore.getters.isAdminMode, () => appContextStore.getters.workspaceId], () => {
+ disposeSelf();
+ });
+ return {
+ state,
+ ...actions,
+ };
+});
diff --git a/apps/web/src/services/ops-flow/stores/task-type-store.ts b/apps/web/src/services/ops-flow/stores/task-type-store.ts
new file mode 100644
index 0000000000..bdb2c6f8e2
--- /dev/null
+++ b/apps/web/src/services/ops-flow/stores/task-type-store.ts
@@ -0,0 +1,161 @@
+import { computed, reactive, watch } from 'vue';
+
+import { defineStore } from 'pinia';
+
+import { SpaceConnector } from '@cloudforet/core-lib/space-connector';
+import { getCancellableFetcher } from '@cloudforet/core-lib/space-connector/cancellable-fetcher';
+
+import type { ListResponse } from '@/schema/_common/api-verbs/list';
+import type { TaskTypeCreateParameters } from '@/schema/opsflow/task-type/api-verbs/create';
+import type { TaskTypeDeleteParameters } from '@/schema/opsflow/task-type/api-verbs/delete';
+import type { TaskTypeGetParameters } from '@/schema/opsflow/task-type/api-verbs/get';
+import type { TaskTypeListParameters } from '@/schema/opsflow/task-type/api-verbs/list';
+import type { TaskTypeUpdateParameters } from '@/schema/opsflow/task-type/api-verbs/update';
+import type { TaskTypeUpdateFieldsParameters } from '@/schema/opsflow/task-type/api-verbs/update-fields';
+import type { TaskTypeModel } from '@/schema/opsflow/task-type/model';
+
+import { useAppContextStore } from '@/store/app-context/app-context-store';
+
+import ErrorHandler from '@/common/composables/error/errorHandler';
+
+interface UseTaskTypeStoreState {
+ loading: boolean;
+ itemsByCategoryId: Record;
+}
+
+export const useTaskTypeStore = defineStore('task-type', () => {
+ const state = reactive({
+ loading: false,
+ itemsByCategoryId: {},
+ });
+
+ const getters = {
+ loading: computed(() => state.loading),
+ };
+
+ const fetchList = getCancellableFetcher>(SpaceConnector.clientV2.opsflow.taskType.list);
+ const actions = {
+ async listByCategoryIds(categoryIds: string[]): Promise> {
+ const _categoryIds = categoryIds.filter((categoryId) => !state.itemsByCategoryId[categoryId]);
+ if (_categoryIds.length === 0) {
+ return state.itemsByCategoryId as Record;
+ }
+
+ try {
+ state.loading = true;
+ const result = await fetchList({ query: { filter: [{ k: 'category_id', v: _categoryIds, o: 'in' }] } });
+ if (result.status === 'succeed') {
+ _categoryIds.forEach((categoryId) => {
+ state.itemsByCategoryId[categoryId] ??= result.response.results?.filter((item) => item.category_id === categoryId) ?? [];
+ });
+ state.itemsByCategoryId = { ...state.itemsByCategoryId }; // trigger reactivity
+ state.loading = false;
+ }
+ } catch (e) {
+ ErrorHandler.handleError(e);
+ state.loading = false;
+ }
+ return state.itemsByCategoryId as Record;
+ },
+ async listByCategoryId(categoryId: string, force?: boolean): Promise {
+ if (state.itemsByCategoryId[categoryId] && !force) {
+ return state.itemsByCategoryId[categoryId] as TaskTypeModel[];
+ }
+
+ try {
+ state.loading = true;
+ const result = await fetchList({ query: { filter: [{ k: 'category_id', v: categoryId, o: 'eq' }] } });
+ if (result.status === 'succeed') {
+ state.itemsByCategoryId = { ...state.itemsByCategoryId, [categoryId]: result.response.results ?? [] };
+ state.loading = false;
+ return result.response.results ?? [];
+ }
+ return undefined;
+ } catch (e) {
+ ErrorHandler.handleError(e);
+ state.loading = false;
+ return state.itemsByCategoryId[categoryId];
+ }
+ },
+ async create(params: TaskTypeCreateParameters) {
+ const response = await SpaceConnector.clientV2.opsflow.taskType.create(params);
+ const categoryId = response.category_id;
+ if (state.itemsByCategoryId[categoryId]) {
+ state.itemsByCategoryId = {
+ ...state.itemsByCategoryId,
+ [categoryId]: [...state.itemsByCategoryId[categoryId] as TaskTypeModel[], response],
+ };
+ } else {
+ state.itemsByCategoryId = {
+ ...state.itemsByCategoryId,
+ [categoryId]: [response],
+ };
+ }
+ return response;
+ },
+ async update(params: TaskTypeUpdateParameters) {
+ const response = await SpaceConnector.clientV2.opsflow.taskType.update(params);
+ const categoryId = response.category_id;
+ const item = state.itemsByCategoryId[categoryId]?.find((c) => c.category_id === categoryId);
+ if (item) {
+ Object.assign(item, response);
+ }
+ return response;
+ },
+ async updateFields(params: TaskTypeUpdateFieldsParameters) {
+ const response = await SpaceConnector.clientV2.opsflow.taskType.updateFields(params);
+ const categoryId = response.category_id;
+ const item = state.itemsByCategoryId[categoryId]?.find((c) => c.category_id === categoryId);
+ if (item) {
+ Object.assign(item, response);
+ }
+ return response;
+ },
+ async get(taskTypeId: string) {
+ const response = await SpaceConnector.clientV2.opsflow.taskType.get({
+ task_type_id: taskTypeId,
+ });
+ const categoryId = response.category_id;
+ const item = state.itemsByCategoryId[categoryId]?.find((c) => c.category_id === categoryId);
+ if (item) {
+ Object.assign(item, response);
+ }
+ return response;
+ },
+ async delete(taskTypeId: string, categoryId?: string) {
+ await SpaceConnector.clientV2.opsflow.taskType.delete({
+ task_type_id: taskTypeId,
+ });
+ if (categoryId) {
+ state.itemsByCategoryId = {
+ ...state.itemsByCategoryId,
+ [categoryId]: state.itemsByCategoryId[categoryId]?.filter((item) => item.task_type_id !== taskTypeId),
+ };
+ } else {
+ const deleted = Object.values(state.itemsByCategoryId).find((items) => items?.find((item) => item.task_type_id === taskTypeId));
+ const _categoryId = deleted?.[0]?.category_id;
+ if (_categoryId) {
+ state.itemsByCategoryId = {
+ ...state.itemsByCategoryId,
+ [_categoryId]: state.itemsByCategoryId[_categoryId]?.filter((item) => item.task_type_id !== taskTypeId),
+ };
+ }
+ }
+ },
+ };
+
+ const disposeSelf = () => {
+ const store = useTaskTypeStore();
+ store.$reset();
+ store.$dispose();
+ };
+ const appContextStore = useAppContextStore();
+ watch([() => appContextStore.getters.isAdminMode, () => appContextStore.getters.workspaceId], () => {
+ disposeSelf();
+ });
+ return {
+ state,
+ getters,
+ ...actions,
+ };
+});
diff --git a/apps/web/src/services/ops-flow/task-fields-configuration/composables/use-task-fields-configuration.ts b/apps/web/src/services/ops-flow/task-fields-configuration/composables/use-task-fields-configuration.ts
index 438d1483b8..c945fe0f8b 100644
--- a/apps/web/src/services/ops-flow/task-fields-configuration/composables/use-task-fields-configuration.ts
+++ b/apps/web/src/services/ops-flow/task-fields-configuration/composables/use-task-fields-configuration.ts
@@ -1,4 +1,4 @@
-import type { TaskField } from '@/schema/opsflow/_types/task-field-type';
+import type { TaskField, TaskFieldSelectionType, TaskFieldType } from '@/schema/opsflow/_types/task-field-type';
import getRandomId from '@/lib/random-id-generator';
@@ -8,6 +8,19 @@ import type {
TaskFieldTypeMetadata,
} from '@/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type';
+const DEFAULT_SELECTION_TYPE_MAP: Record = {
+ GLOBAL: undefined,
+ TEXT: undefined,
+ PARAGRAPH: undefined,
+ LABELS: 'MULTI',
+ DROPDOWN: 'MULTI',
+ DATE: undefined,
+ USER: 'MULTI',
+ ASSET: 'MULTI',
+ PROJECT: 'MULTI',
+ PROVIDER: 'MULTI',
+ SERVICE_ACCOUNT: 'MULTI',
+};
export const useTaskFieldsConfiguration = () => {
const validationMap = new Map();
@@ -20,6 +33,7 @@ export const useTaskFieldsConfiguration = () => {
return {
taskFieldsValidator,
+ fields,
setFields,
addField(field: TaskFieldTypeMetadata) {
const fieldId = getRandomId();
@@ -29,10 +43,10 @@ export const useTaskFieldsConfiguration = () => {
field_type: field.type,
name: field.name,
is_required: false,
+ selection_type: DEFAULT_SELECTION_TYPE_MAP[field.type],
is_primary: false,
- is_folded: false,
options: {},
- }]);
+ } as TaskField]);
},
removeField(fieldId: string) {
validationMap.delete(fieldId);
@@ -43,12 +57,12 @@ export const useTaskFieldsConfiguration = () => {
// manually trigger validation to update invalid state
validateFields();
},
- setInitialFields(initialFields: TaskField[]) {
+ setInitialFields(initialFields?: TaskField[]) {
validationMap.clear();
- initialFields.forEach((field) => {
+ initialFields?.forEach((field) => {
validationMap.set(field.field_id, true);
});
- setFields(initialFields);
+ setFields(initialFields ?? []);
},
};
};
diff --git a/apps/web/src/services/ops-flow/task-fields-configuration/constants/default-field-constant.ts b/apps/web/src/services/ops-flow/task-fields-configuration/constants/default-field-constant.ts
index 9e4adecf9a..2467c92892 100644
--- a/apps/web/src/services/ops-flow/task-fields-configuration/constants/default-field-constant.ts
+++ b/apps/web/src/services/ops-flow/task-fields-configuration/constants/default-field-constant.ts
@@ -1,4 +1,5 @@
export const DEFAULT_FIELD_ID_MAP = {
title: 'title',
- content: 'content',
-};
+ description: 'description',
+ project: 'project',
+} as const;
diff --git a/apps/web/src/services/ops-flow/task-fields-configuration/stores/use-task-field-metadata-store.ts b/apps/web/src/services/ops-flow/task-fields-configuration/stores/use-task-field-metadata-store.ts
index a4b94dc5bb..6fe1bf79e7 100644
--- a/apps/web/src/services/ops-flow/task-fields-configuration/stores/use-task-field-metadata-store.ts
+++ b/apps/web/src/services/ops-flow/task-fields-configuration/stores/use-task-field-metadata-store.ts
@@ -1,4 +1,4 @@
-import { computed, reactive } from 'vue';
+import { computed } from 'vue';
import { defineStore } from 'pinia';
@@ -9,7 +9,7 @@ import type { TaskFieldTypeMetadata } from '@/services/ops-flow/task-fields-conf
export const useTaskFieldMetadataStore = defineStore('task-field-metadata', () => {
// TODO: i18n
- const getters = reactive({
+ const getters = {
taskFieldTypeMetadataMap: computed>(() => ({
GLOBAL: { type: 'GLOBAL', name: 'Global', icon: 'ic_global' }, // not used yet
TEXT: { type: 'TEXT', name: 'Short Text', icon: 'ic_short-text' },
@@ -20,6 +20,8 @@ export const useTaskFieldMetadataStore = defineStore('task-field-metadata', () =
USER: { type: 'USER', name: 'User', icon: 'ic_member' },
ASSET: { type: 'ASSET', name: 'Asset', icon: 'ic_service_asset-inventory' },
PROJECT: { type: 'PROJECT', name: 'Project', icon: 'ic_service_project' },
+ PROVIDER: { type: 'PROVIDER', name: 'Provider', icon: 'ic_service_provider' }, // not used yet
+ SERVICE_ACCOUNT: { type: 'SERVICE_ACCOUNT', name: 'Service Account', icon: 'ic_service_service-account' }, // not used yet
})),
taskFieldTypeMetadataList: computed(() => [
getters.taskFieldTypeMetadataMap.TEXT,
@@ -38,16 +40,29 @@ export const useTaskFieldMetadataStore = defineStore('task-field-metadata', () =
field_type: 'TEXT',
is_required: true,
is_primary: true,
+ options: {
+ example: 'Task Title',
+ },
},
{
- field_id: DEFAULT_FIELD_ID_MAP.content,
- name: 'Content', // TODO: i18n
- field_type: 'PARAGRAPH',
+ field_id: DEFAULT_FIELD_ID_MAP.project,
+ name: 'Project', // TODO: i18n
+ field_type: 'PROJECT',
is_required: true,
is_primary: true,
},
+ {
+ field_id: DEFAULT_FIELD_ID_MAP.description,
+ name: 'Description', // TODO: i18n
+ field_type: 'PARAGRAPH',
+ is_required: false,
+ is_primary: true,
+ options: {
+ example: 'Task Description',
+ },
+ },
]),
- });
+ };
return {
getters,
diff --git a/apps/web/src/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type.ts b/apps/web/src/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type.ts
index b57e6f30de..a2d5f4583c 100644
--- a/apps/web/src/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type.ts
+++ b/apps/web/src/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type.ts
@@ -1,7 +1,11 @@
import type { TaskFieldType } from '@/schema/opsflow/_types/task-field-type';
+import type { DEFAULT_FIELD_ID_MAP } from '@/services/ops-flow/task-fields-configuration/constants/default-field-constant';
+
export interface TaskFieldTypeMetadata {
type: TaskFieldType;
name: string;
icon: string;
}
+
+export type DefaultTaskFieldId = typeof DEFAULT_FIELD_ID_MAP[keyof typeof DEFAULT_FIELD_ID_MAP];
diff --git a/apps/web/src/services/ops-flow/task-fields-form/TaskFieldsForm.vue b/apps/web/src/services/ops-flow/task-fields-form/TaskFieldsForm.vue
new file mode 100644
index 0000000000..4866819351
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/TaskFieldsForm.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/composables/use-task-field-validation.ts b/apps/web/src/services/ops-flow/task-fields-form/composables/use-task-field-validation.ts
new file mode 100644
index 0000000000..a4bf477398
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/composables/use-task-field-validation.ts
@@ -0,0 +1,98 @@
+import type { Ref, UnwrapRef } from 'vue';
+import { watch } from 'vue';
+
+import type { TaskField, TaskFieldType } from '@/schema/opsflow/_types/task-field-type';
+
+import ErrorHandler from '@/common/composables/error/errorHandler';
+import type { ValidatorFn } from '@/common/composables/form-validator';
+import { useFieldValidator } from '@/common/composables/form-validator';
+
+import type {
+ TaskFieldFormEmits,
+ TaskFieldFormProps,
+} from '@/services/ops-flow/task-fields-form/types/task-field-form-type';
+
+type NotRef = T extends Ref ? never : T;
+const stringValidatorTypes: TaskFieldType[] = ['TEXT', 'PARAGRAPH', 'DATE'];
+const stringArrayValidatorTypes: TaskFieldType[] = ['LABELS', 'DROPDOWN', 'USER', 'PROJECT']; // , 'PROVIDER', 'SERVICE_ACCOUNT', 'ASSET'];
+export const useTaskFieldValidation = (
+ props: TaskFieldFormProps>,
+ emit: TaskFieldFormEmits,
+) => {
+ const stringValidator: ValidatorFn = (val): string|boolean => {
+ if (val === undefined || val === null) {
+ if (props.field.is_required) {
+ return 'This field is required';
+ }
+ return true;
+ }
+ if (typeof val !== 'string') {
+ return 'Value must be a string';
+ }
+ if (props.field.is_required && val.trim() === '') {
+ return 'This field is required';
+ }
+ return true;
+ };
+
+ const stringArrayValidator: ValidatorFn = (val): string|boolean => {
+ if (val === undefined || val === null) {
+ if (props.field.is_required) {
+ return 'This field is required';
+ }
+ return true;
+ }
+ if (!Array.isArray(val)) {
+ return 'Value must be an array';
+ }
+ // SINGLE case
+ if (!props.field.selection_type || props.field.selection_type === 'SINGLE') {
+ if (val.length > 1) {
+ return 'Only one value is allowed';
+ }
+ if (props.field.is_required && val.length === 0) {
+ return 'This field is required';
+ }
+ return true;
+ }
+ // MULTI case
+ if (props.field.is_required && val.length === 0) {
+ return 'This field is required';
+ }
+ return true;
+ };
+
+
+ const {
+ value: fieldValue,
+ setValue: setFieldValue,
+ invalidText, isInvalid, validationResult,
+ } = useFieldValidator(props.value, (val) => {
+ if (stringValidatorTypes.includes(props.field.field_type)) {
+ return stringValidator(val);
+ }
+ if (stringArrayValidatorTypes.includes(props.field.field_type)) {
+ return stringArrayValidator(val);
+ }
+ ErrorHandler.handleError(new Error(`Unsupported field type: ${props.field.field_type}`));
+ return 'Unsupported field type';
+ });
+
+ const updateFieldValue = (val: TValue) => {
+ setFieldValue(val as UnwrapRef);
+ emit('update:value', val);
+ };
+
+ watch(validationResult, (val: boolean) => {
+ emit('update:is-valid', val);
+ }, { immediate: true });
+
+ return {
+ fieldValue,
+ setFieldValue,
+ invalidText,
+ isInvalid,
+ isValid: validationResult,
+ updateFieldValue,
+ };
+};
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/AssetTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/AssetTaskField.vue
new file mode 100644
index 0000000000..aa59174b55
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/AssetTaskField.vue
@@ -0,0 +1,7 @@
+
+
+
+ AssetFieldForm
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/DateTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/DateTaskField.vue
new file mode 100644
index 0000000000..ec8d78bf06
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/DateTaskField.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/DropdownTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/DropdownTaskField.vue
new file mode 100644
index 0000000000..63d42faa96
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/DropdownTaskField.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/LabelsTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/LabelsTaskField.vue
new file mode 100644
index 0000000000..ae9a98db73
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/LabelsTaskField.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/ParagraphTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ParagraphTaskField.vue
new file mode 100644
index 0000000000..836c8041fb
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ParagraphTaskField.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/ProjectTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ProjectTaskField.vue
new file mode 100644
index 0000000000..74f4b06ed1
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ProjectTaskField.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/ProviderTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ProviderTaskField.vue
new file mode 100644
index 0000000000..7b3f9f170f
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ProviderTaskField.vue
@@ -0,0 +1,7 @@
+
+
+
+ ProviderFieldForm
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/ServiceAccountTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ServiceAccountTaskField.vue
new file mode 100644
index 0000000000..636fc7d42b
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/ServiceAccountTaskField.vue
@@ -0,0 +1,7 @@
+
+
+
+ ServiceAccountTaskFieldForm
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/TextTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/TextTaskField.vue
new file mode 100644
index 0000000000..78185d5b95
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/TextTaskField.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/UnknownTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/UnknownTaskField.vue
new file mode 100644
index 0000000000..06a394c0fe
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/UnknownTaskField.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/field-templates/UserTaskField.vue b/apps/web/src/services/ops-flow/task-fields-form/field-templates/UserTaskField.vue
new file mode 100644
index 0000000000..42d4aeffc8
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/field-templates/UserTaskField.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/services/ops-flow/task-fields-form/types/task-field-form-type.ts b/apps/web/src/services/ops-flow/task-fields-form/types/task-field-form-type.ts
new file mode 100644
index 0000000000..56f6755319
--- /dev/null
+++ b/apps/web/src/services/ops-flow/task-fields-form/types/task-field-form-type.ts
@@ -0,0 +1,10 @@
+import type { TaskField } from '@/schema/opsflow/_types/task-field-type';
+
+export interface TaskFieldFormProps {
+ field: TField;
+ value: TValue;
+}
+export interface TaskFieldFormEmits {
+ (event: 'update:value', value: TValue): void;
+ (event: 'update:is-valid', value: boolean): void;
+}
diff --git a/apps/web/src/services/ops-flow/types/board-page-type.ts b/apps/web/src/services/ops-flow/types/board-page-type.ts
new file mode 100644
index 0000000000..8fe085e4a5
--- /dev/null
+++ b/apps/web/src/services/ops-flow/types/board-page-type.ts
@@ -0,0 +1,10 @@
+import type { ConsoleFilter } from '@cloudforet/core-lib/query/type';
+
+import type { RouteQueryString } from '@/lib/router-query-string';
+
+export type BoardPageQuery = Partial>;
+
+export interface BoardPageQueryValue {
+ categoryId?: string;
+ filters?: ConsoleFilter[];
+}
diff --git a/apps/web/src/services/ops-flow/types/task-create-page-type.ts b/apps/web/src/services/ops-flow/types/task-create-page-type.ts
new file mode 100644
index 0000000000..a8c111af24
--- /dev/null
+++ b/apps/web/src/services/ops-flow/types/task-create-page-type.ts
@@ -0,0 +1,7 @@
+import type { RouteQueryString } from '@/lib/router-query-string';
+
+export type TaskCreatePageQuery = Partial>;
+
+export interface TaskCreatePageQueryValue {
+ categoryId?: string;
+}
diff --git a/apps/web/src/store/reference/user-reference-store.ts b/apps/web/src/store/reference/user-reference-store.ts
index 39d5dfc88c..b8e4715402 100644
--- a/apps/web/src/store/reference/user-reference-store.ts
+++ b/apps/web/src/store/reference/user-reference-store.ts
@@ -70,6 +70,7 @@ export const useUserReferenceStore = defineStore('reference-user', () => {
});
const getters = reactive({
+ loading: computed(() => state.items === null),
userItems: asyncComputed(async () => {
if (store.getters['user/getCurrentGrantInfo'].scope === 'USER') return {};
if (state.items === null) await load();
diff --git a/packages/mirinae/src/data-display/collapsible/collapsible-list/type.ts b/packages/mirinae/src/data-display/collapsible/collapsible-list/type.ts
index 135f11ad3f..cdd4988612 100644
--- a/packages/mirinae/src/data-display/collapsible/collapsible-list/type.ts
+++ b/packages/mirinae/src/data-display/collapsible/collapsible-list/type.ts
@@ -1,10 +1,10 @@
import type { COLLAPSIBLE_LIST_THEME, COLLAPSIBLE_LIST_TOGGLE_POSITION } from '@/data-display/collapsible/collapsible-list/config';
import type { COLLAPSIBLE_TOGGLE_TYPE } from '@/data-display/collapsible/collapsible-toggle/type';
-export interface CollapsibleItem {
+export interface CollapsibleItem {
name?: string;
title?: string;
- data: string;
+ data: T;
}
export interface CollapsibleListProps {