diff --git a/core/src/main/java/datart/core/data/provider/sql/OrderOperator.java b/core/src/main/java/datart/core/data/provider/sql/OrderOperator.java index 0807bb34f..2a29e8189 100644 --- a/core/src/main/java/datart/core/data/provider/sql/OrderOperator.java +++ b/core/src/main/java/datart/core/data/provider/sql/OrderOperator.java @@ -21,6 +21,8 @@ import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + @Data @EqualsAndHashCode(callSuper = true) public class OrderOperator extends ColumnOperator { @@ -29,9 +31,12 @@ public class OrderOperator extends ColumnOperator { private SqlOperator operator; + private List value; + public enum SqlOperator { ASC, - DESC + DESC, + CUSTOMIZE } @Override diff --git a/data-providers/data-provider-base/src/main/java/datart/data/provider/calcite/SqlBuilder.java b/data-providers/data-provider-base/src/main/java/datart/data/provider/calcite/SqlBuilder.java index 674c4147f..0d09c6315 100644 --- a/data-providers/data-provider-base/src/main/java/datart/data/provider/calcite/SqlBuilder.java +++ b/data-providers/data-provider-base/src/main/java/datart/data/provider/calcite/SqlBuilder.java @@ -25,11 +25,16 @@ import datart.core.data.provider.SingleTypedValue; import datart.core.data.provider.sql.*; import datart.data.provider.calcite.custom.CustomSqlBetweenOperator; +import org.apache.calcite.avatica.util.Casing; +import org.apache.calcite.avatica.util.Quoting; import org.apache.calcite.sql.*; import org.apache.calcite.sql.fun.SqlBetweenOperator; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.parser.SqlParseException; +import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.sql.parser.SqlParserPos; +import org.apache.calcite.sql.parser.impl.SqlParserImpl; +import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; @@ -268,6 +273,30 @@ private SqlNode createOrderNode(OrderOperator operator) { if (operator.getOperator() == OrderOperator.SqlOperator.DESC) { return new SqlBasicCall(SqlStdOperatorTable.DESC, new SqlNode[]{sqlNode}, SqlParserPos.ZERO); + } else if (operator.getOperator() == OrderOperator.SqlOperator.CUSTOMIZE) { + // implements customize sort of single none-num field by 'case + when' + List values = operator.getValue(); + StringBuffer sortExpr = new StringBuffer(); + sortExpr.append("case "); + for (int i = 0; i < values.size(); i++) { + String v = values.get(i); + sortExpr.append("when " + operator.getColumnKey() + " = '" + v + "' then " + i); + } + sortExpr.append(" end"); + + SqlParser.Config config = SqlParser.config() + .withParserFactory(SqlParserImpl.FACTORY) + .withQuotedCasing(Casing.UNCHANGED) + .withUnquotedCasing(Casing.UNCHANGED) + .withConformance(SqlConformanceEnum.LENIENT) + .withCaseSensitive(true) + .withQuoting(Quoting.BRACKET); + try { + SqlParser parser = SqlParser.create(sortExpr.toString(), config); + return parser.parseExpression(); + } catch (SqlParseException e) { + throw new RuntimeException(e); + } } else { return sqlNode; } diff --git a/frontend/src/app/components/ChartEditor.tsx b/frontend/src/app/components/ChartEditor.tsx index 71d57237b..d501bc7c1 100644 --- a/frontend/src/app/components/ChartEditor.tsx +++ b/frontend/src/app/components/ChartEditor.tsx @@ -60,6 +60,7 @@ import { } from 'app/utils/ChartEventListenerHelper'; import { clearRuntimeDateLevelFieldsInChartConfig, + clearUnsupportedChartConfig, filterCurrentUsedComputedFields, getValue, } from 'app/utils/chartHelper'; @@ -315,8 +316,10 @@ export const ChartEditor: FC = ({ setChart(c); const targetChartConfig = CloneValueDeep(c.config); - const finalChartConfig = clearRuntimeDateLevelFieldsInChartConfig( - transferChartConfigs(targetChartConfig, shadowChartConfig || chartConfig), + const finalChartConfig = clearUnsupportedChartConfig( + clearRuntimeDateLevelFieldsInChartConfig( + transferChartConfigs(targetChartConfig, shadowChartConfig || chartConfig) + ) ); const computedFields = updateBy(dataview?.computedFields || [], draft => { @@ -591,7 +594,7 @@ export const ChartEditor: FC = ({ { ...builder.build(), ...{ - analytics: dataChartId ? false : true, + analytics: !dataChartId, vizName: backendChart?.name || 'chart', vizId: isWidget ? widgetId : dataChartId, vizType: isWidget ? 'widget' : 'dataChart', diff --git a/frontend/src/app/components/ChartGraph/MingXiTableChart/config.ts b/frontend/src/app/components/ChartGraph/MingXiTableChart/config.ts index e2b55002a..6ea49f618 100644 --- a/frontend/src/app/components/ChartGraph/MingXiTableChart/config.ts +++ b/frontend/src/app/components/ChartGraph/MingXiTableChart/config.ts @@ -26,6 +26,7 @@ const config: ChartConfig = { key: 'mixed', required: true, type: 'mixed', + allowFieldCustomizeSort: true, }, { label: 'filter', diff --git a/frontend/src/app/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx b/frontend/src/app/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx index e9dac6c6d..b1e188242 100644 --- a/frontend/src/app/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx +++ b/frontend/src/app/components/ChartGraph/PivotSheetChart/PivotSheetChart.tsx @@ -588,16 +588,16 @@ class PivotSheetChart extends ReactChart { ): Array { return sectionConfigRows .map(config => { - if (!config?.sort?.type || config?.sort?.type === SortActionType.None) { + const type = config?.sort?.type; + if (!type || type === SortActionType.None || type === SortActionType.Customize) { return null; } - const isASC = config.sort.type === SortActionType.ASC; return { sortFieldId: chartDataSet.getFieldKey(config), sortFunc: params => { const { data } = params; return data?.sort((a, b) => - isASC ? a?.localeCompare(b) : b?.localeCompare(a), + type === SortActionType.ASC ? a?.localeCompare(b) : b?.localeCompare(a), ); }, }; diff --git a/frontend/src/app/components/ChartGraph/PivotSheetChart/config.ts b/frontend/src/app/components/ChartGraph/PivotSheetChart/config.ts index baa5220bf..2fe46b912 100644 --- a/frontend/src/app/components/ChartGraph/PivotSheetChart/config.ts +++ b/frontend/src/app/components/ChartGraph/PivotSheetChart/config.ts @@ -27,6 +27,7 @@ const config: ChartConfig = { options: { sortable: { backendSort: false }, }, + allowFieldCustomizeSort: true, }, { label: 'row', @@ -37,6 +38,7 @@ const config: ChartConfig = { }, drillable: false, drillContextMenuVisible: true, + allowFieldCustomizeSort: true, }, { label: 'metrics', diff --git a/frontend/src/app/constants.ts b/frontend/src/app/constants.ts index 22003a54b..46445f09d 100644 --- a/frontend/src/app/constants.ts +++ b/frontend/src/app/constants.ts @@ -167,6 +167,7 @@ export const ChartDataSectionFieldActionType = { ColorizeSingle: 'colorSingle', Size: 'size', DateLevel: 'dateLevel', + CustomizeSort: 'customizeSort', }; export const FilterRelationType = { diff --git a/frontend/src/app/hooks/useFieldActionModal.tsx b/frontend/src/app/hooks/useFieldActionModal.tsx index d1504139f..c3fb67426 100644 --- a/frontend/src/app/hooks/useFieldActionModal.tsx +++ b/frontend/src/app/hooks/useFieldActionModal.tsx @@ -56,6 +56,8 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { }; switch (actionType) { + case ChartDataSectionFieldActionType.CustomizeSort: + return ; case ChartDataSectionFieldActionType.Sortable: return ; case ChartDataSectionFieldActionType.Alias: @@ -97,11 +99,18 @@ function useFieldActionModal({ i18nPrefix }: I18NComponentProps) { aggregation?: boolean, ) => { const currentConfig = dataConfig.rows?.find(c => c.uid === columnUid); - let _modalSize = StateModalSize.MIDDLE; - if (actionType === ChartDataSectionFieldActionType.Colorize) { - _modalSize = StateModalSize.XSMALL; - } else if (actionType === ChartDataSectionFieldActionType.ColorizeSingle) { - _modalSize = StateModalSize.XSMALL; + let _modalSize: StateModalSize; + switch (actionType) { + case ChartDataSectionFieldActionType.Colorize: + case ChartDataSectionFieldActionType.ColorizeSingle: + case ChartDataSectionFieldActionType.Sortable: + _modalSize = StateModalSize.XSMALL; + break; + case ChartDataSectionFieldActionType.CustomizeSort: + _modalSize = StateModalSize.SMALL; + break; + default: + _modalSize = StateModalSize.MIDDLE; } return (show as Function)({ title: t(actionType), diff --git a/frontend/src/app/models/ChartDataRequestBuilder.ts b/frontend/src/app/models/ChartDataRequestBuilder.ts index 94b4ac2ec..5e002bd36 100644 --- a/frontend/src/app/models/ChartDataRequestBuilder.ts +++ b/frontend/src/app/models/ChartDataRequestBuilder.ts @@ -442,15 +442,24 @@ export class ChartDataRequestBuilder { return acc; }, []) .filter( - col => - col?.sort?.type && - [SortActionType.ASC, SortActionType.DESC].includes(col?.sort?.type), + col => { + const type = col?.sort?.type; + if (!type) { + return false; + } else if (type === SortActionType.Customize) { + const value = col.sort!.value; + return Array.isArray(value) && value.length > 0; + } else { + return [SortActionType.ASC, SortActionType.DESC].includes(type); + } + } ); const originalSorters = sortColumns.map(aggCol => ({ column: this.buildColumnName(aggCol), operator: aggCol.sort?.type!, aggOperator: aggCol.aggregate, + value: aggCol.sort?.type === SortActionType.Customize ? aggCol.sort.value! : undefined, })); const _extraSorters = this.extraSorters diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDataConfigSectionActionMenu.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDataConfigSectionActionMenu.tsx index 047a23640..b7e023802 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDataConfigSectionActionMenu.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDataConfigSectionActionMenu.tsx @@ -21,6 +21,8 @@ import SubMenu from 'antd/lib/menu/SubMenu'; import { ChartDataSectionFieldActionType, ChartDataViewFieldCategory, + DataViewFieldType, + SortActionType, } from 'app/constants'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; @@ -30,7 +32,7 @@ import { FC } from 'react'; import AggregationAction from '../ChartFieldAction/AggregationAction'; import AggregationLimitAction from '../ChartFieldAction/AggregationLimitAction'; import DateLevelAction from '../ChartFieldAction/DateLevelAction/DateLevelAction'; -import SortAction from '../ChartFieldAction/SortAction'; +import { SortAction } from '../ChartFieldAction/SortAction'; import { updateDataConfigByField } from './utils'; const ChartDataConfigSectionActionMenu: FC< @@ -53,12 +55,7 @@ const ChartDataConfigSectionActionMenu: FC< onConfigChanged, }) => { const t = useI18NPrefix(`viz.palette.data.enum.actionType`); - const subMenuAction = [ - ChartDataSectionFieldActionType.Sortable, - ChartDataSectionFieldActionType.Aggregate, - ChartDataSectionFieldActionType.AggregateLimit, - ChartDataSectionFieldActionType.DateLevel, - ]; + const handleFieldConfigChanged = ( columnUid: string, @@ -131,14 +128,20 @@ const ChartDataConfigSectionActionMenu: FC< } const options = config?.options?.[actionName]; if (actionName === ChartDataSectionFieldActionType.Sortable) { + const allowCustomSort = config?.allowFieldCustomizeSort && type !== DataViewFieldType.NUMERIC; return ( { - handleFieldConfigChanged(uid, config, needRefresh); + if (config.sort?.type === SortActionType.Customize) { + onOpenModal(uid)(ChartDataSectionFieldActionType.CustomizeSort); + } else { + handleFieldConfigChanged(uid, config, needRefresh); + } }} options={options} mode="menu" + allowCustomSort={allowCustomSort} /> ); } @@ -198,4 +201,10 @@ const ChartDataConfigSectionActionMenu: FC< ); }; +const subMenuAction = [ + ChartDataSectionFieldActionType.Sortable, + ChartDataSectionFieldActionType.Aggregate, + ChartDataSectionFieldActionType.AggregateLimit, + ChartDataSectionFieldActionType.DateLevel, +]; export default ChartDataConfigSectionActionMenu; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElementField.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElementField.tsx index d5bea3055..95479abf8 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElementField.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableElementField.tsx @@ -26,6 +26,7 @@ import { GroupOutlined, SortAscendingOutlined, SortDescendingOutlined, + DragOutlined, } from '@ant-design/icons'; import Dropdown from 'antd/lib/dropdown'; import { SortActionType } from 'app/constants'; @@ -137,6 +138,9 @@ const ChartDraggableElementField: FC<{ if (col.sort.type === SortActionType.DESC) { icons.push(); } + if (col.sort.type === SortActionType.Customize) { + icons.push(); + } } if (col.format) { icons.push(); diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx index 488eb5337..403d141d4 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/ChartDraggableTargetContainer.tsx @@ -21,6 +21,7 @@ import { ChartDataSectionType, ChartDataViewFieldCategory, DataViewFieldType, + SortActionType, } from 'app/constants'; import ChartDrillContext from 'app/contexts/ChartDrillContext'; import useFieldActionModal from 'app/hooks/useFieldActionModal'; @@ -29,7 +30,7 @@ import ChartDatasetContext from 'app/pages/ChartWorkbenchPage/contexts/ChartData import VizDataViewContext from 'app/pages/ChartWorkbenchPage/contexts/ChartDataViewContext'; import { ChartDataSectionField } from 'app/types/ChartConfig'; import { ChartDataConfigSectionProps } from 'app/types/ChartDataConfigSection'; -import { getColumnRenderName } from 'app/utils/chartHelper'; +import { getColumnRenderName, removeCustomizeSortConfig } from 'app/utils/chartHelper'; import { reachLowerBoundCount } from 'app/utils/internalChartHelper'; import { updateBy, updateByKey } from 'app/utils/mutation'; import { CHART_DRAG_ELEMENT_TYPE } from 'globalConstants'; @@ -51,7 +52,9 @@ import { uuidv4 } from 'utils/utils'; import ChartDraggableElement from './ChartDraggableElement'; import ChartDraggableElementField from './ChartDraggableElementField'; import ChartDraggableElementHierarchy from './ChartDraggableElementHierarchy'; -import { getDefaultAggregate, updateDataConfigByField } from './utils'; +import {getDefaultAggregate, isUpdate2CustomizeSort, updateDataConfigByField} from './utils'; +import { useSelector } from 'react-redux'; +import { chartConfigSelector } from 'app/pages/ChartWorkbenchPage/slice/selectors'; type DragItem = { index?: number; @@ -65,6 +68,7 @@ export const ChartDraggableTargetContainer: FC = translate: t = (...args) => args?.[0], onConfigChanged, }) { + const chartConfig = useSelector(chartConfigSelector); const { dataset } = useContext(ChartDatasetContext); const { drillOption } = useContext(ChartDrillContext); const { dataView, availableSourceFunctions } = @@ -357,12 +361,32 @@ export const ChartDraggableTargetContainer: FC = if (!fieldConfig) { return; } - const newConfig = updateDataConfigByField( + let _currentConfig = currentConfig; + // Check whether the sort configuration of field has been changed to custom sort + const _isUpdate2CustomizeSort = isUpdate2CustomizeSort(columnUid, _currentConfig, fieldConfig); + if (_isUpdate2CustomizeSort) { + // Only allows custom sorting on a single field + // case 1: Custom sort field already exists in the different config section + chartConfig?.datas?.forEach((item, index) => { + if (item.key !== _currentConfig.key + && item.allowFieldCustomizeSort + && item.rows?.some(r => r?.sort?.type === SortActionType.Customize) + ) { + // remove the custom sort configuration of field + onConfigChanged?.([index], removeCustomizeSortConfig(item), false); + } + }); + // case 2: Custom sort field already exists in the same config section + if (_currentConfig.rows?.some(r => r?.sort?.type === SortActionType.Customize)) { + _currentConfig = removeCustomizeSortConfig(_currentConfig); + } + } + _currentConfig = updateDataConfigByField( columnUid, currentConfig, fieldConfig, ); - onConfigChanged?.(ancestors, newConfig, needRefresh); + onConfigChanged?.(ancestors, _currentConfig, _isUpdate2CustomizeSort || needRefresh); }; const handleOpenActionModal = diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/utils.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/utils.ts index 1390b9682..95f5c07e1 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/utils.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartDraggable/utils.ts @@ -20,7 +20,7 @@ import { AggregateFieldSubAggregateType, ChartDataSectionFieldActionType, ChartDataSectionType, - ChartDataViewFieldCategory, + ChartDataViewFieldCategory, SortActionType, } from 'app/constants'; import { ChartDataConfig, ChartDataSectionField } from 'app/types/ChartConfig'; import { updateBy } from 'app/utils/mutation'; @@ -83,3 +83,13 @@ export const getDefaultAggregate = ( } } }; + +export const isUpdate2CustomizeSort = ( + uid: string, + config: ChartDataConfig, + field: ChartDataSectionField, +) => { + const originField = config?.rows?.find(r => r.uid === uid); + const type = field?.sort?.type; + return type === SortActionType.Customize && originField?.sort?.type !== type; +} diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/CustomizeSortAction/DraggableItem.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/CustomizeSortAction/DraggableItem.tsx new file mode 100644 index 000000000..2f604f9fd --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/CustomizeSortAction/DraggableItem.tsx @@ -0,0 +1,52 @@ +import React, { FC, useRef, } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import styled from 'styled-components/macro'; + +interface DraggableBodyRowProps extends React.HTMLAttributes { + index: number; + moveRow: (dragIndex: number, hoverIndex: number) => void; + canDrag: boolean; +} + +const DraggableItem: FC< + DraggableBodyRowProps +> = ({ + index, + moveRow, + canDrag, + ...restProps +}) => { + const ref = useRef(null); + const [, drop] = useDrop({ + accept: type, + drop: (item: { index: number }) => { + moveRow(item.index, index); + }, + }); + const [, drag] = useDrag({ + type, + item: { index }, + collect: monitor => ({ + isDragging: monitor.isDragging(), + }), + canDrag, + }); + drop(drag(ref)); + + return ( + + ); +}; + +const type = 'DraggableBodyRow'; +export default DraggableItem; + +const Tr = styled.tr<{ + canDrag: boolean; +}>` + cursor: ${p => (p.canDrag ? 'move' : 'no-drop')}; +`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/CustomizeSortAction/index.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/CustomizeSortAction/index.tsx new file mode 100644 index 000000000..e260579b0 --- /dev/null +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/CustomizeSortAction/index.tsx @@ -0,0 +1,115 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; +import { updateBy } from 'app/utils/mutation'; +import { Table } from "antd"; +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { getDistinctFields } from "app/utils/fetch"; +import ChartDataView from "app/types/ChartDataView"; +import DraggableItem from "./DraggableItem"; +import { ChartDataSectionField } from "app/types/ChartConfig"; +import { ColumnsType } from "antd/lib/table"; +import { SortActionType } from "app/constants"; + +type SortableField = { + name: string; +} +const COLUMNS: ColumnsType = [ + { + dataIndex: "name", + key: "name", + ellipsis: true, + } +]; + +const CustomizeSortAction: FC<{ + config: ChartDataSectionField; + dataView: ChartDataView | undefined; + onConfigChange: ( + config: ChartDataSectionField, + needRefresh?: boolean, + ) => void; +}> = ({ config, dataView, onConfigChange }) => { + const [loading, setLoading] = useState(false); + const [dataSource, setDataSource] = useState([]); + + const getViewData = useCallback(async (viewId: string, columns: string[], dataView: ChartDataView) => { + setLoading(true); + try { + const dataSet = await getDistinctFields( + viewId, + columns, + dataView, + undefined, + ); + const dataList = dataSet?.rows + .filter((row: any) => row?.[0] && (row[0] as string).trim()) + .map((item: string) => ({ + name: item[0] + })); + setDataSource(dataList || []); + } finally { + setLoading(false); + } + }, []); + + + const moveRow = useCallback((dragIndex: number, hoverIndex: number) => { + const newDataSource = updateBy(dataSource, draft => { + const dragCard = draft[dragIndex]; + draft.splice(dragIndex, 1); + draft.splice(hoverIndex, 0, dragCard); + return draft; + }); + const newConfig = updateBy(config, draft => { + draft.sort = { + type: SortActionType.Customize, + value: newDataSource.map(item => item.name), + } + }); + setDataSource(newDataSource); + onConfigChange?.(newConfig, true); + }, [dataSource]); + + useEffect(() => { + if (!dataView || !config) { + return; + } + const { type, value } = config.sort || {}; + if (type === SortActionType.Customize && value?.length) { + setDataSource(Array.from(value, item => ({ name: item } as SortableField))); + } else { + getViewData(dataView.id, [config.colName], dataView); + } + }, [dataView, config]); + const canDrag = useMemo(() => dataSource.length >= 1, [dataSource]); + return ( + + { + const attr = { + index, + moveRow, + canDrag, + }; + return attr as React.HTMLAttributes; + }} + pagination={false} + loading={loading} + scroll={{ + y: 480 + }} + bordered + showHeader={false} + /> + + ); +} + +export default CustomizeSortAction; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/DraggableContainer.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/DraggableContainer.tsx deleted file mode 100644 index 9ead85d45..000000000 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/DraggableContainer.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Datart - * - * Copyright 2021 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { updateBy } from 'app/utils/mutation'; -import { FC } from 'react'; -import styled from 'styled-components/macro'; -import { DraggableItem } from './DraggableItem'; - -interface Source { - id: number; - text: string; -} - -export const DraggableContainer: FC<{ source: Source[]; onChange }> = ({ - source, - onChange, -}) => { - const onDrop = (dragIndex: number, hoverIndex: number) => { - const newSource = updateBy(source, draft => { - const dragCard = draft[dragIndex]; - draft.splice(dragIndex, 1); - draft.splice(hoverIndex, 0, dragCard); - return draft; - }); - - onChange?.(newSource.map(s => s.text)); - }; - - const renderDraggableItem = (item: Source, index: number) => { - return ( - - ); - }; - - return ( - - {source.map((item, i) => renderDraggableItem(item, i))} - - ); -}; - -export default DraggableContainer; - -const StyledDiv = styled.div` - width: 100%; - border: 1px dashed gray; -`; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/DraggableItem.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/DraggableItem.tsx deleted file mode 100644 index c24b7ba8b..000000000 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/DraggableItem.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Datart - * - * Copyright 2021 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { XYCoord } from 'dnd-core'; -import { FC, useRef } from 'react'; -import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd'; - -const style = { - border: '1px solid gray', - padding: '0.5rem 1rem', - margin: '.5rem', - backgroundColor: 'white', - cursor: 'move', - fontSize: 12, -}; - -interface DraggableItemProps { - id: any; - text: string; - index: number; - moveCard?: (dragIndex: number, hoverIndex: number) => void; - onDrop?: (dragIndex: any, hoverIndex: any) => void; -} - -interface Item { - index: number; - id: string; - type?: string; -} - -export const DraggableItem: FC = ({ - id, - text, - index, - moveCard, - onDrop, -}) => { - const ref = useRef(null); - const [{ handlerId }, drop] = useDrop({ - accept: 'ItemTypes.Item', - collect(monitor) { - return { - handlerId: monitor.getHandlerId(), - }; - }, - hover(item: Item, monitor: DropTargetMonitor) { - if (!ref.current) { - return; - } - const dragIndex = item.index; - const hoverIndex = index; - - // Don't replace items with themselves - if (dragIndex === hoverIndex) { - return; - } - - // Determine rectangle on screen - const hoverBoundingRect = ref.current?.getBoundingClientRect(); - - // Get vertical middle - const hoverMiddleY = - (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; - - // Determine mouse position - const clientOffset = monitor.getClientOffset(); - - // Get pixels to the top - const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; - - // Only perform the move when the mouse has crossed half of the items height - // When dragging downwards, only move when the cursor is below 50% - // When dragging upwards, only move when the cursor is above 50% - - // Dragging downwards - if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { - return; - } - - // Dragging upwards - if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { - return; - } - - // Time to actually perform the action - moveCard && moveCard(dragIndex, hoverIndex); - - // Note: we're mutating the monitor item here! - // Generally it's better to avoid mutations, - // but it's good here for the sake of performance - // to avoid expensive index searches. - item.index = hoverIndex; - }, - drop(item: Item, monitor: DropTargetMonitor) { - onDrop && onDrop(item.id, item.index); - }, - }); - - const [{ isDragging }, drag] = useDrag({ - type: 'ItemTypes.Item', - item: () => { - return { id, index }; - }, - collect: (monitor: any) => ({ - isDragging: monitor.isDragging(), - }), - }); - - const opacity = isDragging ? 0 : 1; - drag(drop(ref)); - return ( -
- {text} -
- ); -}; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/index.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/index.tsx deleted file mode 100644 index b298438a1..000000000 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/DraggableList/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Datart - * - * Copyright 2021 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { DraggableContainer } from './DraggableContainer'; - -const DraggableList = props => { - return ( - - - - ); -}; - -export default DraggableList; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx index 28f858d85..17a77ed9c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/SortAction.tsx @@ -16,8 +16,8 @@ * limitations under the License. */ -import { CheckOutlined } from '@ant-design/icons'; -import { Col, Menu, Radio, Row, Space } from 'antd'; +import { CheckOutlined, InfoCircleOutlined } from '@ant-design/icons'; +import { Col, Menu, Radio, Row, Space, Tooltip } from 'antd'; import { SortActionType } from 'app/constants'; import useI18NPrefix from 'app/hooks/useI18NPrefix'; import { ChartDataSectionField } from 'app/types/ChartConfig'; @@ -36,7 +36,8 @@ const SortAction: FC<{ ) => void; mode?: 'menu'; options?; -}> = ({ config, dataset, mode, options, onConfigChange }) => { + allowCustomSort?: boolean; +}> = ({ config, dataset, mode, options, onConfigChange, allowCustomSort }) => { const actionNeedNewRequest = isEmpty(options?.backendSort) ? true : Boolean(options?.backendSort); @@ -47,23 +48,26 @@ const SortAction: FC<{ const handleSortTypeChange = direction => { setDirection(direction); - - if (SortActionType.Customize !== direction) { - onConfigChange && - onConfigChange( - updateBy(config, draft => { - draft.sort = { type: direction }; - }), - actionNeedNewRequest, - ); - } + onConfigChange?.( + updateBy(config, draft => { + draft.sort = { type: direction }; + }), + actionNeedNewRequest, + ); }; const renderOptions = mode => { if (mode === 'menu') { + let sortTypeOptions = BASE_SORT_OPTIONS; + if (allowCustomSort) { + sortTypeOptions = [ + ...sortTypeOptions, + SortActionType.Customize + ]; + } return ( <> - {[SortActionType.None, SortActionType.ASC, SortActionType.DESC].map( + {sortTypeOptions.map( sort => { return ( : ''} onClick={() => handleSortTypeChange(sort)} > - {t(`sort.${sort?.toLowerCase()}`)} + { + sort === SortActionType.Customize ? + ( + + {t(`sort.${sort?.toLowerCase()}`)} + + ) + : + t(`sort.${sort?.toLowerCase()}`) + } ); }, @@ -111,6 +124,7 @@ const SortAction: FC<{ return renderOptions(mode); }; +const BASE_SORT_OPTIONS = [SortActionType.None, SortActionType.ASC, SortActionType.DESC]; export default SortAction; const StyledRow = styled(Row)` diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/index.ts index 99d64fa0f..a23372e0c 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/SortAction/index.ts @@ -16,6 +16,6 @@ * limitations under the License. */ -import SortAction from './SortAction'; +export { default as SortAction } from './SortAction'; +export { default as CustomizeSortAction } from './CustomizeSortAction'; -export default SortAction; diff --git a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/index.ts b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/index.ts index f21362aa4..61b0b7d45 100644 --- a/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/index.ts +++ b/frontend/src/app/pages/ChartWorkbenchPage/components/ChartOperationPanel/components/ChartFieldAction/index.ts @@ -25,7 +25,7 @@ import ColorizeSingleAction from './ColorizeSingleAction'; import FilterActions from './FilterAction'; import NumberFormatAction from './NumberFormatAction'; import SizeOptionsAction from './SizeAction'; -import SortAction from './SortAction/SortAction'; +import { SortAction, CustomizeSortAction } from './SortAction'; const { FilterAction } = FilterActions; @@ -34,6 +34,7 @@ const actions = { AliasAction, NumberFormatAction, SortAction, + CustomizeSortAction, AggregationLimitAction, FilterAction, AggregationColorizeAction, diff --git a/frontend/src/app/types/ChartConfig.ts b/frontend/src/app/types/ChartConfig.ts index 787ecd00e..e18f15a61 100644 --- a/frontend/src/app/types/ChartConfig.ts +++ b/frontend/src/app/types/ChartConfig.ts @@ -194,6 +194,7 @@ export type ChartDataConfig = ChartConfigBase & { fieldRelation?: FilterCondition; // Runtime filters [RUNTIME_FILTER_KEY]?: PendingChartDataRequestFilter[]; + allowFieldCustomizeSort?: boolean; }; export type ChartStyleSectionTemplate = { diff --git a/frontend/src/app/utils/chartHelper.ts b/frontend/src/app/utils/chartHelper.ts index 5e4ab458e..85b7d639c 100644 --- a/frontend/src/app/utils/chartHelper.ts +++ b/frontend/src/app/utils/chartHelper.ts @@ -24,6 +24,7 @@ import { DataViewFieldType, FieldFormatType, RUNTIME_DATE_LEVEL_KEY, + SortActionType, } from 'app/constants'; import { ChartDataSet, ChartDataSetRow } from 'app/models/ChartDataSet'; import { DrillMode } from 'app/models/ChartDrillOption'; @@ -1882,3 +1883,25 @@ export function hasAggregationFunction(exp?: string) { AggregateFieldActionType.Sum, ].some(agg => new RegExp(`${agg}\\(`, 'i').test(exp || '')); } + +export function removeCustomizeSortConfig(config: ChartDataConfig): ChartDataConfig { + return updateBy(config, draft => { + draft.rows?.forEach(r => { + if (r?.sort?.type === SortActionType.Customize) { + delete r.sort; + } + }); + }); +} + +export function clearUnsupportedChartConfig(chartConfig: ChartConfig): ChartConfig { + return updateBy(chartConfig, draft => { + draft.datas = (draft?.datas || []).map(item => { + if (!item.allowFieldCustomizeSort) { + return removeCustomizeSortConfig(item); + } + return item; + }); + }); +}; + diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index eefa32b46..c7ca851cb 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -637,7 +637,8 @@ "none": "Default", "asc": "Ascending", "desc": "Descending", - "customize": "Customized" + "customize": "Customized", + "customizeTip": "Only allows custom sorting on a single non-numeric field" }, "format": { "title": "Format Setting", diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 546d7fddc..8748fecac 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -635,7 +635,8 @@ "none": "默认排序", "asc": "升序排列", "desc": "降序排列", - "customize": "自定义" + "customize": "自定义", + "customizeTip": "只允许对单个非数值型字段自定义排序" }, "format": { "title": "格式设置",