fix: 保留工作流列表返回状态
- 记录页码、分类和搜索条件并同步到路由 - 从工作流设计页返回时恢复原列表位置
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { ArrowDown, Search } from '@element-plus/icons-vue';
|
import { ArrowDown, Search } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +31,10 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 3,
|
default: 3,
|
||||||
},
|
},
|
||||||
|
initialValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
searchPlaceholder: {
|
searchPlaceholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: $t('common.searchPlaceholder'),
|
default: $t('common.searchPlaceholder'),
|
||||||
@@ -39,7 +43,14 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['search', 'buttonClick', 'reset']);
|
const emit = defineEmits(['search', 'buttonClick', 'reset']);
|
||||||
|
|
||||||
const searchValue = ref('');
|
const searchValue = ref(props.initialValue);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialValue,
|
||||||
|
(value) => {
|
||||||
|
searchValue.value = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const filterButtonsByPermission = (buttons) => {
|
const filterButtonsByPermission = (buttons) => {
|
||||||
return buttons.filter((button) => {
|
return buttons.filter((button) => {
|
||||||
|
|||||||
@@ -5,6 +5,40 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
import PageData from './PageData.vue';
|
import PageData from './PageData.vue';
|
||||||
|
|
||||||
describe('page data recovery', () => {
|
describe('page data recovery', () => {
|
||||||
|
it('loads the restored page and query without requesting the first page', async () => {
|
||||||
|
const get = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ data: { records: [], totalRow: 30 } });
|
||||||
|
const wrapper = mount(PageData, {
|
||||||
|
global: {
|
||||||
|
directives: { loading: {} },
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initialPageNumber: 3,
|
||||||
|
initialQueryParams: { categoryId: '7', title: '月报' },
|
||||||
|
pageSize: 18,
|
||||||
|
pageUrl: '/page',
|
||||||
|
requestClient: { get },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(get).toHaveBeenCalledWith('/page', {
|
||||||
|
params: {
|
||||||
|
categoryId: '7',
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 18,
|
||||||
|
title: '月报',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect((wrapper.vm as any).getPageState()).toEqual({
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 18,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shows a retry action after a page request fails', async () => {
|
it('shows a retry action after a page request fails', async () => {
|
||||||
const get = vi
|
const get = vi
|
||||||
.fn()
|
.fn()
|
||||||
|
|||||||
@@ -13,27 +13,38 @@ interface PageDataProps {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
pageSizes?: number[];
|
pageSizes?: number[];
|
||||||
extraQueryParams?: Record<string, any>;
|
extraQueryParams?: Record<string, any>;
|
||||||
|
initialPageNumber?: number;
|
||||||
|
initialQueryParams?: Record<string, any>;
|
||||||
requestClient?: any;
|
requestClient?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageDataRow = Record<string, any>;
|
type PageDataRow = Record<string, any>;
|
||||||
|
interface PageDataState {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<PageDataProps>(), {
|
const props = withDefaults(defineProps<PageDataProps>(), {
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
pageSizes: () => [10, 20, 50, 100],
|
pageSizes: () => [10, 20, 50, 100],
|
||||||
extraQueryParams: () => ({}),
|
extraQueryParams: () => ({}),
|
||||||
|
initialPageNumber: 1,
|
||||||
|
initialQueryParams: () => ({}),
|
||||||
requestClient: () => api,
|
requestClient: () => api,
|
||||||
});
|
});
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'stateChange', state: PageDataState): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const pageList = ref<PageDataRow[]>([]);
|
const pageList = ref<PageDataRow[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadError = ref<unknown>();
|
const loadError = ref<unknown>();
|
||||||
const queryParams = ref<Record<string, any>>({});
|
const queryParams = ref<Record<string, any>>({ ...props.initialQueryParams });
|
||||||
let pageRequest = 0;
|
let pageRequest = 0;
|
||||||
|
|
||||||
const pageInfo = reactive({
|
const pageInfo = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: Math.max(1, Math.trunc(props.initialPageNumber)),
|
||||||
pageSize: props.pageSize,
|
pageSize: props.pageSize,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
@@ -80,10 +91,21 @@ const getPageList = async () => {
|
|||||||
const handleSizeChange = (newSize: number) => {
|
const handleSizeChange = (newSize: number) => {
|
||||||
pageInfo.pageSize = newSize;
|
pageInfo.pageSize = newSize;
|
||||||
pageInfo.pageNumber = 1; // 重置到第一页
|
pageInfo.pageNumber = 1; // 重置到第一页
|
||||||
|
emitPageState();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCurrentChange = (newPage: number) => {
|
const handleCurrentChange = (newPage: number) => {
|
||||||
pageInfo.pageNumber = newPage;
|
pageInfo.pageNumber = newPage;
|
||||||
|
emitPageState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageState = (): PageDataState => ({
|
||||||
|
pageNumber: pageInfo.pageNumber,
|
||||||
|
pageSize: pageInfo.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emitPageState = () => {
|
||||||
|
emit('stateChange', getPageState());
|
||||||
};
|
};
|
||||||
|
|
||||||
const patchRowById = (
|
const patchRowById = (
|
||||||
@@ -110,10 +132,12 @@ const setQuery = (newQueryParams: Record<string, any>) => {
|
|||||||
pageInfo.pageNumber = 1;
|
pageInfo.pageNumber = 1;
|
||||||
pageInfo.pageSize = props.pageSize;
|
pageInfo.pageSize = props.pageSize;
|
||||||
queryParams.value = newQueryParams;
|
queryParams.value = newQueryParams;
|
||||||
|
emitPageState();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
getPageState,
|
||||||
reload: getPageList,
|
reload: getPageList,
|
||||||
patchRowById,
|
patchRowById,
|
||||||
setQuery,
|
setQuery,
|
||||||
@@ -130,6 +154,7 @@ watch(
|
|||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
emitPageState();
|
||||||
getPageList();
|
getPageList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ describe('detail return navigation', () => {
|
|||||||
expect(listSource).toContain('tab: activeTab.value');
|
expect(listSource).toContain('tab: activeTab.value');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns workflow design to the originating list page and filters', () => {
|
||||||
|
const detailSource = readViewSource('ai/workflow/WorkflowDesign.vue');
|
||||||
|
const listSource = readViewSource('ai/workflow/WorkflowList.vue');
|
||||||
|
|
||||||
|
expect(detailSource).toContain(
|
||||||
|
'parseWorkflowDesignReturnState(route.query)',
|
||||||
|
);
|
||||||
|
expect(detailSource).toContain(
|
||||||
|
'query: buildWorkflowListRouteQuery(listState)',
|
||||||
|
);
|
||||||
|
expect(listSource).toContain('buildWorkflowDesignReturnQuery');
|
||||||
|
expect(listSource).toContain('getPageState');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns plugin tool editing to its plugin tool list with a safe fallback', () => {
|
it('returns plugin tool editing to its plugin tool list with a safe fallback', () => {
|
||||||
const editSource = readViewSource('ai/plugin/PluginToolEdit.vue');
|
const editSource = readViewSource('ai/plugin/PluginToolEdit.vue');
|
||||||
const listSource = readViewSource('ai/plugin/PluginToolTable.vue');
|
const listSource = readViewSource('ai/plugin/PluginToolTable.vue');
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ import {
|
|||||||
isWorkflowDataEmpty,
|
isWorkflowDataEmpty,
|
||||||
normalizeWorkflowStartNodes,
|
normalizeWorkflowStartNodes,
|
||||||
} from '../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
|
} from '../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
|
||||||
|
import {
|
||||||
|
buildWorkflowListRouteQuery,
|
||||||
|
parseWorkflowDesignReturnState,
|
||||||
|
} from './workflow-list-route-state';
|
||||||
|
|
||||||
import '@tinyflow-ai/vue/dist/index.css';
|
import '@tinyflow-ai/vue/dist/index.css';
|
||||||
|
|
||||||
@@ -160,7 +164,11 @@ async function initializeWorkflow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backToWorkflowList() {
|
function backToWorkflowList() {
|
||||||
router.replace({ path: '/ai/workflow' });
|
const listState = parseWorkflowDesignReturnState(route.query);
|
||||||
|
router.replace({
|
||||||
|
path: '/ai/workflow',
|
||||||
|
query: buildWorkflowListRouteQuery(listState),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const WORKFLOW_DRAFT_WRITE_DELAY = 320;
|
const WORKFLOW_DRAFT_WRITE_DELAY = 320;
|
||||||
let draftWriteTimer: ReturnType<typeof setTimeout> | undefined;
|
let draftWriteTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
|
import type { WorkflowListRouteState } from './workflow-list-route-state';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ActionButton,
|
ActionButton,
|
||||||
CardPrimaryAction,
|
CardPrimaryAction,
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
onMounted,
|
onMounted,
|
||||||
ref,
|
ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { useAccess } from '@easyflow/access';
|
import { useAccess } from '@easyflow/access';
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
@@ -75,6 +78,11 @@ import {
|
|||||||
resolveAiResourceDisplayStatus,
|
resolveAiResourceDisplayStatus,
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildWorkflowDesignReturnQuery,
|
||||||
|
mergeWorkflowListRouteQuery,
|
||||||
|
parseWorkflowListRouteState,
|
||||||
|
} from './workflow-list-route-state';
|
||||||
import WorkflowModal from './WorkflowModal.vue';
|
import WorkflowModal from './WorkflowModal.vue';
|
||||||
|
|
||||||
const ElXMarkdown = defineAsyncComponent(
|
const ElXMarkdown = defineAsyncComponent(
|
||||||
@@ -82,6 +90,8 @@ const ElXMarkdown = defineAsyncComponent(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
const route = useRoute();
|
||||||
|
const initialListState = parseWorkflowListRouteState(route.query);
|
||||||
|
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
// 字段名称
|
// 字段名称
|
||||||
@@ -276,8 +286,15 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
const saveDialog = ref();
|
const saveDialog = ref();
|
||||||
const selectedCategoryId = ref<number | string>('');
|
const selectedCategoryId = ref<number | string>(initialListState.categoryId);
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref(initialListState.keyword);
|
||||||
|
const currentPageNumber = ref(initialListState.pageNumber);
|
||||||
|
const currentPageSize = ref(initialListState.pageSize);
|
||||||
|
const suppressInitialCategoryChange = ref(Boolean(initialListState.categoryId));
|
||||||
|
const initialQueryParams = {
|
||||||
|
categoryId: initialListState.categoryId || undefined,
|
||||||
|
title: initialListState.keyword || undefined,
|
||||||
|
};
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
const headerButtons = [
|
const headerButtons = [
|
||||||
{
|
{
|
||||||
@@ -353,6 +370,32 @@ function applyFilters() {
|
|||||||
title: searchKeyword.value || undefined,
|
title: searchKeyword.value || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function currentListState(): WorkflowListRouteState {
|
||||||
|
const pageState = pageDataRef.value?.getPageState?.();
|
||||||
|
return {
|
||||||
|
categoryId: String(selectedCategoryId.value || ''),
|
||||||
|
keyword: searchKeyword.value,
|
||||||
|
pageNumber: pageState?.pageNumber ?? currentPageNumber.value,
|
||||||
|
pageSize: pageState?.pageSize ?? currentPageSize.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function syncListRouteState() {
|
||||||
|
const target = {
|
||||||
|
path: '/ai/workflow',
|
||||||
|
query: mergeWorkflowListRouteQuery(route.query, currentListState()),
|
||||||
|
};
|
||||||
|
if (router.resolve(target).fullPath !== route.fullPath) {
|
||||||
|
void router.replace(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handlePageStateChange(state: {
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}) {
|
||||||
|
currentPageNumber.value = state.pageNumber;
|
||||||
|
currentPageSize.value = state.pageSize;
|
||||||
|
syncListRouteState();
|
||||||
|
}
|
||||||
function handleSearch(keyword: string) {
|
function handleSearch(keyword: string) {
|
||||||
searchKeyword.value = keyword;
|
searchKeyword.value = keyword;
|
||||||
applyFilters();
|
applyFilters();
|
||||||
@@ -859,6 +902,7 @@ function toDesignPage(row: any) {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
pageKey: '/ai/workflow',
|
pageKey: '/ai/workflow',
|
||||||
navTitle: resolveNavTitle(row),
|
navTitle: resolveNavTitle(row),
|
||||||
|
...buildWorkflowDesignReturnQuery(currentListState()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -979,7 +1023,17 @@ const formRules = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function changeCategory(category: any) {
|
function changeCategory(category: any) {
|
||||||
selectedCategoryId.value = category?.id ?? '';
|
const categoryId = category?.id ?? '';
|
||||||
|
if (
|
||||||
|
suppressInitialCategoryChange.value &&
|
||||||
|
String(categoryId) === initialListState.categoryId
|
||||||
|
) {
|
||||||
|
suppressInitialCategoryChange.value = false;
|
||||||
|
selectedCategoryId.value = categoryId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
suppressInitialCategoryChange.value = false;
|
||||||
|
selectedCategoryId.value = categoryId;
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
function showControlDialog(item: any) {
|
function showControlDialog(item: any) {
|
||||||
@@ -1045,6 +1099,17 @@ const getSideList = async () => {
|
|||||||
},
|
},
|
||||||
...res.data,
|
...res.data,
|
||||||
];
|
];
|
||||||
|
if (
|
||||||
|
selectedCategoryId.value &&
|
||||||
|
!res.data.some(
|
||||||
|
(category: any) =>
|
||||||
|
String(category.id) === String(selectedCategoryId.value),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
suppressInitialCategoryChange.value = false;
|
||||||
|
selectedCategoryId.value = '';
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function handleHeaderButtonClick(data: any) {
|
function handleHeaderButtonClick(data: any) {
|
||||||
@@ -1085,6 +1150,7 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
</ElDialog>
|
</ElDialog>
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
|
:initial-value="searchKeyword"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleHeaderButtonClick"
|
@button-click="handleHeaderButtonClick"
|
||||||
/>
|
/>
|
||||||
@@ -1103,7 +1169,10 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
page-url="/api/v1/workflow/page"
|
page-url="/api/v1/workflow/page"
|
||||||
:page-sizes="[12, 18, 24]"
|
:page-sizes="[12, 18, 24]"
|
||||||
:page-size="12"
|
:page-size="initialListState.pageSize"
|
||||||
|
:initial-page-number="initialListState.pageNumber"
|
||||||
|
:initial-query-params="initialQueryParams"
|
||||||
|
@state-change="handlePageStateChange"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<CardList
|
<CardList
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildWorkflowDesignReturnQuery,
|
||||||
|
buildWorkflowListRouteQuery,
|
||||||
|
mergeWorkflowListRouteQuery,
|
||||||
|
parseWorkflowDesignReturnState,
|
||||||
|
parseWorkflowListRouteState,
|
||||||
|
} from './workflow-list-route-state';
|
||||||
|
|
||||||
|
describe('workflow list route state', () => {
|
||||||
|
it('restores page, page size, category and keyword from list query', () => {
|
||||||
|
expect(
|
||||||
|
parseWorkflowListRouteState({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: '3',
|
||||||
|
pageSize: '18',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 18,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back safely for invalid pagination query', () => {
|
||||||
|
expect(
|
||||||
|
parseWorkflowListRouteState({
|
||||||
|
pageNumber: '-2',
|
||||||
|
pageSize: '999',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
categoryId: '',
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 12,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('round trips list state through workflow design return query', () => {
|
||||||
|
const state = {
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: 3,
|
||||||
|
pageSize: 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
const designQuery = buildWorkflowDesignReturnQuery(state);
|
||||||
|
|
||||||
|
expect(designQuery).toEqual({
|
||||||
|
returnCategoryId: '7',
|
||||||
|
returnKeyword: '月报',
|
||||||
|
returnPageNumber: '3',
|
||||||
|
returnPageSize: '18',
|
||||||
|
});
|
||||||
|
expect(parseWorkflowDesignReturnState(designQuery)).toEqual(state);
|
||||||
|
expect(buildWorkflowListRouteQuery(state)).toEqual({
|
||||||
|
categoryId: '7',
|
||||||
|
keyword: '月报',
|
||||||
|
pageNumber: '3',
|
||||||
|
pageSize: '18',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves unrelated query while removing cleared list state', () => {
|
||||||
|
expect(
|
||||||
|
mergeWorkflowListRouteQuery(
|
||||||
|
{
|
||||||
|
categoryId: '7',
|
||||||
|
devLogin: 'admin',
|
||||||
|
keyword: '旧关键字',
|
||||||
|
pageNumber: '5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryId: '',
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 12,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toEqual({ devLogin: 'admin' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import type { LocationQuery } from 'vue-router';
|
||||||
|
|
||||||
|
const DEFAULT_PAGE_NUMBER = 1;
|
||||||
|
const DEFAULT_PAGE_SIZE = 12;
|
||||||
|
const WORKFLOW_PAGE_SIZES = new Set([12, 18, 24]);
|
||||||
|
|
||||||
|
const LIST_QUERY_KEYS = {
|
||||||
|
categoryId: 'categoryId',
|
||||||
|
keyword: 'keyword',
|
||||||
|
pageNumber: 'pageNumber',
|
||||||
|
pageSize: 'pageSize',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const RETURN_QUERY_KEYS = {
|
||||||
|
categoryId: 'returnCategoryId',
|
||||||
|
keyword: 'returnKeyword',
|
||||||
|
pageNumber: 'returnPageNumber',
|
||||||
|
pageSize: 'returnPageSize',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface WorkflowListQueryKeys {
|
||||||
|
readonly categoryId: string;
|
||||||
|
readonly keyword: string;
|
||||||
|
readonly pageNumber: string;
|
||||||
|
readonly pageSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkflowListRouteState {
|
||||||
|
categoryId: string;
|
||||||
|
keyword: string;
|
||||||
|
pageNumber: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readQueryValue(query: LocationQuery, key: string): string {
|
||||||
|
const value = query[key];
|
||||||
|
const normalized = Array.isArray(value) ? value[0] : value;
|
||||||
|
return normalized === null || normalized === undefined
|
||||||
|
? ''
|
||||||
|
: String(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInteger(value: string, fallback: number): number {
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseState(
|
||||||
|
query: LocationQuery,
|
||||||
|
keys: WorkflowListQueryKeys,
|
||||||
|
): WorkflowListRouteState {
|
||||||
|
const pageSize = parsePositiveInteger(
|
||||||
|
readQueryValue(query, keys.pageSize),
|
||||||
|
DEFAULT_PAGE_SIZE,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
categoryId: readQueryValue(query, keys.categoryId),
|
||||||
|
keyword: readQueryValue(query, keys.keyword),
|
||||||
|
pageNumber: parsePositiveInteger(
|
||||||
|
readQueryValue(query, keys.pageNumber),
|
||||||
|
DEFAULT_PAGE_NUMBER,
|
||||||
|
),
|
||||||
|
pageSize: WORKFLOW_PAGE_SIZES.has(pageSize) ? pageSize : DEFAULT_PAGE_SIZE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStateQuery(
|
||||||
|
state: WorkflowListRouteState,
|
||||||
|
keys: WorkflowListQueryKeys,
|
||||||
|
): LocationQuery {
|
||||||
|
return {
|
||||||
|
...(state.pageNumber === DEFAULT_PAGE_NUMBER
|
||||||
|
? {}
|
||||||
|
: { [keys.pageNumber]: String(state.pageNumber) }),
|
||||||
|
...(state.pageSize === DEFAULT_PAGE_SIZE
|
||||||
|
? {}
|
||||||
|
: { [keys.pageSize]: String(state.pageSize) }),
|
||||||
|
...(state.categoryId ? { [keys.categoryId]: state.categoryId } : {}),
|
||||||
|
...(state.keyword ? { [keys.keyword]: state.keyword } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkflowListRouteState(
|
||||||
|
query: LocationQuery,
|
||||||
|
): WorkflowListRouteState {
|
||||||
|
return parseState(query, LIST_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkflowDesignReturnState(
|
||||||
|
query: LocationQuery,
|
||||||
|
): WorkflowListRouteState {
|
||||||
|
return parseState(query, RETURN_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkflowListRouteQuery(
|
||||||
|
state: WorkflowListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
return buildStateQuery(state, LIST_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkflowDesignReturnQuery(
|
||||||
|
state: WorkflowListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
return buildStateQuery(state, RETURN_QUERY_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeWorkflowListRouteQuery(
|
||||||
|
query: LocationQuery,
|
||||||
|
state: WorkflowListRouteState,
|
||||||
|
): LocationQuery {
|
||||||
|
const listQueryKeys = new Set<string>(Object.values(LIST_QUERY_KEYS));
|
||||||
|
const nextQuery = Object.fromEntries(
|
||||||
|
Object.entries(query).filter(([key]) => !listQueryKeys.has(key)),
|
||||||
|
) as LocationQuery;
|
||||||
|
return {
|
||||||
|
...nextQuery,
|
||||||
|
...buildWorkflowListRouteQuery(state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildWorkflowDesignReturnQuery,
|
||||||
|
buildWorkflowListRouteQuery,
|
||||||
|
mergeWorkflowListRouteQuery,
|
||||||
|
parseWorkflowDesignReturnState,
|
||||||
|
parseWorkflowListRouteState,
|
||||||
|
};
|
||||||
|
export type { WorkflowListRouteState };
|
||||||
Reference in New Issue
Block a user