Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/src/services/advanced/routes/admin/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const adminAdvancedRoutes: RouteConfig = {
},
{
path: 'bookmark',
name: ADMIN_ADVANCED_ROUTE.BOOKMARK._NAME,
meta: {
lsbVisible: true,
menuId: MENU_ID.BOOKMARK,
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/services/alert-manager/v2/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const alertManagerRoute: RouteConfig = {
},
{
path: ':serviceId',
name: ALERT_MANAGER_ROUTE.SERVICE.DETAIL._NAME,
props: true,
component: { template: '<router-view />' },
children: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<router-view />' },
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/services/asset-inventory/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<router-view />' },
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/services/ops-flow/components/BoardTaskFilters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -74,6 +89,7 @@ const handleUpdateAssignee = (userIds: string[]) => {

/* event */
const taskFilters = computed<TaskFilters>(() => ({
taskId: selectedTaskIdItems.value.map((d) => d.name),
taskType: selectedTaskTypeItems.value.map((d) => d.name),
status: selectedStatusItems.value.map((d) => d.name),
project: selectedProjectIds.value,
Expand All @@ -89,6 +105,21 @@ watch(taskFilters, (newValue, oldValue) => {

<template>
<div class="flex flex-wrap gap-4">
<div v-if="props.categoryId">
<p-select-dropdown :key="taskIdsDropdownKey"
:selected="selectedTaskIdItems"
:handler="taskIdMenuItemsHandler"
:selection-label="String($t('OPSFLOW.FIELD_ID', { field: taskManagementTemplateStore.templates.task }))"
appearance-type="badge"
style-type="rounded"
multi-selectable
show-select-marker
show-delete-all-button
is-filterable
menu-width="12rem"
@update:selected="handleUpdateSelectedTaskIdItems"
/>
</div>
<p-select-dropdown v-if="props.categoryId"
:key="taskTypesDropdownKey"
:selected="selectedTaskTypeItems"
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/services/ops-flow/components/BoardTaskTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const handleUpdateFilters = (values: TaskFilters) => {
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();
Expand Down Expand Up @@ -158,6 +159,10 @@ watch(data, (d) => {

/* table fields */
const fields = computed<DataTableField[] >(() => [
{
name: 'task_id',
label: i18n.t('OPSFLOW.FIELD_ID', { field: taskManagementTemplateStore.templates.task }) as string,
},
{
name: 'name',
label: i18n.t('OPSFLOW.TITLE') as string,
Expand Down
79 changes: 79 additions & 0 deletions apps/web/src/services/ops-flow/composables/use-task-ids-field.ts
Original file line number Diff line number Diff line change
@@ -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<string|undefined>;
isRequired?: boolean;
}) => {
const taskManagementTemplateStore = useTaskManagementTemplateStore();

const taskIdsValidator = useFieldValidator<SelectDropdownMenuItem[]>(
[],
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<SelectDropdownMenuItem[]>(() => {
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<string>(() => `task-id-${allTaskIdItems.value.map((item) => item.name).join(',')}`);

return {
selectedTaskIdItems,
taskIdMenuItemsHandler,
setSelectedTaskIdItems,
taskIdsDropdownKey,
};
};
50 changes: 45 additions & 5 deletions apps/web/src/services/ops-flow/pages/TaskDetailPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
},
Expand All @@ -190,10 +193,48 @@ const { isSuccess, mutateAsync: updateTaskMutation, isPending: isUpdating } = us
},
});

/* update task description */
const { isSuccess: isSuccessUpdateDescription, mutateAsync: updateDescriptionMutation, isPending: isUpdatingDescription } = useMutation<TaskModel, APIError>({
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 */
Expand All @@ -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);

Expand Down Expand Up @@ -264,13 +305,13 @@ defineExpose({ setPathFrom });
class="py-3 flex flex-wrap gap-1 justify-end"
>
<p-button style-type="transparent"
:disabled="isUpdating"
:disabled="isUpdating || isUpdatingDescription"
@click="goBack()"
>
{{ $t('COMMON.BUTTONS.CANCEL') }}
</p-button>
<p-button style-type="primary"
:loading="isUpdating"
:loading="isUpdating || isUpdatingDescription"
:disabled="!taskContentFormStore.getters.hasUnsavedChanges || !taskContentFormStore.getters.isAllValid"
@click="handleSaveChanges"
>
Expand All @@ -292,4 +333,3 @@ defineExpose({ setPathFrom });
<task-delete-modal @deleted="goBack()" />
</div>
</template>

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,8 +44,6 @@ interface UseTaskContentFormStoreGetters {
isArchivedTask: boolean;
}
export const useTaskContentFormStore = defineStore('task-content-form', () => {
const userStore = useUserStore();

const state = reactive<UseTaskContentFormStoreState>({
// base form
currentCategoryId: undefined,
Expand All @@ -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,
Expand Down Expand Up @@ -101,7 +100,7 @@ export const useTaskContentFormStore = defineStore('task-content-form', () => {
isEditable: computed<boolean>(() => {
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<References>(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<script setup lang="ts">
import {
computed,
defineAsyncComponent, onUnmounted,
computed, defineAsyncComponent, onUnmounted,
} from 'vue';

import type { TaskFieldType } from '@/api-clients/opsflow/_types/task-field-type';

import { useTaskQuery } from '@/services/ops-flow/composables/use-task-query';
import { useTaskContentFormStore } from '@/services/ops-flow/stores/task-content-form-store';
import { DEFAULT_FIELD_ID_MAP } from '@/services/ops-flow/task-fields-configuration/constants/default-field-constant';


const COMPONENT_MAP: Partial<Record<TaskFieldType, ReturnType<typeof defineAsyncComponent>>> = {
Expand All @@ -34,9 +32,6 @@ const { data: originTask } = useTaskQuery({
taskId: computed(() => taskContentFormState.currentTaskId),
});

/* fields for rendering */
const isEditableFieldInViewMode = (fieldId: string) => fieldId === DEFAULT_FIELD_ID_MAP.title;

/* fields form */
onUnmounted(() => {
taskContentFormStore.resetFieldsForm();
Expand All @@ -51,7 +46,7 @@ onUnmounted(() => {
:key="field.field_id"
:field="field"
:value="taskContentFormGetters.defaultData[field.field_id]"
:readonly="taskContentFormState.mode === 'view' ? !(taskContentFormGetters.isEditable && isEditableFieldInViewMode(field.field_id)) : false"
:readonly="!taskContentFormGetters.isEditable"
:files="originTask?.files"
:references="taskContentFormGetters.references"
@update:value="taskContentFormStore.setDefaultFieldData(field.field_id, $event)"
Expand All @@ -64,7 +59,7 @@ onUnmounted(() => {
:key="field.field_id"
:field="field"
:value="taskContentFormGetters.data[field.field_id]"
:readonly="taskContentFormState.mode === 'view'"
:readonly="!taskContentFormGetters.isEditable"
:files="originTask?.files"
:references="taskContentFormGetters.references"
@update:value="taskContentFormStore.setFieldData(field.field_id, $event)"
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/services/ops-flow/types/task-filters-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface TaskFilters {
project: string[];
createdBy: string[];
assignee: string[];
taskId: string[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ defineExpose({
</p-button>
</div>
<div v-if="props.searchable"
v-show="props.menu.length > 0"
class="search-wrapper"
>
<p-search :value="state.proxySearchText"
Expand Down
Loading