import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import omit from 'lodash/omit'; import { SwapOutlined } from '@ant-design/icons'; import classNames from 'classnames'; import kebabCase from 'lodash.kebabcase'; import { useUrlSearchParams } from 'use-url-search-params'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import useMedia from 'react-use/lib/useMedia'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import useDebounce from 'react-use/lib/useDebounce'; import { useRect } from '@reach/rect'; import Table from '../table'; import { ApolloTableActionType, ApolloTableProps, ApolloTableRowSelection, OrderDirection, SelectedView, Variables, } from './interfaces'; import withRenderByValueType from './utils/withRenderByValueType'; import Filters, { AppliedFilterInterface } from '../filters'; import { renderSearch } from './utils/renderSearch'; import { generateLabel } from './utils/generateLabel'; import { Card } from '../card'; import Popover from '../popover'; import Button from '../button'; import Radio from '../radio'; import { ConfigContext } from '../config-provider'; import { findColumn } from './utils/findColumn'; import SimplePagination from '../simple-pagination'; import { useActionType } from './hooks/useActionType'; import defaultLocale from '../locale/en_US'; import withCopyableAndEllipsis from './utils/withCopyableAndEllipsis'; import useMountMergeState from './utils/useMountMergeState'; import { useColumnSettingsStorage } from './hooks/useColumnSettingsStorage'; import { BatchActions } from './components/BatchActions'; import { filterSearchToComponent } from './utils/filterSearchToComponent'; import { ColumnsSetting } from './components/ColumnsSetting'; import withColumnSettings from './utils/withColumnSettings'; import withHidden from './utils/withHidden'; import useActiveKey from './hooks/useActiveKey'; import { SelectedViewTabs } from './components/SelectedViewTabs'; import { SelectedViewButton } from './components/SelectedViewButton'; import { filterValuesToSearch } from './utils/filterValuesToSearch'; import usePopState from './hooks/usePopState'; import Select from '../select'; import { generateQuery } from './utils/generateQuery'; import { useLayoutHeaderHeight } from '../frame/useLayoutHeaderHeight'; export const ApolloTable = ({ id, syncToUrl, actionRef: propsActionRef, cardProps, columns = [], search, options = { setting: true }, pageInfo, pagination, request, debounceTime = 300, tableAlertRender, rowSelection: propsRowSelection, components, dataSource, loading, sticky: propsSticky, ...props }: ApolloTableProps) => { const { getPrefixCls, locale: { ApolloTable: locale } = { ApolloTable: defaultLocale.ApolloTable }, } = React.useContext(ConfigContext); const [layoutHeaderHeight] = useLayoutHeaderHeight(); /** 计算 Card 宽度 */ const ref = useRef(null); // 用来触发查询的 const [depend, triggerReload] = useReducer(x => x + 1, 0); const isMobile = useMedia('(max-width: 458px)'); const [before, setBefore] = useState(''); const [after, setAfter] = useState(''); // 是否是页面载入第一次触发 handleChange const isChangeFirst = useRef(true); // 是否获取服务端视图结束 const isSelectedViewsGetEnd = useRef(false); // 当前是否是全选模式 const [isSelectedAll, setIsSelectedAll] = useState(false); const [columnSettings, setColumnSettings] = useColumnSettingsStorage(id, columns); const [searchParams, setSearchParams] = useUrlSearchParams({}, {}); const [localValue, setLocalValue] = useLocalStorage( `apollo-table-selected-views-saved:${id}`, ); const [serverSelectedViews, setServerSelectedViews] = useState([]); // 保存的视图,本地或服务端保存的 const [savedSelectedViews, setSavedSelectedViews] = useMemo(() => { if (search && search.selectedViews) { if (search.selectedViews.store) { return [serverSelectedViews, setServerSelectedViews]; } return [localValue || [], setLocalValue]; } return [ [], () => { // }, ]; }, [localValue, search, serverSelectedViews, setLocalValue]); // 配置的加上保存的视图 const allSelectedViews = useMemo(() => { if (search && search.selectedViews?.config) { return search.selectedViews?.config.concat(savedSelectedViews); } return savedSelectedViews; }, [savedSelectedViews, search]); const [activeKey, setActiveKey] = useActiveKey({ searchParams, columns, selectedViews: search ? search.selectedViews : undefined, localValue, }); const interColumns = useMemo(() => { let tempColumns = [...columns]; tempColumns = withColumnSettings(tempColumns, columnSettings); tempColumns = withHidden(tempColumns); tempColumns = withRenderByValueType(tempColumns); tempColumns = withCopyableAndEllipsis(tempColumns); return tempColumns; }, [columns, columnSettings]); /** 单选多选的相关逻辑 */ const [selectedRowKeys, setSelectedRowKeys] = useMountMergeState([], { value: propsRowSelection ? propsRowSelection.selectedRowKeys : undefined, }); const selectedRowsRef = useRef([]); const setSelectedRowsAndKey = useCallback( (keys: React.ReactText[], rows: T[]) => { setSelectedRowKeys(keys); selectedRowsRef.current = rows; }, [setSelectedRowKeys], ); // ============================ RowKey ============================ const getRowKey = React.useMemo(() => { if (typeof props.rowKey === 'function') { return props.rowKey; } return (record: T, index: number) => (record as any)?.[props.rowKey as string] ?? index; }, [props.rowKey]); /** 清空所有的选中项 */ const onCleanSelected = useCallback(() => { if (propsRowSelection && propsRowSelection.onChange) { propsRowSelection.onChange([], []); } setSelectedRowsAndKey([], []); // 退出全选模式 setIsSelectedAll(false); }, [propsRowSelection, setSelectedRowsAndKey]); /** 行选择相关的问题 */ const rowSelection: ApolloTableRowSelection = useMemo( () => ({ selectedRowKeys, ...propsRowSelection, onChange: (keys, rows) => { // 全选模式下触发 onChange 退出全选模式 if (isSelectedAll) { if (propsRowSelection && propsRowSelection.onChange) { propsRowSelection.onChange(keys, rows); } setIsSelectedAll(false); setSelectedRowsAndKey(keys, rows); } else { if (propsRowSelection && propsRowSelection.onChange) { propsRowSelection.onChange(keys, rows); } setSelectedRowsAndKey(keys, rows); } }, }), [isSelectedAll, propsRowSelection, selectedRowKeys, setSelectedRowsAndKey], ); const tableAlertRenderResult = useMemo( () => tableAlertRender ? tableAlertRender({ selectedRowKeys, selectedRows: selectedRowsRef.current, onCleanSelected, isSelectedAll, }) : false, [isSelectedAll, onCleanSelected, selectedRowKeys, tableAlertRender], ); const rect = useRect(ref, { observe: tableAlertRender !== false && propsRowSelection && !!selectedRowKeys.length, }); // 处理 sticky const { sticky } = useMemo(() => { if (propsSticky) { return { sticky: { offsetHeader: layoutHeaderHeight, ...(typeof propsSticky === 'object' ? { sticky: propsSticky } : {}), }, }; } return { sticky: propsSticky, }; }, [propsSticky, layoutHeaderHeight]); const componentsHeader = useMemo( () => tableAlertRender !== false && propsRowSelection && !!selectedRowKeys.length ? { row: ({ children }: any) => ( {children} ), } : {}, [ dataSource, getRowKey, isSelectedAll, onCleanSelected, pageInfo?.hasNextPage, pageInfo?.hasPreviousPage, props.size, propsRowSelection, rect?.left, rowSelection, selectedRowKeys.length, setSelectedRowsAndKey, tableAlertRender, tableAlertRenderResult, ], ); const actionRef = useRef(); const [inputValue, setInputValue] = useState(''); const [queryValue, setQueryValue] = useState(() => { // url 的优先级高 if (searchParams.query && syncToUrl) { return searchParams.query as string; } return ''; }); // 如果是按下回车搜索,queryValue 改变就要同步 inputValue useEffect(() => { if (search && search.pressEnterToSearch) { setInputValue(queryValue); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [queryValue]); const [filterValues, setFilterValues] = useState>(() => { if (syncToUrl) { return filterSearchToComponent(searchParams, columns); } return {}; }); const [sortValue, setSortValue] = useState(() => { if (syncToUrl && searchParams.order) { // url 的优先级最高 return searchParams.order as string; } if (search && search.defaultSortValue) { // 开启了视图 if (search.selectedViews) { if (activeKey === 'all') { return `${search.defaultSortValue.field} ${search.defaultSortValue.direction}`; } return ''; // 没开启视图 } return `${search.defaultSortValue.field} ${search.defaultSortValue.direction}`; } return ''; }); useEffect(() => { (async () => { if (search && search.selectedViews?.store) { const result = await search.selectedViews.store.get(); isSelectedViewsGetEnd.current = true; setServerSelectedViews(result || []); if (searchParams.saved_search_id) { const current = (result || []).find(i => i.id === searchParams.saved_search_id); if (current) { setQueryValue(current.query || ''); setSortValue( current.orderBy ? `${current.orderBy.field} ${current.orderBy.direction}` : '', ); setFilterValues(current.filters || {}); } } } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // isSelectedAll 发生变化回调 useUpdateEffect(() => { if (propsRowSelection && propsRowSelection.onSelectedAllChange) { propsRowSelection.onSelectedAllChange(isSelectedAll); } }, [isSelectedAll]); // activeKey 发生变化清空选中 useUpdateEffect(() => { onCleanSelected(); }, [activeKey]); // 数据发生改变时,且是全选模式时 ---(点击分页时触发,把) useUpdateEffect(() => { if (isSelectedAll) { const allKeys = (dataSource || []).map((item: any) => getRowKey(item)); setSelectedRowsAndKey(allKeys, (dataSource as any) || []); } }, [dataSource]); // 一页多少个 const pageSize = (pagination !== false && pagination?.pageSize) || 10; const appliedFilters = useMemo( (): AppliedFilterInterface[] => Object.keys(filterValues).map(key => { const col = findColumn(columns, key); return { key, label: ( <> {col?.title}: {generateLabel(col, filterValues[key], locale)} ), allowClear: col.search?.allowClear, onRemove: () => { setFilterValues(omit(filterValues, [key])); if (col.search?.onRemove) { col.search.onRemove(); } }, tagRender: col.search?.tagRender ? tagRenderProps => col.search?.tagRender?.({ ...tagRenderProps, value: filterValues[key], }) as React.ReactElement> : undefined, }; }), [columns, filterValues, locale], ); // 改变 url 参数函数 const changeHistoryState = () => { let queryParams: Record = {}; // 当前是否是服务端保存的 tab const isCurrentServerTab = serverSelectedViews.find(i => i.id === activeKey); /* ---------- 设置 selectedView、saved_search_id start ---------- */ if (search && search.selectedViews) { // 服务端视图 if (search.selectedViews.store) { // 如果当前是服务端 tab if (isCurrentServerTab) { // 从服务端 tab 切换到服务端 tab 或者 在服务端 tab 分页 或者 页面第一次加载 或者由 pop 触发 if (searchParams.saved_search_id) { // 分页触发的 或 改变了筛选 或 页面第一次加载才会相等 if (searchParams.saved_search_id === activeKey) { // 由分页触发或第一次加载或 pop if (before || after || isChangeFirst.current) { queryParams.saved_search_id = activeKey; // 改变了筛选 } else { queryParams.selectedView = 'all'; } // 从服务端 tab 切换到 服务端 tab } else { queryParams.saved_search_id = activeKey; } // queryParams.selectedView = 'all'; // 如果是普通 tab 切换到服务端 tab } else { queryParams.saved_search_id = activeKey; } } else { queryParams.selectedView = activeKey; } // 本地视图 } else { queryParams.selectedView = activeKey.startsWith('savedSearch') ? 'savedSearch' : activeKey; } } /* ---------- 设置 selectedView、saved_search_id end ---------- */ /* ---------- 设置筛选值 start ---------- */ const fn = () => { if (queryValue) { queryParams.query = queryValue; } if (Object.keys(filterValues).length > 0) { queryParams = { ...queryParams, ...filterValuesToSearch(filterValues, columns) }; } if (sortValue) { queryParams.order = sortValue; } }; // 如果当前的 tab 是服务端保存 if (search && search.selectedViews?.store && isCurrentServerTab) { // 如果是服务端 tab切换到服务端 tab, 不用做什么 // 如果从服务端 tab 切换到非服务端 tab,就要切换到 all if (searchParams.saved_search_id && !queryParams.saved_search_id && !isChangeFirst.current) { setActiveKey('all'); fn(); } } else { fn(); } /* ---------- 设置筛选值 end ---------- */ /* ---------- 设置分页 start ---------- */ // 如果是第一次加载,要把分页数据加上去 if (isChangeFirst.current) { if (searchParams.before) { queryParams.before = searchParams.before as string; } if (searchParams.after) { queryParams.after = searchParams.after as string; } } else { // 点击上一页 if (before) { queryParams.before = before; } // 点击下一页 if (after) { queryParams.after = after; } } /* ---------- 设置分页 end ---------- */ // 否则不添加 before 或 after(筛选排序直接跳回第一页) // 把参数设置进 localstorage localStorage.setItem(`apollo-table-query-params:${kebabCase(id)}`, JSON.stringify(queryParams)); // 是页面第一次加载或 pop 不执行 url 修改 if (!isChangeFirst.current) { // 先把原来的都 undefined 掉 const undefinedSearchParams: any = {}; Object.keys(searchParams).forEach(key => { // 判断是否要 undefined,如果 是输入框 或 排序 或 筛选 或 分页信息 let flag = false; if (isCurrentServerTab) { flag = true; } else if ( key === 'before' || key === 'after' || key === 'query' || key === 'order' || findColumn(columns, key) || key === 'selectedView' || key === 'saved_search_id' ) { flag = true; } if (flag) { undefinedSearchParams[key] = undefined; } }); setSearchParams({ ...undefinedSearchParams, ...queryParams, }); } }; const handleChange = async () => { // 服务端保存视图时,页面第一次加载时,请求完 get 再执行 if ( searchParams.saved_search_id && search && search.selectedViews?.store && !isSelectedViewsGetEnd.current ) { return; } if (search && search.pressEnterToSearch && queryValue !== inputValue) { setInputValue(queryValue); } const query = generateQuery(queryValue, filterValues, columns); const sortArr = sortValue.split(' '); const variables: Variables = {}; if (query) { variables.query = query; } if (sortValue && sortArr.length === 2) { variables.orderBy = { field: sortArr[0], direction: sortArr[1] as OrderDirection, }; } if (isChangeFirst.current) { const { before: firstBefore, after: firstAfter } = searchParams; if (firstBefore) { variables.last = pageSize; variables.before = firstBefore as string; } else if (firstAfter) { variables.first = pageSize; variables.after = firstAfter as string; } else { variables.first = pageSize; } } else { // 点击上一页 // eslint-disable-next-line no-lonely-if if (before) { variables.last = pageSize; variables.before = before; // 点击下一页 } else if (after) { variables.first = pageSize; variables.after = after; // 点击筛选或者排序或者 query } else { variables.first = pageSize; } } if (syncToUrl) { changeHistoryState(); } // 如果是页面载入第一次触发不执行 changeHistoryState isChangeFirst.current = false; if (request) { await request(variables); } }; // 防抖 如果是服务端保存,要在 get 结束后再请求 useDebounce(handleChange, debounceTime, [ queryValue, sortValue, JSON.stringify(filterValues), before, after, // 用来触发此函数的 depend, ]); usePopState({ searchParams, selectedViews: search ? search.selectedViews : undefined, localValue, serverSelectedViews, setQueryValue, setSortValue, setFilterValues, setActiveKey, defaultSortValue: search ? search.defaultSortValue : undefined, columns, setBefore, setAfter, onCleanSelected, on: syncToUrl, onMount: () => { isChangeFirst.current = true; }, }); /* ------- ref 操作 -------- */ useActionType(actionRef, { reload: triggerReload, apolloQuery: generateQuery(queryValue, filterValues, columns), queryValue, sortValue: sortValue ? { field: sortValue.split(' ')[0], direction: sortValue.split(' ')[1] as OrderDirection } : undefined, filterValues, setQueryValue: value => setQueryValue(value), setSortValue: value => setSortValue(`${value.field} ${value.direction}`), setFilterValues: value => setFilterValues(value), resetAndReload: () => { setQueryValue(''); setSortValue(''); setFilterValues({}); setBefore(''); setAfter(''); onCleanSelected(); triggerReload(); }, clearSelected: onCleanSelected, }); /* ------- ref 操作 -------- */ if (propsActionRef) { propsActionRef.current = actionRef.current; } // 回到第一页 const resetPage = useCallback(() => { setBefore(''); setAfter(''); }, []); // 筛选发生变化直接重置页码 useUpdateEffect(() => { resetPage(); }, [queryValue, JSON.stringify(filterValues), sortValue, activeKey]); const renderFilter = () => { if (search !== false) { return (
{ if (search?.pressEnterToSearch) { setInputValue(e.target.value); } else { setQueryValue(e.target.value); } search?.queryProps?.onChange?.(e); }, onPressEnter: e => { if (search?.pressEnterToSearch) { e.preventDefault(); setQueryValue(e.currentTarget.value); } search?.queryProps?.onPressEnter?.(e); }, }} appliedFilters={appliedFilters} filters={renderSearch(columns, filterValues, setFilterValues, getPrefixCls)} onClearAll={() => { setFilterValues({}); setQueryValue(''); if (search?.onClearAll) { search.onClearAll(); } }} filtersExtra={ isMobile && search?.sortOptions && ( <>