snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { LocaleKey } from '../../locale/types';
import type { ITableFilterColorItem, ITableFilterColorList } from '../../types';
import { LocaleService } from '@univerjs/core';
import { clsx, resetButtonClassName } from '@univerjs/design';
import { AIcon, BanIcon, CheckMarkIcon } from '@univerjs/icons';
import { useDependency } from '@univerjs/ui';
interface IMobileSheetTableColorFilterPanelProps {
colors: ITableFilterColorList;
onChange: (colors: ITableFilterColorList) => void;
}
function MobileColorOption(props: {
item: ITableFilterColorItem;
label: string;
textColor?: boolean;
onClick: () => void;
}) {
const { item, label, textColor = false, onClick } = props;
return (
<button
type="button"
aria-label={item.color ?? label}
aria-pressed={item.checked}
className={clsx(resetButtonClassName, `
univer-relative univer-flex univer-size-12 univer-items-center univer-justify-center univer-rounded-full
univer-border univer-border-solid univer-border-gray-200 univer-bg-gray-0
dark:!univer-border-gray-600 dark:!univer-bg-gray-800
`)}
style={!textColor && item.color ? { backgroundColor: item.color } : undefined}
onClick={onClick}
>
{!item.color && <BanIcon className="univer-size-7" />}
{textColor && item.color && <AIcon className="univer-size-7" style={{ color: item.color }} />}
{item.checked && (
<span
className="
univer-absolute -univer-bottom-1 -univer-right-1 univer-flex univer-size-5 univer-items-center
univer-justify-center univer-rounded-full univer-bg-primary-600 univer-text-gray-0
"
>
<CheckMarkIcon className="univer-size-3" />
</span>
)}
</button>
);
}
export function MobileSheetTableColorFilterPanel(props: IMobileSheetTableColorFilterPanelProps) {
const { colors, onChange } = props;
const localeService = useDependency(LocaleService);
const fillLabel = localeService.t<LocaleKey>('sheets-table-ui.filter.filter-by-cell-fill-color');
const textLabel = localeService.t<LocaleKey>('sheets-table-ui.filter.filter-by-cell-text-color');
const hasMultipleColors = colors.cellFillColors.length > 1 || colors.cellTextColors.length > 1;
function toggleColor(item: ITableFilterColorItem, fillColor: boolean) {
const key = fillColor ? 'cellFillColors' : 'cellTextColors';
const otherKey = fillColor ? 'cellTextColors' : 'cellFillColors';
onChange({
...colors,
[key]: colors[key].map((color) => color.color === item.color
? { ...color, checked: !color.checked }
: color),
[otherKey]: colors[otherKey].map((color) => ({ ...color, checked: false })),
});
}
return (
<div data-u-comp="mobile-sheet-table-filter-by-colors" className="univer-grid univer-gap-6">
{colors.cellFillColors.length > 1 && (
<section>
<h3 className="univer-m-0 univer-mb-4 univer-text-base univer-font-medium">{fillLabel}</h3>
<div className="univer-flex univer-flex-wrap univer-gap-4">
{colors.cellFillColors.map((item) => (
<MobileColorOption
key={item.color ?? 'none'}
item={item}
label={fillLabel}
onClick={() => toggleColor(item, true)}
/>
))}
</div>
</section>
)}
{colors.cellTextColors.length > 1 && (
<section>
<h3 className="univer-m-0 univer-mb-4 univer-text-base univer-font-medium">{textLabel}</h3>
<div className="univer-flex univer-flex-wrap univer-gap-4">
{colors.cellTextColors.map((item) => (
<MobileColorOption
key={item.color ?? 'none'}
item={item}
label={textLabel}
textColor
onClick={() => toggleColor(item, false)}
/>
))}
</div>
</section>
)}
{!hasMultipleColors && (
<div
className="
univer-flex univer-min-h-40 univer-items-center univer-justify-center univer-text-gray-500
"
>
{localeService.t<LocaleKey>('sheets-table-ui.filter.filter-by-color-none')}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,220 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ITableFilterItem } from '@univerjs/sheets-table';
import type { LocaleKey } from '../../locale/types';
import type {
IConditionCompareTypeEnum,
IConditionExpect,
IConditionInfo,
ITableConditionTypeEnumWithoutLogic,
} from './type';
import { Injector, LocaleService } from '@univerjs/core';
import {
borderClassName,
CascaderList,
clsx,
DatePicker,
DateRangePicker,
Input,
InputNumber,
MobileDropdown,
MobileSelect,
} from '@univerjs/design';
import { MoreDownIcon } from '@univerjs/icons';
import { TableConditionTypeEnum, TableDateCompareTypeEnum, TableStringCompareTypeEnum } from '@univerjs/sheets-table';
import { useDependency } from '@univerjs/ui';
import { useState } from 'react';
import { ConditionSubComponentEnum } from './type';
import { datePickerSet, getCascaderListOptions, getConditionDateSelect, getSubComponentType } from './util';
interface IMobileSheetTableConditionPanelProps {
unitId: string;
subUnitId: string;
tableFilter: ITableFilterItem | undefined;
tableId: string;
columnIndex: number;
conditionInfo: IConditionInfo;
onChange: (conditionInfo: IConditionInfo) => void;
}
export function MobileSheetTableConditionPanel(props: IMobileSheetTableConditionPanelProps) {
const { conditionInfo, onChange } = props;
const localeService = useDependency(LocaleService);
const injector = useDependency(Injector);
const [conditionVisible, setConditionVisible] = useState(false);
const [defaultDate] = useState(() => new Date());
const cascaderOptions = getCascaderListOptions(injector);
function handleConditionInfo(
info: IConditionExpect,
type?: ITableConditionTypeEnumWithoutLogic,
compare?: IConditionCompareTypeEnum
) {
onChange({
type: type ?? conditionInfo.type,
compare: compare ?? conditionInfo.compare,
info,
});
}
function handleChange(value: string[]) {
const type = value[0] as ITableConditionTypeEnumWithoutLogic;
const compare = value[1] as IConditionCompareTypeEnum;
if (compare) {
setConditionVisible(false);
}
const info: IConditionExpect = {};
if (type === TableConditionTypeEnum.Date) {
if (compare === TableDateCompareTypeEnum.Quarter) {
info.dateSelect = TableDateCompareTypeEnum.Q1;
} else if (compare === TableDateCompareTypeEnum.Month) {
info.dateSelect = TableDateCompareTypeEnum.M1;
} else if (datePickerSet.has(compare)) {
info.date = new Date();
} else {
info.dateRange = [new Date(), new Date()];
}
} else if (type === TableConditionTypeEnum.Number) {
info.number = 0;
} else if (type === TableConditionTypeEnum.String) {
info.string = '';
}
handleConditionInfo(info, type, compare ?? TableStringCompareTypeEnum.Equal);
}
const subComponentType = getSubComponentType(conditionInfo.type, conditionInfo.compare);
const selectType = conditionInfo.compare
? `${localeService.t(`sheets-table-ui.condition.${conditionInfo.type}`)} - ${localeService.t(`sheets-table-ui.${conditionInfo.type}.compare.${conditionInfo.compare}`)}`
: localeService.t(`sheets-table-ui.condition.${conditionInfo.type}`);
const conditionDateOptions = getConditionDateSelect(injector, conditionInfo.compare as TableDateCompareTypeEnum);
return (
<div>
<MobileDropdown
align="start"
open={conditionVisible}
onOpenChange={setConditionVisible}
overlay={(
<CascaderList
value={[conditionInfo.type, conditionInfo.compare!]}
options={cascaderOptions}
onChange={handleChange}
contentClassName="univer-flex-1"
wrapperClassName="!univer-h-[150px]"
/>
)}
>
<div
className={clsx(`
univer-box-border univer-flex univer-h-8 univer-w-full univer-items-center univer-justify-between
univer-rounded-md univer-bg-gray-0 univer-px-2 univer-text-sm univer-transition-colors
univer-duration-200
hover:univer-border-primary-600
focus:univer-border-primary-600 focus:univer-outline-none focus:univer-ring-2
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
`, borderClassName)}
>
<span>{selectType}</span>
<MoreDownIcon />
</div>
</MobileDropdown>
<div className="univer-mt-3 univer-w-full">
{subComponentType === ConditionSubComponentEnum.Input && (
<>
{conditionInfo.type === TableConditionTypeEnum.String
? (
<Input
className="univer-w-full"
placeholder={localeService.t<LocaleKey>('sheets-table-ui.filter.input-values-placeholder')}
value={conditionInfo.info.string}
onChange={(value) => handleConditionInfo({ string: value })}
/>
)
: (
<InputNumber
className="univer-h-7 univer-w-full"
value={conditionInfo.info.number}
controls={false}
onChange={(value) => {
if (value !== null) {
handleConditionInfo({ number: value });
}
}}
/>
)}
</>
)}
{subComponentType === ConditionSubComponentEnum.DatePicker && (
<div id="univer-table-date-picker-wrapper">
<DatePicker
className="univer-w-full"
value={conditionInfo.info.date ?? defaultDate}
onValueChange={(value) => handleConditionInfo({ date: value })}
/>
</div>
)}
{subComponentType === ConditionSubComponentEnum.DateRange && (
<div id="univer-table-date-range-wrapper">
<DateRangePicker
className="univer-w-full"
value={[
conditionInfo.info.dateRange?.[0] ?? defaultDate,
conditionInfo.info.dateRange?.[1] ?? defaultDate,
]}
onValueChange={(value) => handleConditionInfo(value ? { dateRange: value } : {})}
/>
</div>
)}
{subComponentType === ConditionSubComponentEnum.Inputs && (
<div className="univer-flex univer-items-center univer-gap-2">
<InputNumber
className="univer-w-full"
value={conditionInfo.info.numberRange?.[0]}
controls={false}
onChange={(value) => {
if (value !== null) {
handleConditionInfo({ numberRange: [value, conditionInfo.info.numberRange?.[1]] });
}
}}
/>
<span> - </span>
<InputNumber
className="univer-w-full"
value={conditionInfo.info.numberRange?.[1]}
controls={false}
onChange={(value) => {
if (value !== null) {
handleConditionInfo({ numberRange: [conditionInfo.info.numberRange?.[0], value] });
}
}}
/>
</div>
)}
{subComponentType === ConditionSubComponentEnum.Select && (
<MobileSelect
className="univer-w-full"
value={conditionInfo.info.dateSelect ?? conditionDateOptions[0].value}
options={conditionDateOptions}
onChange={(value) => handleConditionInfo({ dateSelect: value })}
/>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,398 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ISortRangeCommandParams } from '@univerjs/sheets-sort';
import type {
ITableColorFilterItem,
ITableConditionFilterItem,
ITableManualFilterItem,
} from '@univerjs/sheets-table';
import type { MobileDrawerSnap } from '@univerjs/ui';
import type { ITableFilterPanelInfo } from '../../controllers/sheet-table-component.controller';
import type { LocaleKey } from '../../locale/types';
import type { ITableFilterColorList } from '../../types';
import type { IConditionInfo } from './type';
import { generateRandomId, ICommandService, IPermissionService, IUndoRedoService, LocaleService } from '@univerjs/core';
import { Button, MobileActionRow, MobileActionRowGroup, resetButtonClassName } from '@univerjs/design';
import {
AscendingIcon,
DeleteColumnDoubleIcon,
DescendingIcon,
LeftInsertColumnDoubleIcon,
MoreLeftIcon,
MoreRightIcon,
RightInsertColumnDoubleIcon,
} from '@univerjs/icons';
import { WorkbookEditablePermission } from '@univerjs/sheets';
import { SortRangeCommand, SortType } from '@univerjs/sheets-sort';
import {
SetSheetTableSortStateCommand,
SheetsTableSortStateEnum,
SheetTableInsertColumnAtCommand,
SheetTableRemoveColumnAtCommand,
TABLE_FILTER_EMPTY_VALUE,
TableColumnFilterTypeEnum,
TableDateCompareTypeEnum,
TableManager,
} from '@univerjs/sheets-table';
import { MobileDrawer, useDependency } from '@univerjs/ui';
import { useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { SheetsTableMobileComponentController } from '../../controllers/mobile/sheet-table-component.controller';
import { SheetsTableUiService } from '../../services/sheets-table-ui.service';
import { FilterByEnum } from '../../types';
import { MobileSheetTableColorFilterPanel } from './MobileSheetTableColorFilterPanel';
import { MobileSheetTableConditionPanel } from './MobileSheetTableConditionPanel';
import { MobileSheetTableItemsFilterPanel } from './MobileSheetTableItemsFilterPanel';
import { getInitConditionInfo } from './util';
const FILTER_BY_OPTIONS: Array<{ label: LocaleKey; value: FilterByEnum }> = [
{ label: 'sheets-table-ui.filter.by-values', value: FilterByEnum.Items },
{ label: 'sheets-table-ui.filter.by-colors', value: FilterByEnum.Color },
{ label: 'sheets-table-ui.filter.by-conditions', value: FilterByEnum.Condition },
];
export function MobileSheetTableFilterPanel() {
const sheetsTableComponentController = useDependency(SheetsTableMobileComponentController);
const tableFilterPanelInfo = sheetsTableComponentController.getCurrentTableFilterInfo();
if (!tableFilterPanelInfo) {
return null;
}
return <MobileSheetTableFilterPanelContent tableFilterPanelInfo={tableFilterPanelInfo} />;
}
interface IMobileSheetTableFilterPanelContentProps {
tableFilterPanelInfo: ITableFilterPanelInfo;
}
function MobileSheetTableFilterPanelContent(props: IMobileSheetTableFilterPanelContentProps) {
const { tableFilterPanelInfo } = props;
const localeService = useDependency(LocaleService);
const tableUiService = useDependency(SheetsTableUiService);
const tableManager = useDependency(TableManager);
const commandService = useDependency(ICommandService);
const permissionService = useDependency(IPermissionService);
const undoRedoService = useDependency(IUndoRedoService);
const sheetsTableComponentController = useDependency(SheetsTableMobileComponentController);
const [detailFilterBy, setDetailFilterBy] = useState<FilterByEnum | null>(null);
const [drawerSnap, setDrawerSnap] = useState<MobileDrawerSnap>('expanded');
const layerRef = useRef<HTMLDivElement>(null);
const panelProps = tableUiService.getTableFilterPanelInitProps(
tableFilterPanelInfo.unitId,
tableFilterPanelInfo.subUnitId,
tableFilterPanelInfo.tableId,
tableFilterPanelInfo.column
);
const { unitId, subUnitId, tableId, tableFilter, columnIndex } = panelProps;
const currentTable = tableManager.getTable(unitId, tableId)!;
const items = tableUiService.getTableFilterItems(unitId, subUnitId, tableId, columnIndex);
const checkedItems = tableUiService.getTableFilterCheckedItems(unitId, tableId, columnIndex);
const [checkedItemSet, setCheckedItemSet] = useState<Set<string>>(() => new Set(
tableFilter?.filterType === TableColumnFilterTypeEnum.manual
? checkedItems
: items.data.map((item) => item.title)
));
const [conditionInfo, setConditionInfo] = useState<IConditionInfo>(
() => getInitConditionInfo(tableFilter) as IConditionInfo
);
const [colors, setColors] = useState<ITableFilterColorList>(
() => tableUiService.getTableFilterColors(unitId, subUnitId, tableId, columnIndex)
);
const tableRange = currentTable.getRange();
const canDeleteColumn = tableRange.endColumn > tableRange.startColumn;
const absoluteColumn = tableFilterPanelInfo.column;
const editable = permissionService.getPermissionPoint(new WorkbookEditablePermission(unitId).id)?.value;
function closePanel() {
sheetsTableComponentController.closeFilterPanel();
}
async function applySort(ascending: boolean) {
const range = currentTable.getTableFilterRange();
const undoRedoGroup = undoRedoService.beginUndoRedoGroup(unitId, generateRandomId(), 'append');
try {
const success = await commandService.executeCommand<ISortRangeCommandParams>(SortRangeCommand.id, {
unitId,
subUnitId,
range,
orderRules: [{ colIndex: columnIndex + range.startColumn, type: ascending ? SortType.ASC : SortType.DESC }],
hasTitle: false,
});
if (!success) {
return;
}
const sortStateSet = await commandService.executeCommand(SetSheetTableSortStateCommand.id, {
unitId,
tableId,
sortInfo: {
columnIndex,
sortState: ascending ? SheetsTableSortStateEnum.Asc : SheetsTableSortStateEnum.Desc,
},
});
if (sortStateSet) {
closePanel();
}
} finally {
undoRedoGroup.dispose();
}
}
async function insertColumn(side: 'left' | 'right') {
const success = await commandService.executeCommand(SheetTableInsertColumnAtCommand.id, {
unitId,
subUnitId,
tableId,
index: side === 'left' ? absoluteColumn : absoluteColumn + 1,
count: 1,
});
if (success) {
closePanel();
}
}
async function deleteColumn() {
if (!canDeleteColumn) {
return;
}
const success = await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
unitId,
subUnitId,
tableId,
index: absoluteColumn,
count: 1,
});
if (success) {
closePanel();
}
}
async function applyFilter() {
let filter: ITableColorFilterItem | ITableConditionFilterItem | ITableManualFilterItem | undefined;
if (detailFilterBy === FilterByEnum.Items) {
const emptyLabel = localeService.t<LocaleKey>('sheets-table-ui.condition.empty');
const values = items.data
.filter((item) => checkedItemSet.has(item.title))
.map((item) => item.title === emptyLabel ? TABLE_FILTER_EMPTY_VALUE : item.title);
filter = values.length === items.data.length
? undefined
: { filterType: TableColumnFilterTypeEnum.manual, values } satisfies ITableManualFilterItem;
} else if (detailFilterBy === FilterByEnum.Color) {
const cellFillColors = colors.cellFillColors.filter((item) => item.checked).map((item) => item.color);
const cellTextColors = colors.cellTextColors.filter((item) => item.checked).map((item) => item.color);
if (cellFillColors.length) {
filter = { filterType: TableColumnFilterTypeEnum.color, cellFillColors } satisfies ITableColorFilterItem;
} else if (cellTextColors.length) {
filter = { filterType: TableColumnFilterTypeEnum.color, cellTextColors } satisfies ITableColorFilterItem;
}
} else if (detailFilterBy === FilterByEnum.Condition) {
const dynamicCompare = conditionInfo.compare === TableDateCompareTypeEnum.Quarter
|| conditionInfo.compare === TableDateCompareTypeEnum.Month;
filter = {
filterType: TableColumnFilterTypeEnum.condition,
filterInfo: {
conditionType: conditionInfo.type,
compareType: dynamicCompare
? Object.values(conditionInfo.info)[0] as IConditionInfo['compare']
: conditionInfo.compare,
expectedValue: dynamicCompare ? undefined : Object.values(conditionInfo.info)[0],
} as ITableConditionFilterItem['filterInfo'],
} satisfies ITableConditionFilterItem;
} else {
return;
}
const success = await tableUiService.setTableFilter(unitId, tableId, columnIndex, filter);
if (success) {
closePanel();
}
}
async function clearFilter() {
const success = await tableUiService.setTableFilter(unitId, tableId, columnIndex, undefined);
if (success) {
closePanel();
}
}
function openDetail(filterBy: FilterByEnum) {
if (filterBy === FilterByEnum.Items) {
setCheckedItemSet(new Set(
tableFilter?.filterType === TableColumnFilterTypeEnum.manual
? checkedItems
: items.data.map((item) => item.title)
));
} else if (filterBy === FilterByEnum.Color) {
setColors(tableUiService.getTableFilterColors(unitId, subUnitId, tableId, columnIndex));
} else if (filterBy === FilterByEnum.Condition) {
setConditionInfo(getInitConditionInfo(tableFilter) as IConditionInfo);
}
setDetailFilterBy(filterBy);
setDrawerSnap('expanded');
}
function closeDetail() {
setDetailFilterBy(null);
}
const detailOption = FILTER_BY_OPTIONS.find((option) => option.value === detailFilterBy);
const detailTitle = detailOption ? localeService.t<LocaleKey>(detailOption.label) : '';
const detailDrawer = detailFilterBy
? createPortal(
<div ref={layerRef} className="univer-pointer-events-none univer-fixed univer-inset-0 univer-z-[1300]">
<MobileDrawer
layerRef={layerRef}
componentName="mobile-sheet-table-filter-detail"
openMode="push"
snap={drawerSnap}
expandLabel={localeService.t<LocaleKey>('sheets-table-ui.filter.resize')}
collapseLabel={localeService.t<LocaleKey>('sheets-table-ui.filter.resize')}
onSnapChange={setDrawerSnap}
onClose={closeDetail}
role="dialog"
ariaLabel={detailTitle}
panelClassName="univer-pointer-events-auto"
contentClassName="univer-min-h-0 univer-px-4 univer-pb-4"
header={(
<header
className="
univer-grid univer-h-12 univer-flex-1 univer-grid-cols-[48px_1fr_48px] univer-items-center
"
>
<button
type="button"
aria-label={localeService.t<LocaleKey>('sheets-table-ui.filter.back')}
className={`
${resetButtonClassName}
univer-flex univer-size-12 univer-items-center univer-justify-center
`}
onClick={closeDetail}
>
<MoreLeftIcon className="univer-size-6" />
</button>
<div className="univer-truncate univer-text-center univer-text-base univer-font-semibold">
{detailTitle}
</div>
</header>
)}
footer={(
<footer
className="
univer-shrink-0 univer-border-0 univer-border-t univer-border-solid univer-border-gray-200
univer-p-4
dark:!univer-border-gray-700
"
>
<MobileActionRowGroup>
<Button onClick={closeDetail}>
{localeService.t<LocaleKey>('sheets-table-ui.filter.cancel')}
</Button>
<Button variant="primary" onClick={() => applyFilter().catch(() => undefined)}>
{localeService.t<LocaleKey>('sheets-table-ui.filter.confirm')}
</Button>
</MobileActionRowGroup>
</footer>
)}
>
{detailFilterBy === FilterByEnum.Items && (
<MobileSheetTableItemsFilterPanel
items={items}
checkedItemSet={checkedItemSet}
setCheckedItemSet={setCheckedItemSet}
/>
)}
{detailFilterBy === FilterByEnum.Color && (
<MobileSheetTableColorFilterPanel colors={colors} onChange={setColors} />
)}
{detailFilterBy === FilterByEnum.Condition && (
<MobileSheetTableConditionPanel
tableFilter={tableFilter}
unitId={unitId}
subUnitId={subUnitId}
tableId={tableId}
columnIndex={columnIndex}
conditionInfo={conditionInfo}
onChange={setConditionInfo}
/>
)}
</MobileDrawer>
</div>,
document.body
)
: null;
return (
<>
<div className="univer-flex univer-flex-col univer-gap-2">
{editable && (
<>
<MobileActionRow
variant="subtle"
icon={<LeftInsertColumnDoubleIcon />}
title={localeService.t<LocaleKey>('sheets-table-ui.columnMenu.insert-left')}
onClick={() => insertColumn('left').catch(() => undefined)}
/>
<MobileActionRow
variant="subtle"
icon={<RightInsertColumnDoubleIcon />}
title={localeService.t<LocaleKey>('sheets-table-ui.columnMenu.insert-right')}
onClick={() => insertColumn('right').catch(() => undefined)}
/>
<MobileActionRow
variant="subtle"
icon={<DeleteColumnDoubleIcon />}
title={localeService.t<LocaleKey>('sheets-table-ui.columnMenu.delete')}
disabled={!canDeleteColumn}
onClick={() => deleteColumn().catch(() => undefined)}
/>
<MobileActionRow
variant="subtle"
icon={<AscendingIcon />}
title={localeService.t<LocaleKey>('sheets-table-ui.sort.sort-asc')}
onClick={() => applySort(true).catch(() => undefined)}
/>
<MobileActionRow
variant="subtle"
icon={<DescendingIcon />}
title={localeService.t<LocaleKey>('sheets-table-ui.sort.sort-desc')}
onClick={() => applySort(false).catch(() => undefined)}
/>
</>
)}
{FILTER_BY_OPTIONS.map((option) => (
<MobileActionRow
key={option.value}
variant="subtle"
title={localeService.t<LocaleKey>(option.label)}
trailing={<MoreRightIcon className="univer-size-5 univer-text-gray-400" />}
onClick={() => openDetail(option.value)}
/>
))}
<MobileActionRow
variant="subtle"
title={localeService.t<LocaleKey>('sheets-table-ui.filter.clear-filter')}
disabled={!tableFilter}
onClick={() => clearFilter().catch(() => undefined)}
/>
</div>
{detailDrawer}
</>
);
}

View File

@@ -0,0 +1,126 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { Dispatch, SetStateAction } from 'react';
import type { LocaleKey } from '../../locale/types';
import type { ITableFilterItemList } from '../../types';
import { LocaleService } from '@univerjs/core';
import { Checkbox, Input } from '@univerjs/design';
import { useDependency } from '@univerjs/ui';
import { useMemo, useState } from 'react';
interface IMobileSheetTableItemsFilterPanelProps {
items: ITableFilterItemList;
checkedItemSet: Set<string>;
setCheckedItemSet: Dispatch<SetStateAction<Set<string>>>;
}
export function MobileSheetTableItemsFilterPanel(props: IMobileSheetTableItemsFilterPanelProps) {
const { items, checkedItemSet, setCheckedItemSet } = props;
const localeService = useDependency(LocaleService);
const [searchText, setSearchText] = useState('');
const displayItems = useMemo(() => {
const normalizedSearchText = searchText.toLowerCase();
return normalizedSearchText
? items.data.filter((item) => String(item.title).toLowerCase().includes(normalizedSearchText))
: items.data;
}, [items.data, searchText]);
const allDisplayedChecked = displayItems.length > 0
&& displayItems.every((item) => checkedItemSet.has(item.title));
const someDisplayedChecked = displayItems.some((item) => checkedItemSet.has(item.title));
function toggleAllDisplayed() {
setCheckedItemSet((current) => {
const next = new Set(current);
for (const item of displayItems) {
if (allDisplayedChecked) {
next.delete(item.title);
} else {
next.add(item.title);
}
}
return next;
});
}
function toggleItem(title: string) {
setCheckedItemSet((current) => {
const next = new Set(current);
if (next.has(title)) {
next.delete(title);
} else {
next.add(title);
}
return next;
});
}
return (
<div data-u-comp="mobile-sheet-table-filter-by-values" className="univer-grid univer-gap-3">
<Input
value={searchText}
placeholder={localeService.t<LocaleKey>('sheets-table-ui.filter.search-placeholder')}
onChange={setSearchText}
/>
<div
className="
univer-overflow-hidden univer-rounded-xl univer-bg-gray-0
dark:!univer-bg-gray-800
"
>
<div
className="
univer-border-0 univer-border-b univer-border-solid univer-border-gray-200 univer-px-4
dark:!univer-border-gray-700
"
>
<Checkbox
className="univer-flex univer-min-h-12 univer-items-center"
checked={allDisplayedChecked}
indeterminate={!allDisplayedChecked && someDisplayedChecked}
disabled={displayItems.length === 0}
onChange={toggleAllDisplayed}
>
{localeService.t<LocaleKey>('sheets-table-ui.filter.select-all')}
</Checkbox>
</div>
{displayItems.map((item) => (
<div
key={item.key}
className="
univer-border-0 univer-border-b univer-border-solid univer-border-gray-200 univer-px-4
last:univer-border-b-0
dark:!univer-border-gray-700
"
>
<Checkbox
className="univer-flex univer-min-h-12 univer-items-center"
checked={checkedItemSet.has(item.title)}
onChange={() => toggleItem(item.title)}
>
<span className="univer-flex univer-min-w-0 univer-flex-1 univer-items-center univer-gap-2">
<span className="univer-min-w-0 univer-flex-1 univer-truncate">{item.title}</span>
<span className="univer-shrink-0 univer-text-gray-400">
{`(${items.itemsCountMap.get(item.title) ?? 0})`}
</span>
</span>
</Checkbox>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ISheetTableMenuExtraProps, SheetTableMenuAction } from './SheetTableMenu';
import { MobileActionRow } from '@univerjs/design';
const ACTIONS: SheetTableMenuAction[] = ['rename', 'update-range', 'set-theme', 'delete'];
export function MobileSheetTableMenu({ menu }: { menu?: ISheetTableMenuExtraProps }) {
if (!menu) {
return null;
}
return (
<div className="univer-flex univer-flex-col univer-gap-2">
{ACTIONS.map((action) => (
<MobileActionRow
key={action}
title={menu.labels[action]}
aria-label={menu.labels[action]}
variant="subtle"
onClick={() => menu.onSelect(action)}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ISheetTableRenameDialogProps } from './SheetTableRenameDialog';
import { MobileActionRowGroup } from '@univerjs/design';
import { SheetTableRenameDialog } from './SheetTableRenameDialog';
export function MobileSheetTableRenameDialog(props: ISheetTableRenameDialogProps) {
return <SheetTableRenameDialog {...props} ActionRowComponent={MobileActionRowGroup} />;
}

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ISheetTableSelectorProps } from './SheetTableSelector';
import { MobileActionRowGroup } from '@univerjs/design';
import { SheetTableSelector } from './SheetTableSelector';
export function MobileSheetTableSelector(props: ISheetTableSelectorProps) {
return <SheetTableSelector {...props} ActionRowComponent={MobileActionRowGroup} />;
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ISheetTableThemePanelProps } from './SheetTableThemePanel';
import { MobileColorPicker, MobileDropdown } from '@univerjs/design';
import { SheetTableThemePanel } from './SheetTableThemePanel';
export function MobileSheetTableThemePanel(props: ISheetTableThemePanelProps) {
return (
<SheetTableThemePanel
{...props}
ColorPickerComponent={MobileColorPicker}
DropdownComponent={MobileDropdown}
/>
);
}

View File

@@ -0,0 +1,123 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { LocaleKey } from '../../locale/types';
import type { ITableFilterColorItem, ITableFilterColorList } from '../../types';
import { LocaleService } from '@univerjs/core';
import { clsx, resetButtonClassName } from '@univerjs/design';
import { AIcon, BanIcon, SuccessIcon } from '@univerjs/icons';
import { useDependency } from '@univerjs/ui';
interface ISheetTableColorFilterPanelProps {
colors: ITableFilterColorList;
onChange: (colors: ITableFilterColorList) => void;
}
function ColorOption(props: {
item: ITableFilterColorItem;
label: string;
textColor?: boolean;
onClick: () => void;
}) {
const { item, label, textColor = false, onClick } = props;
return (
<button
type="button"
aria-label={item.color ?? label}
aria-pressed={item.checked}
className={clsx(resetButtonClassName, `
univer-relative univer-flex univer-size-6 univer-items-center univer-justify-center univer-rounded-full
univer-border univer-border-solid univer-border-gray-200 univer-bg-gray-0
hover:univer-ring-2 hover:univer-ring-offset-2
dark:!univer-border-gray-600 dark:!univer-bg-gray-800
`)}
style={!textColor && item.color ? { backgroundColor: item.color } : undefined}
onClick={onClick}
>
{!item.color && <BanIcon className="univer-size-5" />}
{textColor && item.color && <AIcon className="univer-size-5" style={{ color: item.color }} />}
{item.checked && (
<SuccessIcon
className="
univer-absolute -univer-bottom-1 -univer-right-1 univer-size-3 univer-rounded-full
univer-bg-gray-0 univer-text-green-600
"
/>
)}
</button>
);
}
export function SheetTableColorFilterPanel(props: ISheetTableColorFilterPanelProps) {
const { colors, onChange } = props;
const localeService = useDependency(LocaleService);
const fillLabel = localeService.t<LocaleKey>('sheets-table-ui.filter.filter-by-cell-fill-color');
const textLabel = localeService.t<LocaleKey>('sheets-table-ui.filter.filter-by-cell-text-color');
const hasMultipleColors = colors.cellFillColors.length > 1 || colors.cellTextColors.length > 1;
function toggleColor(item: ITableFilterColorItem, fillColor: boolean) {
const key = fillColor ? 'cellFillColors' : 'cellTextColors';
const otherKey = fillColor ? 'cellTextColors' : 'cellFillColors';
onChange({
...colors,
[key]: colors[key].map((color) => color.color === item.color
? { ...color, checked: !color.checked }
: color),
[otherKey]: colors[otherKey].map((color) => ({ ...color, checked: false })),
});
}
return (
<div className="univer-grid univer-h-full univer-gap-4 univer-overflow-auto univer-rounded-md univer-p-2">
{colors.cellFillColors.length > 1 && (
<section>
<h3 className="univer-m-0 univer-mb-2 univer-text-sm univer-font-normal">{fillLabel}</h3>
<div className="univer-grid univer-grid-cols-8 univer-gap-2">
{colors.cellFillColors.map((item) => (
<ColorOption
key={item.color ?? 'none'}
item={item}
label={fillLabel}
onClick={() => toggleColor(item, true)}
/>
))}
</div>
</section>
)}
{colors.cellTextColors.length > 1 && (
<section>
<h3 className="univer-m-0 univer-mb-2 univer-text-sm univer-font-normal">{textLabel}</h3>
<div className="univer-grid univer-grid-cols-8 univer-gap-2">
{colors.cellTextColors.map((item) => (
<ColorOption
key={item.color ?? 'none'}
item={item}
label={textLabel}
textColor
onClick={() => toggleColor(item, false)}
/>
))}
</div>
</section>
)}
{!hasMultipleColors && (
<div className="univer-flex univer-size-full univer-items-center univer-justify-center univer-text-sm">
{localeService.t<LocaleKey>('sheets-table-ui.filter.filter-by-color-none')}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,213 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ITableFilterItem } from '@univerjs/sheets-table';
import type { LocaleKey } from '../../locale/types';
import type { IConditionCompareTypeEnum, IConditionExpect, IConditionInfo, ITableConditionTypeEnumWithoutLogic } from './type';
import { Injector, LocaleService } from '@univerjs/core';
import { borderClassName, CascaderList, clsx, DatePicker, DateRangePicker, Dropdown, Input, InputNumber, Select } from '@univerjs/design';
import { MoreDownIcon } from '@univerjs/icons';
import { TableConditionTypeEnum, TableDateCompareTypeEnum, TableStringCompareTypeEnum } from '@univerjs/sheets-table';
import { useDependency } from '@univerjs/ui';
import { useState } from 'react';
import { ConditionSubComponentEnum } from './type';
import { datePickerSet, getCascaderListOptions, getConditionDateSelect, getSubComponentType } from './util';
interface IConditionFilterProps {
unitId: string;
subUnitId: string;
tableFilter: ITableFilterItem | undefined;
tableId: string;
columnIndex: number;
conditionInfo: IConditionInfo;
onChange: (conditionInfo: IConditionInfo) => void;
}
export const SheetTableConditionPanel = (props: IConditionFilterProps) => {
const { conditionInfo, onChange } = props;
const localeService = useDependency(LocaleService);
const [conditionVisible, setConditionVisible] = useState(false);
const injector = useDependency(Injector);
const cascaderOptions = getCascaderListOptions(injector);
const handleConditionInfo = (info: IConditionExpect, type?: ITableConditionTypeEnumWithoutLogic, compare?: IConditionCompareTypeEnum) => {
onChange({
type: type ?? conditionInfo.type,
compare: compare ?? conditionInfo.compare,
info,
});
};
const handleChange = (value: string[]) => {
const type = value[0] as ITableConditionTypeEnumWithoutLogic;
const compare = value[1] as IConditionCompareTypeEnum;
if (compare) {
setConditionVisible(false);
};
const info: IConditionExpect = {};
if (type === TableConditionTypeEnum.Date) {
if (compare === TableDateCompareTypeEnum.Quarter) {
info.dateSelect = TableDateCompareTypeEnum.Q1;
} else if (compare === TableDateCompareTypeEnum.Month) {
info.dateSelect = TableDateCompareTypeEnum.M1;
} else if (datePickerSet.has(compare)) {
info.date = new Date();
} else {
info.dateRange = [new Date(), new Date()];
}
} else if (type === TableConditionTypeEnum.Number) {
info.number = 0;
} else if (type === TableConditionTypeEnum.String) {
info.string = '';
}
handleConditionInfo(info, value[0] as ITableConditionTypeEnumWithoutLogic, (value[1] as IConditionCompareTypeEnum ?? TableStringCompareTypeEnum.Equal));
};
const subComponentType = getSubComponentType(conditionInfo.type, conditionInfo.compare);
let selectType = '';
if (conditionInfo.compare) {
selectType = `${localeService.t(`sheets-table-ui.condition.${conditionInfo.type}`)} - ${localeService.t(`sheets-table-ui.${conditionInfo.type}.compare.${conditionInfo.compare}`)}`;
} else {
selectType = localeService.t(`sheets-table-ui.condition.${conditionInfo.type}`);
}
const conditionDateOptions = getConditionDateSelect(injector, conditionInfo.compare as TableDateCompareTypeEnum);
return (
<div>
<Dropdown
align="start"
open={conditionVisible}
onOpenChange={setConditionVisible}
overlay={(
<CascaderList
value={[conditionInfo.type, conditionInfo.compare!]}
options={cascaderOptions}
onChange={handleChange}
contentClassName="univer-flex-1"
wrapperClassName="!univer-h-[150px]"
/>
)}
>
<div
className={clsx(`
univer-box-border univer-flex univer-h-8 univer-w-full univer-items-center univer-justify-between
univer-rounded-md univer-bg-gray-0 univer-px-2 univer-text-sm univer-transition-colors
univer-duration-200
hover:univer-border-primary-600
focus:univer-border-primary-600 focus:univer-outline-none focus:univer-ring-2
dark:!univer-bg-gray-700 dark:!univer-text-gray-0
`, borderClassName)}
>
<span>
{selectType}
</span>
<MoreDownIcon />
</div>
</Dropdown>
<div className="univer-mt-3 univer-w-full">
{subComponentType === ConditionSubComponentEnum.Input && (
<>
{conditionInfo.type === TableConditionTypeEnum.String
? (
<Input
className="univer-w-full"
placeholder={localeService.t<LocaleKey>('sheets-table-ui.filter.input-values-placeholder')}
value={conditionInfo.info.string}
onChange={(v) => handleConditionInfo({ string: v })}
/>
)
: (
<InputNumber
className="univer-h-7 univer-w-full"
value={conditionInfo.info.number}
controls={false}
onChange={(v) => {
if (v !== null) {
handleConditionInfo({ number: v });
}
}}
/>
)}
</>
)}
{!!(subComponentType === ConditionSubComponentEnum.DatePicker) && (
<div id="univer-table-date-picker-wrapper">
<DatePicker
className="univer-w-full"
value={conditionInfo.info.date ?? new Date()}
onValueChange={(v) => handleConditionInfo({ date: v })}
/>
</div>
)}
{!!(subComponentType === ConditionSubComponentEnum.DateRange) && (
<div id="univer-table-date-range-wrapper">
<DateRangePicker
className="univer-w-full"
value={[conditionInfo.info.dateRange?.[0] ?? new Date(), conditionInfo.info.dateRange?.[1] ?? new Date()]}
onValueChange={(v) => {
if (v) {
handleConditionInfo({ dateRange: v });
} else {
handleConditionInfo({});
}
}}
/>
</div>
)}
{subComponentType === ConditionSubComponentEnum.Inputs && (
<div className="univer-flex univer-items-center univer-gap-2">
<InputNumber
className="univer-w-full"
value={conditionInfo.info.numberRange?.[0]}
onChange={(v) => {
if (v !== null) {
handleConditionInfo({ numberRange: [v, conditionInfo.info.numberRange?.[1]] });
}
}}
controls={false}
/>
<span> - </span>
<InputNumber
className="univer-w-full"
value={conditionInfo.info.numberRange?.[1]}
controls={false}
onChange={(v) => {
if (v !== null) {
handleConditionInfo({ numberRange: [conditionInfo.info.numberRange?.[0], v] });
}
}}
/>
</div>
)}
{!!(subComponentType === ConditionSubComponentEnum.Select) && (
<Select
className="univer-w-full"
value={conditionInfo.info.dateSelect ?? conditionDateOptions[0].value}
options={conditionDateOptions}
onChange={(v) => handleConditionInfo({ dateSelect: v })}
/>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,383 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ISortRangeCommandParams } from '@univerjs/sheets-sort';
import type {
ITableColorFilterItem,
ITableConditionFilterItem,
ITableManualFilterItem,
} from '@univerjs/sheets-table';
import type { LocaleKey } from '../../locale/types';
import type { ITableFilterColorList } from '../../types';
import type { IConditionInfo } from './type';
import { generateRandomId, ICommandService, IPermissionService, IUndoRedoService, LocaleService } from '@univerjs/core';
import { ActionRow, Button, ButtonGroup, Segmented } from '@univerjs/design';
import {
AscendingIcon,
DeleteColumnDoubleIcon,
DescendingIcon,
LeftInsertColumnDoubleIcon,
RightInsertColumnDoubleIcon,
} from '@univerjs/icons';
import { WorkbookEditablePermission } from '@univerjs/sheets';
import { SortRangeCommand, SortType } from '@univerjs/sheets-sort';
import {
isManualTableFilter,
SetSheetTableSortStateCommand,
SheetsTableSortStateEnum,
SheetTableInsertColumnAtCommand,
SheetTableRemoveColumnAtCommand,
TABLE_FILTER_EMPTY_VALUE,
TableColumnFilterTypeEnum,
TableDateCompareTypeEnum,
TableManager,
} from '@univerjs/sheets-table';
import { useDependency } from '@univerjs/ui';
import { useState } from 'react';
import { SheetsTableComponentController } from '../../controllers/sheet-table-component.controller';
import { SheetsTableUiService } from '../../services/sheets-table-ui.service';
import { FilterByEnum } from '../../types';
import { SheetTableColorFilterPanel } from './SheetTableColorFilterPanel';
import { SheetTableConditionPanel } from './SheetTableConditionPanel';
import { SheetTableItemsFilterPanel } from './SheetTableItemsFilterPanel';
import { getInitConditionInfo } from './util';
const FILTER_BY_OPTIONS: Array<{ label: LocaleKey; value: FilterByEnum }> = [
{ label: 'sheets-table-ui.filter.by-values', value: FilterByEnum.Items },
{ label: 'sheets-table-ui.filter.by-colors', value: FilterByEnum.Color },
{ label: 'sheets-table-ui.filter.by-conditions', value: FilterByEnum.Condition },
];
export function SheetTableFilterPanel() {
const localeService = useDependency(LocaleService);
const filterByItems = FILTER_BY_OPTIONS.map((option) => ({ ...option, label: localeService.t(option.label) }));
const tableUiService = useDependency(SheetsTableUiService);
const tableManager = useDependency(TableManager);
const commandService = useDependency(ICommandService);
const permissionService = useDependency(IPermissionService);
const undoRedoService = useDependency(IUndoRedoService);
const sheetsTableComponentController = useDependency(SheetsTableComponentController);
const tableFilterPanelInfo = sheetsTableComponentController.getCurrentTableFilterInfo()!;
const panelProps = tableUiService.getTableFilterPanelInitProps(
tableFilterPanelInfo.unitId,
tableFilterPanelInfo.subUnitId,
tableFilterPanelInfo.tableId,
tableFilterPanelInfo.column
);
const { unitId, subUnitId, tableId, tableFilter, currentFilterBy, columnIndex } = panelProps;
const { data } = tableUiService.getTableFilterItems(unitId, subUnitId, tableId, columnIndex);
const checkedItems = tableUiService.getTableFilterCheckedItems(unitId, tableId, columnIndex);
const [checkedItemSet, setCheckedItemSet] = useState<Set<string>>(() => new Set<string>(
isManualTableFilter(tableFilter) ? checkedItems : data.map((item) => item.title)
));
const [filterBy, setFilterBy] = useState(currentFilterBy || FilterByEnum.Items);
const [conditionInfo, setConditionInfo] = useState<IConditionInfo>(() => {
const tableFilter = panelProps.tableFilter;
return getInitConditionInfo(tableFilter) as IConditionInfo;
});
const [colors, setColors] = useState<ITableFilterColorList>(
() => tableUiService.getTableFilterColors(unitId, subUnitId, tableId, columnIndex)
);
const table = tableManager.getTable(unitId, tableId);
if (!table) return null;
const tableRange = table.getRange();
const absoluteColumn = tableFilterPanelInfo.column;
const canDeleteColumn = tableRange.endColumn > tableRange.startColumn;
const closeDialog = (): void => {
sheetsTableComponentController.closeFilterPanel();
};
const onCancel = () => {
closeDialog();
};
const applySort = async (asc: boolean) => {
const range = table.getTableFilterRange();
const undoRedoGroup = undoRedoService.beginUndoRedoGroup(unitId, generateRandomId(), 'append');
try {
const success = await commandService.executeCommand<ISortRangeCommandParams>(SortRangeCommand.id, {
unitId,
subUnitId,
range,
orderRules: [{ colIndex: columnIndex + range.startColumn, type: asc ? SortType.ASC : SortType.DESC }],
hasTitle: false,
});
if (!success) {
return;
}
const sortStateSet = await commandService.executeCommand(SetSheetTableSortStateCommand.id, {
unitId,
tableId,
sortInfo: {
columnIndex,
sortState: asc ? SheetsTableSortStateEnum.Asc : SheetsTableSortStateEnum.Desc,
},
});
if (sortStateSet) {
closeDialog();
}
} finally {
undoRedoGroup.dispose();
}
};
const insertColumn = async (side: 'left' | 'right') => {
const success = await commandService.executeCommand(SheetTableInsertColumnAtCommand.id, {
unitId,
subUnitId,
tableId,
index: side === 'left' ? absoluteColumn : absoluteColumn + 1,
count: 1,
});
if (success) {
closeDialog();
}
};
const deleteColumn = async () => {
if (!canDeleteColumn) {
return;
}
const success = await commandService.executeCommand(SheetTableRemoveColumnAtCommand.id, {
unitId,
subUnitId,
tableId,
index: absoluteColumn,
count: 1,
});
if (success) {
closeDialog();
}
};
const onApply = async () => {
let filter: ITableColorFilterItem | ITableConditionFilterItem | ITableManualFilterItem | undefined;
if (filterBy === FilterByEnum.Items) {
// do items
const filteredItems: string[] = [];
const emptyLabel = localeService.t<LocaleKey>('sheets-table-ui.condition.empty');
for (const itemInfo of data) {
if (checkedItemSet.has(itemInfo.title)) {
filteredItems.push(itemInfo.title === emptyLabel ? TABLE_FILTER_EMPTY_VALUE : itemInfo.title);
}
}
const originFilter = table.getTableFilterColumn(columnIndex);
if (isManualTableFilter(originFilter)) {
const originValue = originFilter.values;
if (originValue.join(',') === filteredItems.join(',')) {
closeDialog();
return;
}
} else if (filteredItems.length === data.length && !originFilter) {
closeDialog();
return;
}
filter = filteredItems.length === data.length
? undefined
: {
filterType: TableColumnFilterTypeEnum.manual,
values: filteredItems,
};
} else if (filterBy === FilterByEnum.Color) {
const cellFillColors = colors.cellFillColors.filter((item) => item.checked).map((item) => item.color);
const cellTextColors = colors.cellTextColors.filter((item) => item.checked).map((item) => item.color);
if (cellFillColors.length) {
filter = { filterType: TableColumnFilterTypeEnum.color, cellFillColors };
} else if (cellTextColors.length) {
filter = { filterType: TableColumnFilterTypeEnum.color, cellTextColors };
}
} else {
let filterInfo;
if (conditionInfo.compare === TableDateCompareTypeEnum.Quarter || conditionInfo.compare === TableDateCompareTypeEnum.Month) {
filterInfo = {
conditionType: conditionInfo.type,
compareType: Object.values(conditionInfo.info)[0],
};
} else {
filterInfo = {
conditionType: conditionInfo.type,
compareType: conditionInfo.compare,
expectedValue: Object.values(conditionInfo.info)[0],
};
}
filter = {
filterType: TableColumnFilterTypeEnum.condition,
// @ts-ignore
filterInfo,
};
}
const success = await tableUiService.setTableFilter(unitId, tableId, columnIndex, filter);
if (success) {
closeDialog();
}
};
const onClearFilter = async () => {
const success = await tableUiService.setTableFilter(unitId, tableId, columnIndex, undefined);
if (success) {
closeDialog();
}
};
const workbookEditableId = new WorkbookEditablePermission(unitId).id;
const editable = permissionService.getPermissionPoint(workbookEditableId)?.value;
return (
<div
className={`
univer-box-border univer-flex univer-w-[400px] univer-flex-col univer-rounded-[10px] univer-bg-gray-0
univer-p-4 univer-shadow-lg
dark:!univer-border-gray-600 dark:!univer-bg-gray-700
`}
>
{editable && (
<>
<div
className={`
-univer-mx-4 -univer-mt-2 univer-mb-3 univer-border-0 univer-border-b univer-border-solid
univer-border-gray-200 univer-py-1
`}
>
<button
type="button"
className={`
univer-box-border univer-flex univer-h-10 univer-w-full univer-cursor-pointer
univer-items-center univer-gap-3 univer-border-none univer-bg-transparent univer-px-4
univer-text-left univer-text-sm univer-text-gray-900
hover:univer-bg-gray-100
disabled:univer-cursor-not-allowed disabled:univer-text-gray-400
dark:!univer-text-gray-0
dark:hover:!univer-bg-gray-600
`}
onClick={() => insertColumn('left').catch(() => undefined)}
>
<LeftInsertColumnDoubleIcon className="univer-size-5" extend={{ colorChannel1: 'var(--univer-primary-600)' }} />
<span>{localeService.t<LocaleKey>('sheets-table-ui.columnMenu.insert-left')}</span>
</button>
<button
type="button"
className={`
univer-box-border univer-flex univer-h-10 univer-w-full univer-cursor-pointer
univer-items-center univer-gap-3 univer-border-none univer-bg-transparent univer-px-4
univer-text-left univer-text-sm univer-text-gray-900
hover:univer-bg-gray-100
disabled:univer-cursor-not-allowed disabled:univer-text-gray-400
dark:!univer-text-gray-0
dark:hover:!univer-bg-gray-600
`}
onClick={() => insertColumn('right').catch(() => undefined)}
>
<RightInsertColumnDoubleIcon className="univer-size-5" extend={{ colorChannel1: 'var(--univer-primary-600)' }} />
<span>{localeService.t<LocaleKey>('sheets-table-ui.columnMenu.insert-right')}</span>
</button>
<button
type="button"
className={`
univer-box-border univer-flex univer-h-10 univer-w-full univer-cursor-pointer
univer-items-center univer-gap-3 univer-border-none univer-bg-transparent univer-px-4
univer-text-left univer-text-sm univer-text-gray-900
hover:univer-bg-gray-100
disabled:univer-cursor-not-allowed disabled:univer-text-gray-400
dark:!univer-text-gray-0
dark:hover:!univer-bg-gray-600
`}
disabled={!canDeleteColumn}
onClick={() => deleteColumn().catch(() => undefined)}
>
<DeleteColumnDoubleIcon className="univer-size-5" extend={{ colorChannel1: 'var(--univer-primary-600)' }} />
<span>{localeService.t<LocaleKey>('sheets-table-ui.columnMenu.delete')}</span>
</button>
</div>
<div className="univer-mb-3 univer-flex">
<ButtonGroup className="univer-mb-3 !univer-flex univer-w-full">
<Button className="univer-w-1/2" onClick={() => applySort(true).catch(() => undefined)}>
<AscendingIcon className="univer-mr-1" />
{localeService.t<LocaleKey>('sheets-table-ui.sort.sort-asc')}
</Button>
<Button className="univer-w-1/2" onClick={() => applySort(false).catch(() => undefined)}>
<DescendingIcon className="univer-mr-1" />
{localeService.t<LocaleKey>('sheets-table-ui.sort.sort-desc')}
</Button>
</ButtonGroup>
</div>
</>
)}
<div className="univer-w-full">
<Segmented
value={filterBy}
items={filterByItems}
onChange={(value) => setFilterBy(value as FilterByEnum)}
/>
</div>
<div className="univer-z-10 univer-h-60">
<div className="univer-mt-3 univer-size-full">
{filterBy === FilterByEnum.Items && (
<SheetTableItemsFilterPanel
tableFilter={tableFilter}
unitId={unitId}
subUnitId={subUnitId}
tableId={tableId}
columnIndex={columnIndex}
checkedItemSet={checkedItemSet}
setCheckedItemSet={setCheckedItemSet}
/>
)}
{filterBy === FilterByEnum.Color && (
<SheetTableColorFilterPanel colors={colors} onChange={setColors} />
)}
{filterBy === FilterByEnum.Condition && (
<SheetTableConditionPanel
tableFilter={tableFilter}
unitId={unitId}
subUnitId={subUnitId}
tableId={tableId}
columnIndex={columnIndex}
conditionInfo={conditionInfo}
onChange={setConditionInfo}
/>
)}
</div>
</div>
<ActionRow
className={`
univer-flex-wrap-nowrap univer-mt-4 univer-inline-flex univer-flex-shrink-0 univer-flex-grow-0
univer-justify-between univer-gap-6 univer-overflow-hidden
`}
>
<Button
disabled={tableFilter === undefined}
onClick={() => onClearFilter().catch(() => undefined)}
>
{localeService.t<LocaleKey>('sheets-table-ui.filter.clear-filter')}
</Button>
<ActionRow className="univer-flex univer-flex-1 univer-gap-x-2">
<Button onClick={onCancel}>{localeService.t<LocaleKey>('sheets-table-ui.filter.cancel')}</Button>
<Button variant="primary" onClick={() => onApply().catch(() => undefined)}>
{localeService.t<LocaleKey>('sheets-table-ui.filter.confirm')}
</Button>
</ActionRow>
</ActionRow>
</div>
);
}

View File

@@ -0,0 +1,185 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ITableFilterItem } from '@univerjs/sheets-table';
import type { Dispatch, SetStateAction } from 'react';
import type { LocaleKey } from '../../locale/types';
import { LocaleService } from '@univerjs/core';
import { borderClassName, Checkbox, clsx, Input, scrollbarClassName } from '@univerjs/design';
import { useDependency } from '@univerjs/ui';
import { useCallback, useMemo, useState } from 'react';
import { SheetsTableUiService } from '../../services/sheets-table-ui.service';
interface ISheetTableItemsFilterPanelProps {
unitId: string;
subUnitId: string;
tableFilter: ITableFilterItem | undefined;
tableId: string;
columnIndex: number;
checkedItemSet: Set<string>;
setCheckedItemSet: Dispatch<SetStateAction<Set<string>>>;
}
const getCheckedItemCount = (map: Map<string, number>, checkedItemSet: Set<string>) => {
let count = 0;
checkedItemSet.forEach((item) => {
count += map.get(item) ?? 0;
});
return count;
};
export function SheetTableItemsFilterPanel(props: ISheetTableItemsFilterPanelProps) {
const { unitId, tableId, subUnitId, columnIndex, checkedItemSet, setCheckedItemSet, tableFilter } = props;
const localeService = useDependency(LocaleService);
const tableService = useDependency(SheetsTableUiService);
const { data: items, itemsCountMap, allItemsCount } = tableService.getTableFilterItems(unitId, subUnitId, tableId, columnIndex);
// const allChecked = tableFilter === undefined ? true : checkedItemSet.size === itemsCountMap.size;
const [allChecked, setAllChecked] = useState<boolean>(tableFilter === undefined ? true : checkedItemSet.size === itemsCountMap.size);
const [checkedCount, setCheckedCount] = useState<number>(() => allChecked ? allItemsCount : getCheckedItemCount(itemsCountMap, checkedItemSet));
const indeterminate = !allChecked && checkedItemSet.size > 0;
const [searchText, setSearchText] = useState('');
const displayItems = useMemo(() => {
return searchText
? items.filter((item: { title: string }) => {
return String(item.title).toLowerCase().includes(searchText.toLowerCase());
})
: items;
}, [searchText, items]);
const onCheckAllToggled = useCallback(() => {
if (allChecked) {
checkedItemSet.clear();
setCheckedItemSet(new Set(checkedItemSet));
setAllChecked(false);
} else {
displayItems.forEach((item) => {
checkedItemSet.add(item.title);
});
setCheckedItemSet(new Set(checkedItemSet));
setAllChecked(true);
}
}, [allChecked]);
const onSearchValueChange = useCallback((str: string) => {
if (str === '') {
setAllChecked(true);
items.forEach((item) => {
checkedItemSet.add(item.title);
});
setCheckedCount(allItemsCount);
} else {
checkedItemSet.clear();
setAllChecked(false);
setCheckedCount(0);
}
setSearchText(str);
}, []);
const onCheckItemToggled = (key: string) => {
if (allChecked) {
setAllChecked(false);
const newSet = new Set<string>();
for (const { title } of items) {
if (key !== title) {
newSet.add(title);
}
}
setCheckedCount(allItemsCount - itemsCountMap.get(key)!);
setCheckedItemSet(newSet);
} else {
if (checkedItemSet.has(key)) {
checkedItemSet.delete(key);
setCheckedCount(checkedCount - itemsCountMap.get(key)!);
} else {
checkedItemSet.add(key);
setCheckedCount(checkedCount + itemsCountMap.get(key)!);
}
setCheckedItemSet(new Set(checkedItemSet));
}
};
return (
<div className="univer-flex univer-h-full univer-flex-col">
<Input autoFocus value={searchText} placeholder={localeService.t<LocaleKey>('sheets-table-ui.filter.search-placeholder')} onChange={onSearchValueChange} />
<div
className={clsx(`
univer-mt-2 univer-box-border univer-flex univer-h-[180px] univer-max-h-[180px] univer-flex-grow
univer-flex-col univer-overflow-hidden univer-rounded-md univer-py-1.5 univer-pl-2
`, borderClassName)}
>
<div
className={clsx('univer-h-40 univer-min-w-0 univer-overflow-y-auto univer-py-1 univer-pl-2', scrollbarClassName)}
>
<div className="univer-h-full">
<div className="univer-flex univer-items-center univer-px-2 univer-py-1">
<Checkbox
className="univer-min-w-0 univer-flex-1"
contentClassName="univer-flex-1 univer-min-w-0"
indeterminate={indeterminate}
disabled={items.length === 0}
checked={allChecked}
onChange={onCheckAllToggled}
>
<div className="univer-flex univer-h-5 univer-flex-1 univer-items-center univer-text-sm">
<span className="univer-flex-1 univer-truncate">{`${localeService.t<LocaleKey>('sheets-table-ui.filter.select-all')}`}</span>
<span className="univer-ml univer-text-gray-400">{`(${checkedCount}/${searchText ? displayItems.length : allItemsCount})`}</span>
</div>
</Checkbox>
</div>
{displayItems.map((item) => {
return (
<div
key={item.key}
className="univer-flex univer-items-center univer-px-2 univer-py-1"
>
<Checkbox
className="univer-min-w-0 univer-flex-1"
contentClassName="univer-flex-1 univer-min-w-0"
checked={allChecked || checkedItemSet.has(item.title)}
onChange={() => { onCheckItemToggled(item.title); }}
>
<span
className={`
univer-flex univer-h-5 univer-flex-1 univer-items-center univer-text-sm
`}
>
<span className="univer-flex-1 univer-truncate">
{item.title}
</span>
<span
className={`
univer-ml-1 univer-inline-flex univer-h-full univer-items-center
univer-text-gray-400
`}
>
{`(${itemsCountMap.get(item.title) || 0})`}
</span>
</span>
</Checkbox>
</div>
);
})}
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ICanvasPopup } from '@univerjs/sheets-ui';
import { DropdownMenu } from '@univerjs/design';
export type SheetTableMenuAction = 'rename' | 'update-range' | 'set-theme' | 'delete';
export interface ISheetTableMenuExtraProps {
anchorWidth: number;
tableName: string;
labels: Record<SheetTableMenuAction, string>;
onSelect: (action: SheetTableMenuAction) => void | Promise<void>;
onClose: () => void;
}
interface ISheetTableMenuProps {
popup: ICanvasPopup;
}
export function SheetTableMenu({ popup }: ISheetTableMenuProps) {
const menu = popup.extraProps as unknown as ISheetTableMenuExtraProps | undefined;
if (!menu) {
return null;
}
return (
<DropdownMenu
align="start"
className="univer-min-w-40"
items={[
{ type: 'item', children: menu.labels.rename, onSelect: () => menu.onSelect('rename') },
{ type: 'item', children: menu.labels['update-range'], onSelect: () => menu.onSelect('update-range') },
{ type: 'item', children: menu.labels['set-theme'], onSelect: () => menu.onSelect('set-theme') },
{ type: 'item', children: menu.labels.delete, variant: 'destructive', onSelect: () => menu.onSelect('delete') },
]}
open
sideOffset={0}
onOpenChange={(open) => {
if (!open) {
menu.onClose();
}
}}
>
<button
type="button"
aria-label={menu.tableName}
className="univer-block univer-h-px univer-opacity-0"
style={{ width: menu.anchorWidth }}
tabIndex={-1}
/>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { LocaleKey } from '../../locale/types';
import { ICommandService, IUniverInstanceService, LocaleService } from '@univerjs/core';
import { ActionRow, Button, Input } from '@univerjs/design';
import { IDefinedNamesService } from '@univerjs/engine-formula';
import {
getExistingNamesSet,
SetSheetTableCommand,
TableManager,
validateSheetTableName,
} from '@univerjs/sheets-table';
import { useDependency } from '@univerjs/ui';
import { useMemo, useState } from 'react';
export interface ISheetTableRenameDialogProps {
unitId: string;
tableId: string;
onClose: () => void;
ActionRowComponent?: typeof ActionRow;
}
export function SheetTableRenameDialog(props: ISheetTableRenameDialogProps) {
const { unitId, tableId, onClose, ActionRowComponent = ActionRow } = props;
const localeService = useDependency(LocaleService);
const commandService = useDependency(ICommandService);
const tableManager = useDependency<TableManager>(TableManager);
const univerInstanceService = useDependency(IUniverInstanceService);
const definedNamesService = useDependency<IDefinedNamesService>(IDefinedNamesService);
const table = tableManager.getTableById(unitId, tableId);
const [value, setValue] = useState(table?.getDisplayName() ?? '');
const [error, setError] = useState('');
const existingNames = useMemo(() => {
const names = getExistingNamesSet(unitId, {
univerInstanceService,
tableManager,
definedNamesService,
});
const currentName = table?.getDisplayName().toLowerCase();
if (currentName) {
names.delete(currentName);
}
return names;
}, [definedNamesService, table, tableManager, unitId, univerInstanceService]);
const handleConfirm = () => {
const nextName = value.trim();
if (!table || nextName === table.getDisplayName()) {
onClose();
return;
}
const validation = validateSheetTableName(nextName, existingNames);
if (!validation.valid) {
setError(localeService.t<LocaleKey>('sheets-table-ui.tableNameError'));
return;
}
commandService.executeCommand(SetSheetTableCommand.id, {
unitId,
tableId,
name: nextName,
});
onClose();
};
return (
<div
className={`
univer-box-border univer-flex univer-w-full univer-flex-col univer-gap-4 univer-pb-3 univer-pt-2
`}
>
<Input
size="middle"
value={value}
placeholder={localeService.t<LocaleKey>('sheets-table-ui.renamePlaceholder')}
onChange={(nextValue) => {
setValue(nextValue);
setError('');
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleConfirm();
}
}}
autoFocus
/>
{error ? <div className="-univer-mt-2 univer-text-sm univer-text-red-500">{error}</div> : null}
<ActionRowComponent
className="univer-flex univer-w-full univer-items-center univer-justify-end univer-gap-2"
>
<Button className="univer-min-w-16" onClick={onClose}>{localeService.t<LocaleKey>('sheets-table-ui.cancel')}</Button>
<Button className="univer-min-w-16" variant="primary" onClick={handleConfirm}>{localeService.t<LocaleKey>('sheets-table-ui.confirm')}</Button>
</ActionRowComponent>
</div>
);
}

View File

@@ -0,0 +1,142 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ITableSelectionInfo } from '../../commands/operations/open-table-selector.operation';
import type { LocaleKey } from '../../locale/types';
import { IUniverInstanceService, LocaleService, Rectangle } from '@univerjs/core';
import { ActionRow, Button } from '@univerjs/design';
import { deserializeRangeWithSheet, serializeRange } from '@univerjs/engine-formula';
import { getSheetCommandTarget } from '@univerjs/sheets';
import { RangeSelector } from '@univerjs/sheets-formula-ui';
import { TableManager } from '@univerjs/sheets-table';
import { useDependency } from '@univerjs/ui';
import { useState } from 'react';
export interface ISheetTableSelectorProps extends ITableSelectionInfo {
onConfirm: (info: ITableSelectionInfo) => void;
onCancel: () => void;
ActionRowComponent?: typeof ActionRow;
}
export const SheetTableSelector = (props: ISheetTableSelectorProps) => {
const { unitId, subUnitId, range, onCancel, onConfirm, tableId, ActionRowComponent = ActionRow } = props;
const tableManager = useDependency(TableManager);
const [selectedRange, setSelectedRange] = useState(range);
const [rangeError, setRangeError] = useState('');
const localeService = useDependency(LocaleService);
const univerInstanceService = useDependency(IUniverInstanceService);
return (
<>
<RangeSelector
maxRangeCount={1}
unitId={unitId}
subUnitId={subUnitId}
initialValue={serializeRange(range)}
onChange={(_, text) => {
const originValue = serializeRange(range);
const newRange = deserializeRangeWithSheet(text).range;
const target = getSheetCommandTarget(univerInstanceService, { unitId, subUnitId });
if (!target) {
return;
}
const worksheet = target.worksheet;
const merges = worksheet.getMergeData();
const hasOverlapWithMerge = merges.some((merge) => {
return Rectangle.intersects(newRange, merge);
});
if (hasOverlapWithMerge) {
setRangeError(localeService.t<LocaleKey>('sheets-table-ui.tableRangeWithMergeError'));
return;
}
const hasOverlapWithOtherTable = tableManager.getTablesBySubunitId(unitId, subUnitId).some((table) => {
if (table.getId() === tableId) {
return false;
}
const tableRange = table.getRange();
return Rectangle.intersects(newRange, tableRange);
});
if (hasOverlapWithOtherTable) {
setRangeError(localeService.t<LocaleKey>('sheets-table-ui.tableRangeWithOtherTableError'));
return;
}
const { startRow, endRow } = newRange;
const isSingleRow = startRow === endRow;
if (isSingleRow) {
setRangeError(localeService.t<LocaleKey>('sheets-table-ui.tableRangeSingleRowError'));
return;
}
if (originValue === text) {
return;
}
if (tableId) {
const table = tableManager.getTableById(unitId, tableId);
if (table) {
const oldRange = table.getRange();
if (Rectangle.intersects(newRange, oldRange) && oldRange.startRow === newRange.startRow) {
setSelectedRange(newRange);
setRangeError('');
onConfirm({
unitId,
subUnitId,
range: newRange,
});
return;
} else {
setRangeError(localeService.t<LocaleKey>('sheets-table-ui.updateError'));
return;
}
}
}
setSelectedRange(newRange);
setRangeError('');
}}
supportAcrossSheet={false}
/>
{rangeError && (
<div className="univer-mt-1 univer-text-xs univer-text-red-500">
{rangeError}
</div>
)}
<ActionRowComponent className="univer-mt-4 univer-flex univer-justify-end univer-gap-2">
<Button onClick={onCancel}>{localeService.t<LocaleKey>('sheets-table-ui.cancel')}</Button>
<Button
variant="primary"
onClick={() => {
if (rangeError) {
return;
}
onConfirm({
unitId,
subUnitId,
range: selectedRange,
});
}}
>
{localeService.t<LocaleKey>('sheets-table-ui.confirm')}
</Button>
</ActionRowComponent>
</>
);
};

View File

@@ -0,0 +1,431 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { IRangeThemeStyleItem, ISetRangeThemeMutationParams } from '@univerjs/sheets';
import type { IAddTableThemeCommandParams, ISetSheetTableCommandParams, ITableSetConfig } from '@univerjs/sheets-table';
import type { LocaleKey } from '../../locale/types';
import { ColorKit, ErrorService, ICommandService, LocaleService } from '@univerjs/core';
import { borderClassName, clsx, ColorPicker, Dropdown } from '@univerjs/design';
import { CloseIcon, DownIcon, IncreaseIcon } from '@univerjs/icons';
import { RangeThemeStyle, SetRangeThemeMutation, SheetRangeThemeModel } from '@univerjs/sheets';
import {
AddTableThemeCommand,
customEmptyThemeWithBorderStyle,
processStyleWithBorderStyle,
RemoveTableThemeCommand,
SetSheetTableCommand,
TableManager,
} from '@univerjs/sheets-table';
import { useDependency, useObservable } from '@univerjs/ui';
import { useEffect, useState } from 'react';
import {
TABLE_BORDER_DEFAULT,
TABLE_BORDER_NONE,
TABLE_CUSTOM_NAME_PREFIX,
TABLE_DEFAULT_BG_COLOR,
TABLE_DEFAULT_NAME_PREFIX,
} from '../../const';
import { SheetTableThemeUIController } from '../../controllers/sheet-table-theme-ui.controller';
export interface ISheetTableThemePanelProps {
unitId: string;
subUnitId: string;
tableId: string;
oldConfig: ITableSetConfig;
ColorPickerComponent?: typeof ColorPicker;
DropdownComponent?: typeof Dropdown;
}
interface ITableThemeStyle {
headerRowStyle?: IRangeThemeStyleItem;
firstRowStyle?: IRangeThemeStyleItem;
secondRowStyle?: IRangeThemeStyleItem;
lastRowStyle?: IRangeThemeStyleItem;
}
export const SheetTableThemePanel = (props: ISheetTableThemePanelProps) => {
const {
unitId,
subUnitId,
tableId,
ColorPickerComponent = ColorPicker,
DropdownComponent = Dropdown,
} = props;
const commandService = useDependency(ICommandService);
const localeService = useDependency(LocaleService);
const tableManager = useDependency(TableManager);
const table = tableManager.getTableById(unitId, tableId);
const rangeThemeModel = useDependency(SheetRangeThemeModel);
const sheetTableThemeUIController = useDependency(SheetTableThemeUIController);
const rangeThemeMapChanged = useObservable(rangeThemeModel.rangeThemeMapChange$);
const tableRefresh = useObservable(sheetTableThemeUIController.refreshTable$);
const errorService = useDependency(ErrorService);
const [, setRefresh] = useState(Math.random);
const themeConfig = useObservable(tableManager.tableThemeChanged$, {
theme: table?.getTableStyleId(),
oldTheme: table?.getTableStyleId(),
unitId,
subUnitId,
tableId,
});
const defaultRangeThemes = rangeThemeModel.getRegisteredRangeThemes().filter((item) => item?.startsWith(TABLE_DEFAULT_NAME_PREFIX));
const customRangeThemes = rangeThemeModel.getALLRegisteredTheme(unitId).filter((item) => item?.startsWith(TABLE_CUSTOM_NAME_PREFIX));
const selectedTheme = table?.getTableStyleId();
const customSelected = customRangeThemes.find((item) => item === selectedTheme);
const customStyleName = customSelected || customRangeThemes[0];
const customStyle = rangeThemeModel.getCustomRangeThemeStyle(unitId, customStyleName);
const headerBg = customStyle?.getHeaderRowStyle()?.bg?.rgb ?? TABLE_DEFAULT_BG_COLOR;
const firstRowBg = customStyle?.getFirstRowStyle()?.bg?.rgb ?? TABLE_DEFAULT_BG_COLOR;
const secondRowBg = customStyle?.getSecondRowStyle()?.bg?.rgb ?? TABLE_DEFAULT_BG_COLOR;
const lastRowBg = customStyle?.getLastRowStyle()?.bg?.rgb ?? TABLE_DEFAULT_BG_COLOR;
const direction = useObservable(localeService.direction$, localeService.getDirection());
const handleThemeChange = (theme: string) => {
commandService.executeCommand<ISetSheetTableCommandParams>(SetSheetTableCommand.id, {
unitId,
tableId,
theme,
});
};
const handleAddCustomTheme = () => {
if (customRangeThemes.length >= 11) {
errorService.emit(localeService.t<LocaleKey>('sheets-table-ui.customTooMore'));
return;
}
const lastCustomTheme = customRangeThemes[customRangeThemes.length - 1];
let newThemeName = `${TABLE_CUSTOM_NAME_PREFIX}1`;
if (lastCustomTheme) {
const index = Number(lastCustomTheme.split('-')[2]);
newThemeName = `${TABLE_CUSTOM_NAME_PREFIX}${index + 1}`;
}
const newTheme = new RangeThemeStyle(newThemeName, { ...customEmptyThemeWithBorderStyle });
commandService.executeCommand<IAddTableThemeCommandParams>(AddTableThemeCommand.id, {
unitId,
tableId,
themeStyle: newTheme,
});
};
const setCustomTheme = (themeName: string, tableThemeStyle: ITableThemeStyle) => {
commandService.executeCommand<ISetRangeThemeMutationParams>(SetRangeThemeMutation.id, {
unitId,
subUnitId,
styleName: themeName,
style: tableThemeStyle,
});
};
const removeCustomTheme = (themeName: string) => {
commandService.executeCommand(RemoveTableThemeCommand.id, {
unitId,
tableId,
themeName,
});
};
useEffect(() => {
setRefresh(Math.random());
}, [rangeThemeMapChanged, tableRefresh]);
if (!table) return null;
const headerBgIsDark = new ColorKit(headerBg).isDark();
const firstRowBgIsDark = new ColorKit(firstRowBg).isDark();
const secondRowBgIsDark = new ColorKit(secondRowBg).isDark();
const lastRowBgIsDark = new ColorKit(lastRowBg).isDark();
const customThemeRows = [
{
key: 'header',
label: localeService.t<LocaleKey>('sheets-table-ui.header'),
background: headerBg,
isDark: headerBgIsDark,
onChange: (value: string) => {
const headerRowStyle = processStyleWithBorderStyle('headerRowStyle', {
bg: { rgb: value },
cl: { rgb: new ColorKit(value).isDark() ? '#fff' : '#000' },
});
setCustomTheme(table.getTableStyleId(), { headerRowStyle });
},
},
{
key: 'first-row',
label: localeService.t<LocaleKey>('sheets-table-ui.firstLine'),
background: firstRowBg,
isDark: firstRowBgIsDark,
onChange: (value: string) => {
setCustomTheme(table.getTableStyleId(), {
firstRowStyle: {
bg: { rgb: value },
cl: { rgb: new ColorKit(value).isDark() ? '#fff' : '#000' },
},
});
},
},
{
key: 'second-row',
label: localeService.t<LocaleKey>('sheets-table-ui.secondLine'),
background: secondRowBg,
isDark: secondRowBgIsDark,
onChange: (value: string) => {
setCustomTheme(table.getTableStyleId(), {
secondRowStyle: {
bg: { rgb: value },
cl: { rgb: new ColorKit(value).isDark() ? '#fff' : '#000' },
},
});
},
},
{
key: 'last-row',
label: localeService.t<LocaleKey>('sheets-table-ui.footer'),
background: lastRowBg,
isDark: lastRowBgIsDark,
onChange: (value: string) => {
const lastRowStyle = processStyleWithBorderStyle('lastRowStyle', {
bg: { rgb: value },
cl: { rgb: new ColorKit(value).isDark() ? '#fff' : '#000' },
});
setCustomTheme(table.getTableStyleId(), { lastRowStyle });
},
},
];
return (
<div dir={direction} className="univer-flex univer-flex-col univer-gap-5 univer-pb-4">
<section className="univer-flex univer-flex-col univer-gap-2">
<h5
className="
univer-m-0 univer-text-xs univer-font-medium univer-text-gray-500
dark:!univer-text-gray-300
"
>
{localeService.t<LocaleKey>('sheets-table-ui.defaultStyle')}
</h5>
<div className="univer-grid univer-grid-cols-6 univer-gap-2">
{defaultRangeThemes.map((item) => {
const rangeThemeItem = rangeThemeModel.getDefaultRangeThemeStyle(item);
const rowBackgrounds = [
rangeThemeItem?.getHeaderRowStyle()?.bg?.rgb || TABLE_DEFAULT_BG_COLOR,
rangeThemeItem?.getFirstRowStyle()?.bg?.rgb || TABLE_DEFAULT_BG_COLOR,
rangeThemeItem?.getSecondRowStyle()?.bg?.rgb || TABLE_DEFAULT_BG_COLOR,
rangeThemeItem?.getLastRowStyle()?.bg?.rgb || TABLE_DEFAULT_BG_COLOR,
];
const selected = item === themeConfig.theme;
return (
<button
key={item}
type="button"
title={item}
aria-label={item}
aria-pressed={selected}
className={clsx(`
univer-flex univer-h-12 univer-w-full univer-cursor-pointer univer-flex-col
univer-overflow-hidden univer-rounded-lg univer-bg-gray-0 univer-p-1 univer-shadow-sm
univer-transition-all
hover:univer-border-gray-400 hover:univer-shadow-md
focus:univer-outline-none focus:univer-ring-2 focus:univer-ring-primary-200
dark:!univer-bg-gray-800
`, borderClassName, {
'!univer-border-primary-500 univer-ring-2 univer-ring-primary-100': selected,
})}
onClick={() => handleThemeChange(item)}
>
{rowBackgrounds.map((background, index) => (
<span
key={index}
className="univer-min-h-0 univer-w-full univer-flex-1"
style={{
background,
border: background ? TABLE_BORDER_NONE : TABLE_BORDER_DEFAULT,
}}
/>
))}
</button>
);
})}
</div>
</section>
<section className="univer-flex univer-flex-col univer-gap-2">
<h5
className="
univer-m-0 univer-text-xs univer-font-medium univer-text-gray-500
dark:!univer-text-gray-300
"
>
{localeService.t<LocaleKey>('sheets-table-ui.customStyle')}
</h5>
<div
className="
univer-grid univer-grid-cols-5 univer-gap-2 univer-rounded-lg univer-bg-gray-50 univer-p-2
dark:!univer-bg-gray-800
"
>
<button
type="button"
data-u-comp="sheet-table-theme-add"
aria-label={localeService.t<LocaleKey>('sheets-table-ui.setTheme')}
className="
univer-flex univer-h-12 univer-w-full univer-cursor-pointer univer-items-center
univer-justify-center univer-rounded-lg univer-border univer-border-dashed
univer-border-gray-300 univer-bg-gray-0 univer-p-0 univer-text-gray-500
univer-transition-colors
hover:univer-border-primary-500 hover:univer-text-primary-600
focus:univer-outline-none focus:univer-ring-2 focus:univer-ring-primary-200
dark:!univer-border-gray-600 dark:!univer-bg-gray-900 dark:!univer-text-gray-300
"
onClick={handleAddCustomTheme}
>
<IncreaseIcon className="univer-size-4" />
</button>
{customRangeThemes.map((item) => {
const rangeThemeItem = rangeThemeModel.getCustomRangeThemeStyle(unitId, item);
const rowBackgrounds = [
rangeThemeItem?.getHeaderRowStyle()?.bg?.rgb,
rangeThemeItem?.getFirstRowStyle()?.bg?.rgb,
rangeThemeItem?.getSecondRowStyle()?.bg?.rgb,
rangeThemeItem?.getLastRowStyle()?.bg?.rgb,
];
const selected = item === themeConfig.theme;
return (
<div key={item} className="univer-group univer-relative univer-h-12 univer-min-w-0">
<button
type="button"
title={item}
aria-label={item}
aria-pressed={selected}
className={clsx(`
univer-flex univer-size-full univer-cursor-pointer univer-flex-col
univer-overflow-hidden univer-rounded-lg univer-bg-gray-0 univer-p-1
univer-shadow-sm univer-transition-all
hover:univer-border-gray-400 hover:univer-shadow-md
focus:univer-outline-none focus:univer-ring-2 focus:univer-ring-primary-200
dark:!univer-bg-gray-900
`, borderClassName, {
'!univer-border-primary-500 univer-ring-2 univer-ring-primary-100': selected,
})}
onClick={() => handleThemeChange(item)}
>
{rowBackgrounds.map((background, index) => (
<span
key={index}
className="univer-min-h-0 univer-w-full univer-flex-1"
style={{
background: background ?? TABLE_BORDER_NONE,
border: background ? TABLE_BORDER_NONE : TABLE_BORDER_DEFAULT,
}}
/>
))}
</button>
<button
type="button"
data-u-comp="sheet-table-theme-remove"
aria-label={`${localeService.t<LocaleKey>('sheets-table-ui.customStyle')} ${item}`}
className={clsx(`
univer-absolute univer-right-[-6px] univer-top-[-6px] univer-z-10 univer-flex
univer-size-5 univer-cursor-pointer univer-items-center univer-justify-center
univer-rounded-full univer-border univer-border-solid univer-border-gray-200
univer-bg-gray-0 univer-p-0 univer-text-gray-500 univer-shadow-sm
univer-transition-all
hover:univer-border-red-200 hover:univer-bg-red-50 hover:univer-text-red-600
focus:univer-opacity-100 focus:univer-outline-none focus:univer-ring-2
focus:univer-ring-primary-200
rtl:univer-left-[-6px] rtl:univer-right-auto
dark:!univer-border-gray-600 dark:!univer-bg-gray-900
`, {
'univer-opacity-100': selected,
'univer-opacity-0 group-focus-within:univer-opacity-100 group-hover:univer-opacity-100': !selected,
})}
onClick={(event) => {
event.stopPropagation();
removeCustomTheme(item);
}}
>
<CloseIcon className="univer-size-3" />
</button>
</div>
);
})}
</div>
{customSelected && (
<div
className="
univer-flex univer-flex-col univer-gap-2 univer-rounded-lg univer-bg-gray-50 univer-p-3
dark:!univer-bg-gray-800
"
>
{customThemeRows.map((row) => (
<div key={row.key} className="univer-flex univer-h-10 univer-min-w-0 univer-gap-2">
<div
className={clsx(`
univer-flex univer-min-w-0 univer-flex-1 univer-items-center univer-rounded-md
univer-px-3 univer-text-sm univer-font-medium
rtl:univer-text-right
`, borderClassName, {
'univer-text-gray-0': row.isDark,
'univer-text-gray-900': !row.isDark,
})}
style={{ background: row.background }}
>
<span className="univer-truncate">{row.label}</span>
</div>
<DropdownComponent
overlay={(
<div dir={direction} className="univer-p-2">
<ColorPickerComponent
value={new ColorKit(row.background).toHexString()}
onChange={row.onChange}
/>
</div>
)}
>
<button
type="button"
aria-label={`${row.label} ${localeService.t<LocaleKey>('sheets-table-ui.setTheme')}`}
className={clsx(`
univer-flex univer-h-10 univer-cursor-pointer univer-items-center univer-gap-2
univer-rounded-md univer-bg-gray-0 univer-px-2 univer-text-gray-600
univer-transition-colors
hover:univer-bg-gray-100
focus:univer-outline-none focus:univer-ring-2 focus:univer-ring-primary-200
dark:!univer-bg-gray-900 dark:!univer-text-gray-300
`, borderClassName)}
>
<span
className={clsx('univer-size-4 univer-rounded-full', borderClassName)}
style={{ background: row.background }}
/>
<DownIcon className="univer-size-3" />
</button>
</DropdownComponent>
</div>
))}
</div>
)}
</section>
</div>
);
};

View File

@@ -0,0 +1,94 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { ITableFilterItem } from '@univerjs/sheets-table';
import {
TableColumnFilterTypeEnum,
TableConditionTypeEnum,
TableDateCompareTypeEnum,
TableNumberCompareTypeEnum,
TableStringCompareTypeEnum,
} from '@univerjs/sheets-table';
import { describe, expect, it } from 'vitest';
import { ConditionSubComponentEnum } from '../type';
import { getInitConditionInfo, getSubComponentType } from '../util';
describe('sheet table condition utilities', () => {
it('initializes a new condition filter as a text equality rule', () => {
expect(getInitConditionInfo()).toEqual({
type: TableConditionTypeEnum.String,
compare: TableStringCompareTypeEnum.Equal,
info: {},
});
});
it('maps condition compare types to the editor control needed by users', () => {
expect(getSubComponentType(TableConditionTypeEnum.String, TableStringCompareTypeEnum.Contains)).toBe(ConditionSubComponentEnum.Input);
expect(getSubComponentType(TableConditionTypeEnum.Number, TableNumberCompareTypeEnum.Between)).toBe(ConditionSubComponentEnum.Inputs);
expect(getSubComponentType(TableConditionTypeEnum.Date, TableDateCompareTypeEnum.Between)).toBe(ConditionSubComponentEnum.DateRange);
expect(getSubComponentType(TableConditionTypeEnum.Date, TableDateCompareTypeEnum.Quarter)).toBe(ConditionSubComponentEnum.Select);
expect(getSubComponentType(TableConditionTypeEnum.Date, TableDateCompareTypeEnum.After)).toBe(ConditionSubComponentEnum.DatePicker);
expect(getSubComponentType(TableConditionTypeEnum.Date, TableDateCompareTypeEnum.Today)).toBe(ConditionSubComponentEnum.None);
});
it('hydrates saved date filters back into panel state without losing relative date choices', () => {
const absoluteDateFilter: ITableFilterItem = {
filterType: TableColumnFilterTypeEnum.condition,
filterInfo: {
conditionType: TableConditionTypeEnum.Date,
compareType: TableDateCompareTypeEnum.Between,
expectedValue: ['2026-01-01', '2026-01-31'],
},
};
const quarterFilter: ITableFilterItem = {
filterType: TableColumnFilterTypeEnum.condition,
filterInfo: {
conditionType: TableConditionTypeEnum.Date,
compareType: TableDateCompareTypeEnum.Q3,
},
};
const relativeDateFilter: ITableFilterItem = {
filterType: TableColumnFilterTypeEnum.condition,
filterInfo: {
conditionType: TableConditionTypeEnum.Date,
compareType: TableDateCompareTypeEnum.ThisMonth,
},
};
expect(getInitConditionInfo(absoluteDateFilter)).toMatchObject({
type: TableConditionTypeEnum.Date,
compare: TableDateCompareTypeEnum.Between,
info: {
dateRange: [
new Date('2026-01-01'),
new Date('2026-01-31'),
],
},
});
expect(getInitConditionInfo(quarterFilter)).toEqual({
type: TableConditionTypeEnum.Date,
compare: TableDateCompareTypeEnum.Quarter,
info: {
dateSelect: TableDateCompareTypeEnum.Q3,
},
});
expect(getInitConditionInfo(relativeDateFilter)).toEqual({
type: TableConditionTypeEnum.Date,
compare: TableDateCompareTypeEnum.ThisMonth,
info: {},
});
});
});

View File

@@ -0,0 +1,45 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { TableConditionTypeEnum, TableDateCompareTypeEnum, TableNumberCompareTypeEnum, TableStringCompareTypeEnum } from '@univerjs/sheets-table';
export enum ConditionSubComponentEnum {
DatePicker = 'DatePicker',
DateRange = 'DateRange',
Input = 'Input',
Inputs = 'Inputs',
Select = 'Select',
None = 'None',
}
export type IConditionCompareTypeEnum = TableStringCompareTypeEnum | TableDateCompareTypeEnum | TableNumberCompareTypeEnum;
export type ITableConditionTypeEnumWithoutLogic = Exclude<TableConditionTypeEnum, TableConditionTypeEnum.Logic>;
export interface IConditionInfo {
type: ITableConditionTypeEnumWithoutLogic;
compare: IConditionCompareTypeEnum;
info: IConditionExpect;
}
export interface IConditionExpect {
date?: Date;
dateRange?: [Date, Date];
dateSelect?: string;
number?: number;
numberRange?: [number?, number?];
string?: string;
}

View File

@@ -0,0 +1,446 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* 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 type { Injector } from '@univerjs/core';
import type { ITableFilterItem } from '@univerjs/sheets-table';
import type { LocaleKey } from '../../locale/types';
import type { IConditionCompareTypeEnum } from './type';
import { LocaleService } from '@univerjs/core';
import {
TableColumnFilterTypeEnum,
TableConditionTypeEnum,
TableDateCompareTypeEnum,
TableNumberCompareTypeEnum,
TableStringCompareTypeEnum,
} from '@univerjs/sheets-table';
import { ConditionSubComponentEnum } from './type';
export function getCascaderListOptions(injector: Injector) {
const localeService = injector.get(LocaleService);
return [
{
value: TableConditionTypeEnum.String,
label: localeService.t<LocaleKey>(`sheets-table-ui.condition.${TableConditionTypeEnum.String}`),
children: [
{
value: TableStringCompareTypeEnum.Equal,
label: localeService.t<LocaleKey>(`sheets-table-ui.string.compare.${TableStringCompareTypeEnum.Equal}`),
},
{
value: TableStringCompareTypeEnum.NotEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.string.compare.${TableStringCompareTypeEnum.NotEqual}`),
},
{
value: TableStringCompareTypeEnum.Contains,
label: localeService.t<LocaleKey>(`sheets-table-ui.string.compare.${TableStringCompareTypeEnum.Contains}`),
},
{
value: TableStringCompareTypeEnum.NotContains,
label: localeService.t<LocaleKey>(`sheets-table-ui.string.compare.${TableStringCompareTypeEnum.NotContains}`),
},
{
value: TableStringCompareTypeEnum.StartsWith,
label: localeService.t<LocaleKey>(`sheets-table-ui.string.compare.${TableStringCompareTypeEnum.StartsWith}`),
},
{
value: TableStringCompareTypeEnum.EndsWith,
label: localeService.t<LocaleKey>(`sheets-table-ui.string.compare.${TableStringCompareTypeEnum.EndsWith}`),
},
],
},
{
value: TableConditionTypeEnum.Number,
label: localeService.t<LocaleKey>(`sheets-table-ui.condition.${TableConditionTypeEnum.Number}`),
children: [
{
value: TableNumberCompareTypeEnum.Equal,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.Equal}`),
},
{
value: TableNumberCompareTypeEnum.NotEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.NotEqual}`),
},
{
value: TableNumberCompareTypeEnum.GreaterThan,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.GreaterThan}`),
},
{
value: TableNumberCompareTypeEnum.GreaterThanOrEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.GreaterThanOrEqual}`),
},
{
value: TableNumberCompareTypeEnum.LessThan,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.LessThan}`),
},
{
value: TableNumberCompareTypeEnum.LessThanOrEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.LessThanOrEqual}`),
},
{
value: TableNumberCompareTypeEnum.Between,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.Between}`),
},
{
value: TableNumberCompareTypeEnum.NotBetween,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.NotBetween}`),
},
{
value: TableNumberCompareTypeEnum.Above,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.Above}`),
},
{
value: TableNumberCompareTypeEnum.Below,
label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.Below}`),
},
// {
// value: TableNumberCompareTypeEnum.TopN,
// label: localeService.t<LocaleKey>(`sheets-table-ui.number.compare.${TableNumberCompareTypeEnum.TopN}`),
// },
],
},
{
value: TableConditionTypeEnum.Date,
label: localeService.t<LocaleKey>(`sheets-table-ui.condition.${TableConditionTypeEnum.Date}`),
children: [
{
value: TableDateCompareTypeEnum.Equal,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Equal}`),
},
{
value: TableDateCompareTypeEnum.NotEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.NotEqual}`),
},
{
value: TableDateCompareTypeEnum.After,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.After}`),
},
{
value: TableDateCompareTypeEnum.AfterOrEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.AfterOrEqual}`),
},
{
value: TableDateCompareTypeEnum.Before,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Before}`),
},
{
value: TableDateCompareTypeEnum.BeforeOrEqual,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.BeforeOrEqual}`),
},
{
value: TableDateCompareTypeEnum.Between,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Between}`),
},
{
value: TableDateCompareTypeEnum.NotBetween,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.NotBetween}`),
},
{
value: TableDateCompareTypeEnum.Today,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Today}`),
},
{
value: TableDateCompareTypeEnum.Yesterday,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Yesterday}`),
},
{
value: TableDateCompareTypeEnum.Tomorrow,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Tomorrow}`),
},
{
value: TableDateCompareTypeEnum.ThisWeek,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.ThisWeek}`),
},
{
value: TableDateCompareTypeEnum.LastWeek,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.LastWeek}`),
},
{
value: TableDateCompareTypeEnum.NextWeek,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.NextWeek}`),
},
{
value: TableDateCompareTypeEnum.ThisMonth,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.ThisMonth}`),
},
{
value: TableDateCompareTypeEnum.LastMonth,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.LastMonth}`),
},
{
value: TableDateCompareTypeEnum.NextMonth,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.NextMonth}`),
},
{
value: TableDateCompareTypeEnum.ThisYear,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.ThisYear}`),
},
{
value: TableDateCompareTypeEnum.LastYear,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.LastYear}`),
},
{
value: TableDateCompareTypeEnum.NextYear,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.NextYear}`),
},
{
value: TableDateCompareTypeEnum.Quarter,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Quarter}`),
},
{
value: TableDateCompareTypeEnum.Month,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Month}`),
},
],
},
];
}
export function getConditionDateSelect(injector: Injector, dateType?: TableDateCompareTypeEnum) {
if (!dateType) {
return [];
}
const localeService = injector.get(LocaleService);
switch (dateType) {
case TableDateCompareTypeEnum.Quarter:
return [
{
value: TableDateCompareTypeEnum.Q1,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Q1}`),
},
{
value: TableDateCompareTypeEnum.Q2,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Q2}`),
},
{
value: TableDateCompareTypeEnum.Q3,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Q3}`),
},
{
value: TableDateCompareTypeEnum.Q4,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.Q4}`),
},
];
case TableDateCompareTypeEnum.Month:
return [
{
value: TableDateCompareTypeEnum.M1,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M1}`),
},
{
value: TableDateCompareTypeEnum.M2,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M2}`),
},
{
value: TableDateCompareTypeEnum.M3,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M3}`),
},
{
value: TableDateCompareTypeEnum.M4,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M4}`),
},
{
value: TableDateCompareTypeEnum.M5,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M5}`),
},
{
value: TableDateCompareTypeEnum.M6,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M6}`),
},
{
value: TableDateCompareTypeEnum.M7,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M7}`),
},
{
value: TableDateCompareTypeEnum.M8,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M8}`),
},
{
value: TableDateCompareTypeEnum.M9,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M9}`),
},
{
value: TableDateCompareTypeEnum.M10,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M10}`),
},
{
value: TableDateCompareTypeEnum.M11,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M11}`),
},
{
value: TableDateCompareTypeEnum.M12,
label: localeService.t<LocaleKey>(`sheets-table-ui.date.compare.${TableDateCompareTypeEnum.M12}`),
},
];
default:
return [];
}
}
export const datePickerSet: Set<IConditionCompareTypeEnum> = new Set([
TableDateCompareTypeEnum.Equal,
TableDateCompareTypeEnum.NotEqual,
TableDateCompareTypeEnum.After,
TableDateCompareTypeEnum.AfterOrEqual,
TableDateCompareTypeEnum.Before,
TableDateCompareTypeEnum.BeforeOrEqual,
]);
export function getSubComponentType(type: TableConditionTypeEnum, compare?: TableStringCompareTypeEnum | TableNumberCompareTypeEnum | TableDateCompareTypeEnum): ConditionSubComponentEnum {
if (!compare) {
return ConditionSubComponentEnum.None;
}
if (type === TableConditionTypeEnum.String) {
return ConditionSubComponentEnum.Input;
} else if (type === TableConditionTypeEnum.Number) {
if (compare === TableNumberCompareTypeEnum.Between || compare === TableNumberCompareTypeEnum.NotBetween) {
return ConditionSubComponentEnum.Inputs;
} else {
return ConditionSubComponentEnum.Input;
}
} else if (type === TableConditionTypeEnum.Date) {
if (compare === TableDateCompareTypeEnum.Between || compare === TableDateCompareTypeEnum.NotBetween) {
return ConditionSubComponentEnum.DateRange;
} else if (compare === TableDateCompareTypeEnum.Quarter || compare === TableDateCompareTypeEnum.Month) {
return ConditionSubComponentEnum.Select;
} else if (datePickerSet.has(compare as TableDateCompareTypeEnum)) {
return ConditionSubComponentEnum.DatePicker;
}
return ConditionSubComponentEnum.None;
}
return ConditionSubComponentEnum.None;
}
// eslint-disable-next-line complexity
export function getInitConditionInfo(tableFilter?: ITableFilterItem) {
if (!tableFilter || tableFilter.filterType !== TableColumnFilterTypeEnum.condition) {
return {
type: TableConditionTypeEnum.String,
compare: TableStringCompareTypeEnum.Equal,
info: {},
};
}
const filterInfo = tableFilter.filterInfo;
const { conditionType, compareType } = filterInfo;
if (conditionType === TableConditionTypeEnum.Date) {
if (compareType === TableDateCompareTypeEnum.Between || compareType === TableDateCompareTypeEnum.NotBetween) {
// ensure expectedValue is an array of two dates
let dateRange: [Date, Date] | undefined;
if (Array.isArray(filterInfo.expectedValue)) {
dateRange = filterInfo.expectedValue.map((i) => (typeof i === 'string' ? new Date(i) : i)) as [Date, Date];
}
return {
type: conditionType,
compare: compareType,
info: {
dateRange,
},
};
} else if (
compareType === TableDateCompareTypeEnum.Today
|| compareType === TableDateCompareTypeEnum.Yesterday
|| compareType === TableDateCompareTypeEnum.Tomorrow
|| compareType === TableDateCompareTypeEnum.ThisWeek
|| compareType === TableDateCompareTypeEnum.LastWeek
|| compareType === TableDateCompareTypeEnum.NextWeek
|| compareType === TableDateCompareTypeEnum.ThisMonth
|| compareType === TableDateCompareTypeEnum.LastMonth
|| compareType === TableDateCompareTypeEnum.NextMonth
|| compareType === TableDateCompareTypeEnum.ThisYear
|| compareType === TableDateCompareTypeEnum.LastYear
|| compareType === TableDateCompareTypeEnum.NextYear
) {
return {
type: conditionType,
compare: compareType,
info: {},
};
} else if (datePickerSet.has(compareType as TableDateCompareTypeEnum)) {
// ensure expectedValue is a date or an array of two dates
let date: Date | [Date, Date] | undefined;
if (typeof filterInfo.expectedValue === 'string') {
date = new Date(filterInfo.expectedValue);
} else if (Array.isArray(filterInfo.expectedValue)) {
for (let i = 0; i < filterInfo.expectedValue.length; i++) {
if (typeof filterInfo.expectedValue[i] === 'string') {
filterInfo.expectedValue[i] = new Date(filterInfo.expectedValue[i]);
}
}
}
return {
type: conditionType,
compare: compareType,
info: {
date,
},
};
} else {
const quarter = new Set([TableDateCompareTypeEnum.Q1, TableDateCompareTypeEnum.Q2, TableDateCompareTypeEnum.Q3, TableDateCompareTypeEnum.Q4]);
if (quarter.has(compareType as TableDateCompareTypeEnum)) {
return {
type: conditionType,
compare: TableDateCompareTypeEnum.Quarter,
info: {
dateSelect: filterInfo.compareType,
},
};
} else {
return {
type: conditionType,
compare: TableDateCompareTypeEnum.Month,
info: {
dateSelect: filterInfo.compareType,
},
};
}
}
} else if (conditionType === TableConditionTypeEnum.Number) {
if (compareType === TableNumberCompareTypeEnum.Between || compareType === TableNumberCompareTypeEnum.NotBetween) {
return {
type: conditionType,
compare: compareType,
info: {
numberRange: filterInfo.expectedValue,
},
};
} else {
return {
type: conditionType,
compare: compareType,
info: {
number: filterInfo.expectedValue,
},
};
}
} else if (conditionType === TableConditionTypeEnum.String) {
return {
type: conditionType,
compare: compareType,
info: {
string: filterInfo.expectedValue,
},
};
}
return {
type: TableConditionTypeEnum.String,
compare: TableStringCompareTypeEnum.Equal,
info: {},
};
}