diff --git a/apps/web/src/services/advanced/routes/admin/routes.ts b/apps/web/src/services/advanced/routes/admin/routes.ts
index 72a235ab80..4a3c1bf5e4 100644
--- a/apps/web/src/services/advanced/routes/admin/routes.ts
+++ b/apps/web/src/services/advanced/routes/admin/routes.ts
@@ -49,7 +49,6 @@ const adminAdvancedRoutes: RouteConfig = {
},
{
path: 'bookmark',
- name: ADMIN_ADVANCED_ROUTE.BOOKMARK._NAME,
meta: {
lsbVisible: true,
menuId: MENU_ID.BOOKMARK,
diff --git a/apps/web/src/services/alert-manager/v2/routes/routes.ts b/apps/web/src/services/alert-manager/v2/routes/routes.ts
index 1741e510c4..2c960f005b 100644
--- a/apps/web/src/services/alert-manager/v2/routes/routes.ts
+++ b/apps/web/src/services/alert-manager/v2/routes/routes.ts
@@ -52,7 +52,6 @@ const alertManagerRoute: RouteConfig = {
},
{
path: ':serviceId',
- name: ALERT_MANAGER_ROUTE.SERVICE.DETAIL._NAME,
props: true,
component: { template: '' },
children: [
diff --git a/apps/web/src/services/asset-inventory/routes/admin/routes.ts b/apps/web/src/services/asset-inventory/routes/admin/routes.ts
index 20bad53fca..53320bf258 100644
--- a/apps/web/src/services/asset-inventory/routes/admin/routes.ts
+++ b/apps/web/src/services/asset-inventory/routes/admin/routes.ts
@@ -169,7 +169,6 @@ const adminAssetInventoryRoute: RouteConfig = {
},
{
path: ':metricId',
- name: ADMIN_ASSET_INVENTORY_ROUTE.METRIC_EXPLORER.DETAIL._NAME,
meta: { label: ({ params }) => params.metricId, lsbVisible: true },
props: true,
component: { template: '' },
diff --git a/apps/web/src/services/asset-inventory/routes/routes.ts b/apps/web/src/services/asset-inventory/routes/routes.ts
index ebcc9d7cd9..1c973617fb 100644
--- a/apps/web/src/services/asset-inventory/routes/routes.ts
+++ b/apps/web/src/services/asset-inventory/routes/routes.ts
@@ -177,7 +177,6 @@ const assetInventoryRoute: RouteConfig = {
},
{
path: ':metricId',
- name: ASSET_INVENTORY_ROUTE.METRIC_EXPLORER.DETAIL._NAME,
meta: { label: ({ params }) => params.metricId, lsbVisible: true },
props: true,
component: { template: '' },
diff --git a/apps/web/src/services/ops-flow/components/BoardTaskFilters.vue b/apps/web/src/services/ops-flow/components/BoardTaskFilters.vue
index 8b366cf281..5198799348 100644
--- a/apps/web/src/services/ops-flow/components/BoardTaskFilters.vue
+++ b/apps/web/src/services/ops-flow/components/BoardTaskFilters.vue
@@ -11,6 +11,7 @@ import type { SelectDropdownMenuItem } from '@cloudforet/mirinae/types/controls/
import ProjectSelectDropdown from '@/common/modules/project/ProjectSelectDropdown.vue';
import UserSelectDropdown from '@/common/modules/user/UserSelectDropdown.vue';
+import { useTaskIdsField } from '@/services/ops-flow/composables/use-task-ids-field';
import { useTaskStatusField } from '@/services/ops-flow/composables/use-task-status-field';
import { useTaskTypeField } from '@/services/ops-flow/composables/use-task-type-field';
import {
@@ -26,6 +27,20 @@ const emit = defineEmits<{(event: 'update', value: TaskFilters): void;
const taskManagementTemplateStore = useTaskManagementTemplateStore();
+/* task id */
+const {
+ selectedTaskIdItems,
+ taskIdMenuItemsHandler,
+ setSelectedTaskIdItems,
+ taskIdsDropdownKey,
+} = useTaskIdsField({
+ categoryId: toRef(props, 'categoryId'),
+ isRequired: true,
+});
+const handleUpdateSelectedTaskIdItems = (items: SelectDropdownMenuItem[]) => {
+ setSelectedTaskIdItems(items);
+};
+
/* task type */
const {
selectedTaskTypeItems,
@@ -74,6 +89,7 @@ const handleUpdateAssignee = (userIds: string[]) => {
/* event */
const taskFilters = computed(() => ({
+ taskId: selectedTaskIdItems.value.map((d) => d.name),
taskType: selectedTaskTypeItems.value.map((d) => d.name),
status: selectedStatusItems.value.map((d) => d.name),
project: selectedProjectIds.value,
@@ -89,6 +105,21 @@ watch(taskFilters, (newValue, oldValue) => {
+
{
if (values.project.length) _taskFilterHelper.addFilter({ k: 'project_id', v: values.project, o: '=' });
if (values.createdBy.length) _taskFilterHelper.addFilter({ k: 'created_by', v: values.createdBy, o: '=' });
if (values.assignee.length) _taskFilterHelper.addFilter({ k: 'assignee', v: values.assignee, o: '=' });
+ if (values.taskId.length) _taskFilterHelper.addFilter({ k: 'task_id', v: values.taskId, o: '=' });
taskFilters.value = _taskFilterHelper.filters;
};
const _taskListQueryHelper = new ApiQueryHelper();
@@ -158,6 +159,10 @@ watch(data, (d) => {
/* table fields */
const fields = computed(() => [
+ {
+ name: 'task_id',
+ label: i18n.t('OPSFLOW.FIELD_ID', { field: taskManagementTemplateStore.templates.task }) as string,
+ },
{
name: 'name',
label: i18n.t('OPSFLOW.TITLE') as string,
diff --git a/apps/web/src/services/ops-flow/composables/use-task-ids-field.ts b/apps/web/src/services/ops-flow/composables/use-task-ids-field.ts
new file mode 100644
index 0000000000..b7f9a23e69
--- /dev/null
+++ b/apps/web/src/services/ops-flow/composables/use-task-ids-field.ts
@@ -0,0 +1,79 @@
+import type { Ref } from 'vue';
+import { computed } from 'vue';
+
+import { getTextHighlightRegex } from '@cloudforet/mirinae';
+import type { AutocompleteHandler, SelectDropdownMenuItem } from '@cloudforet/mirinae/types/controls/dropdown/select-dropdown/type';
+
+import { getParticle, i18n } from '@/translations';
+
+import { useFieldValidator } from '@/common/composables/form-validator';
+
+import {
+ useTaskManagementTemplateStore,
+} from '@/services/ops-flow/task-management-templates/stores/use-task-management-template-store';
+
+import { useAssociatedTasksQuery } from './use-associated-tasks-query';
+
+
+export const useTaskIdsField = ({
+ isRequired, categoryId,
+}: {
+ categoryId: Ref;
+ isRequired?: boolean;
+}) => {
+ const taskManagementTemplateStore = useTaskManagementTemplateStore();
+
+ const taskIdsValidator = useFieldValidator(
+ [],
+ isRequired ? (val) => {
+ if (val.length === 0) {
+ const taskIdLabel = i18n.t('OPSFLOW.FIELD_ID', { field: taskManagementTemplateStore.templates.task }) as string;
+ return i18n.t('OPSFLOW.VALIDATION.REQUIRED', {
+ topic: taskIdLabel,
+ particle: getParticle(taskIdLabel, 'topic'),
+ });
+ }
+ return true;
+ } : undefined,
+ );
+ const selectedTaskIdItems = taskIdsValidator.value;
+
+ const setSelectedTaskIdItems = (selectedTaskIds: SelectDropdownMenuItem[]) => {
+ taskIdsValidator.setValue(selectedTaskIds);
+ };
+
+ const { tasks } = useAssociatedTasksQuery({
+ queryKey: computed(() => ({
+ query: { filter: [{ k: 'category_id', v: categoryId.value, o: 'eq' }] },
+ })),
+ enabled: computed(() => !!categoryId.value),
+ });
+ const allTaskIdItems = computed(() => {
+ if (!categoryId.value) return [];
+ if (!tasks.value) return [];
+ return tasks.value.map((t) => ({
+ name: t.task_id,
+ label: t.task_id,
+ })) || [];
+ });
+ const taskIdMenuItemsHandler: AutocompleteHandler = (keyword: string, pageStart = 1, pageLimit = 10) => {
+ const filteredItems = allTaskIdItems.value.filter((item) => getTextHighlightRegex(keyword).test(item.label as string));
+ const _totalCount = pageStart - 1 + Number(pageLimit);
+ const _slicedResults = filteredItems.slice(pageStart - 1, _totalCount);
+ return {
+ results: _slicedResults,
+ more: _totalCount < filteredItems.length,
+ };
+ };
+
+ // taskIdsdropdownKey is for the dropdown component to re-render when the allTaskIdItems value changes
+ // This is a workaround for the issue that the dropdown component does not re-render when the allTaskIdItems value changes
+ const taskIdsDropdownKey = computed(() => `task-id-${allTaskIdItems.value.map((item) => item.name).join(',')}`);
+
+ return {
+ selectedTaskIdItems,
+ taskIdMenuItemsHandler,
+ setSelectedTaskIdItems,
+ taskIdsDropdownKey,
+ };
+};
diff --git a/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue b/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
index 2c17367789..99c8d5f76b 100644
--- a/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
+++ b/apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
@@ -31,6 +31,7 @@ import {
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router/composables';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
+import { isEqual } from 'lodash';
import type { APIError } from '@cloudforet/core-lib/space-connector/error';
import {
@@ -175,6 +176,8 @@ const { isSuccess, mutateAsync: updateTaskMutation, isPending: isUpdating } = us
const res = await taskAPI.update({
task_id: task.value.task_id,
name: taskContentFormStore.getters.defaultData[DEFAULT_FIELD_ID_MAP.title],
+ project_id: taskContentFormStore.getters.defaultData[DEFAULT_FIELD_ID_MAP.project],
+ data: taskContentFormStore.getters.data,
});
return res;
},
@@ -190,10 +193,48 @@ const { isSuccess, mutateAsync: updateTaskMutation, isPending: isUpdating } = us
},
});
+/* update task description */
+const { isSuccess: isSuccessUpdateDescription, mutateAsync: updateDescriptionMutation, isPending: isUpdatingDescription } = useMutation({
+ mutationFn: async () => {
+ if (!task.value) throw new Error('Origin task is not defined');
+ const res = await taskAPI.updateDescription({
+ task_id: task.value.task_id,
+ description: taskContentFormStore.getters.defaultData[DEFAULT_FIELD_ID_MAP.description],
+ });
+ return res;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: taskDetailQueryKey.value });
+ showSuccessMessage(_i18n.t('OPSFLOW.ALT_S_UPDATE_TARGET', { target: taskManagementTemplateStore.templates.task }), '');
+ refetchEvents();
+ },
+ onError: (error) => {
+ ErrorHandler.handleRequestError(error, _i18n.t('OPSFLOW.ALT_E_UPDATE_TARGET', { target: taskManagementTemplateStore.templates.task }));
+ },
+});
+
+
+
+
/* form button handling */
const handleSaveChanges = async () => {
if (!taskContentFormStore.getters.isAllValid) return;
- await updateTaskMutation();
+
+ const currentDescription = taskContentFormStore.getters.defaultData[DEFAULT_FIELD_ID_MAP.description];
+ const isDescriptionChanged = task.value?.description !== currentDescription;
+ if (isDescriptionChanged) {
+ await updateDescriptionMutation();
+ }
+
+ const isDataChanged = !isEqual(task.value?.data, taskContentFormStore.getters.data);
+ const otherFields = Object.entries(taskContentFormStore.getters.defaultData).filter(([k]) => k !== DEFAULT_FIELD_ID_MAP.description);
+ const isOtherFieldsChanged = otherFields.some(([k, v]) => {
+ if (k === DEFAULT_FIELD_ID_MAP.description) return false;
+ return task.value?.[k] !== v;
+ });
+ if (isDataChanged || isOtherFieldsChanged) {
+ await updateTaskMutation();
+ }
};
/* confirm leave modal */
@@ -203,7 +244,7 @@ const {
confirmRouteLeave,
stopRouteLeave,
} = useConfirmRouteLeave({
- passConfirmation: computed(() => !taskContentFormStore.getters.hasUnsavedChanges || isSuccess.value),
+ passConfirmation: computed(() => !taskContentFormStore.getters.hasUnsavedChanges || isSuccess.value || isSuccessUpdateDescription.value),
});
onBeforeRouteLeave(handleBeforeRouteLeave);
@@ -264,13 +305,13 @@ defineExpose({ setPathFrom });
class="py-3 flex flex-wrap gap-1 justify-end"
>
{{ $t('COMMON.BUTTONS.CANCEL') }}
@@ -292,4 +333,3 @@ defineExpose({ setPathFrom });
-
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
index 08ec8c23e9..86d740dd92 100644
--- 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
@@ -8,15 +8,15 @@ import { APIError } from '@cloudforet/core-lib/space-connector/error';
import type { TaskField } from '@/api-clients/opsflow/_types/task-field-type';
import type { TaskModel } from '@/api-clients/opsflow/task/schema/model';
-import { useUserStore } from '@/store/user/user-store';
-
import { useCategoryQuery } from '@/services/ops-flow/composables/use-current-category';
import { useCurrentTaskType } from '@/services/ops-flow/composables/use-current-task-type';
import { useTaskFieldsForm } from '@/services/ops-flow/composables/use-task-fields-form';
+import { useTaskQuery } from '@/services/ops-flow/composables/use-task-query';
import { DEFAULT_FIELD_ID_MAP } from '@/services/ops-flow/task-fields-configuration/constants/default-field-constant';
import type { DefaultTaskFieldId } from '@/services/ops-flow/task-fields-configuration/types/task-field-type-metadata-type';
import type { References } from '@/services/ops-flow/task-fields-form/types/task-field-form-type';
+
interface UseTaskContentFormStoreState {
// base form
currentCategoryId?: string;
@@ -44,8 +44,6 @@ interface UseTaskContentFormStoreGetters {
isArchivedTask: boolean;
}
export const useTaskContentFormStore = defineStore('task-content-form', () => {
- const userStore = useUserStore();
-
const state = reactive({
// base form
currentCategoryId: undefined,
@@ -67,6 +65,7 @@ export const useTaskContentFormStore = defineStore('task-content-form', () => {
/* fields form */
const taskTypeId = computed(() => state.currentTaskTypeId);
const { currentTaskType } = useCurrentTaskType({ taskTypeId });
+ const { data: currentTask } = useTaskQuery({ taskId: computed(() => state.currentTaskId) });
const {
fieldsToShow,
hasUnsavedFieldsChanges,
@@ -101,7 +100,7 @@ export const useTaskContentFormStore = defineStore('task-content-form', () => {
isEditable: computed(() => {
if (state.mode === 'create' || state.mode === 'create-minimal') return true;
if (isArchivedTask.value) return false;
- if (userStore.getters.isDomainAdmin) return true;
+ if (currentTask.value?.status_type === 'TODO') return true;
return false;
}),
references: computed(() => {
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
index c115802904..51c08ca20c 100644
--- a/apps/web/src/services/ops-flow/task-fields-form/TaskFieldsForm.vue
+++ b/apps/web/src/services/ops-flow/task-fields-form/TaskFieldsForm.vue
@@ -1,14 +1,12 @@