mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 02:48:51 +00:00
metadata copy and paste cell (#6472)
This commit is contained in:
@@ -273,10 +273,14 @@ const formatTextToNumber = (value) => {
|
||||
return isNaN(newData) ? null : newData;
|
||||
};
|
||||
|
||||
const isNumber = (number) => (number || number === 0) && Object.prototype.toString.call(number) === '[object Number]';
|
||||
|
||||
export {
|
||||
getPrecisionNumber,
|
||||
getNumberDisplayString,
|
||||
replaceNumberNotAllowInput,
|
||||
formatStringToNumber,
|
||||
formatTextToNumber,
|
||||
getFloatNumber,
|
||||
isNumber,
|
||||
};
|
||||
|
@@ -97,11 +97,10 @@ const createOption = (options, optionName, optionColor = '') => {
|
||||
const generatorCellOption = (options, optionName) => {
|
||||
const existOption = options.find((option) => option.name === optionName);
|
||||
if (existOption) {
|
||||
return { selectedOptionId: existOption.id };
|
||||
return existOption;
|
||||
}
|
||||
|
||||
const newOption = createOption(options, optionName) || {};
|
||||
return { cellOption: newOption, selectedOptionId: newOption.id };
|
||||
return newOption;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -8,10 +8,12 @@ import { useRecordDetails } from '../../../hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, searchResult, ...params }) => {
|
||||
const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, searchResult, recordGetterByIndex, recordGetterById, ...params }) => {
|
||||
|
||||
const gridUtils = useMemo(() => {
|
||||
return new GridUtils(metadata, { modifyRecord, modifyRecords });
|
||||
}, [metadata, modifyRecord, modifyRecords]);
|
||||
return new GridUtils(metadata, { modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById });
|
||||
}, [metadata, modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById]);
|
||||
|
||||
const { openRecordDetails } = useRecordDetails();
|
||||
|
||||
const groupbysCount = useMemo(() => {
|
||||
@@ -28,6 +30,10 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
|
||||
return columns.filter(column => !hidden_columns.includes(column.key));
|
||||
}, [metadata]);
|
||||
|
||||
const getCopiedRecordsAndColumnsFromRange = useCallback(({ type, copied, isGroupView }) => {
|
||||
return gridUtils.getCopiedContent({ type, copied, isGroupView, columns });
|
||||
}, [gridUtils, columns]);
|
||||
|
||||
const updateRecord = useCallback(({ rowId, updates, originalUpdates, oldRowData, originalOldRowData }) => {
|
||||
modifyRecord && modifyRecord(rowId, updates, oldRowData, originalUpdates, originalOldRowData);
|
||||
}, [modifyRecord]);
|
||||
@@ -36,6 +42,10 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
|
||||
modifyRecords && modifyRecords(recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData, isCopyPaste);
|
||||
}, [modifyRecords]);
|
||||
|
||||
const paste = useCallback(({ type, copied, multiplePaste, pasteRange, isGroupView }) => {
|
||||
gridUtils.paste({ type, copied, multiplePaste, pasteRange, isGroupView, columns });
|
||||
}, [gridUtils, columns]);
|
||||
|
||||
return (
|
||||
<div className={classnames('table-main-container container-fluid p-0', { [`group-level-${groupbysCount + 1}`]: groupbysCount > 0 })}>
|
||||
<Records
|
||||
@@ -49,10 +59,14 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
|
||||
gridUtils={gridUtils}
|
||||
scrollToLoadMore={loadMore}
|
||||
loadAll={loadAll}
|
||||
paste={paste}
|
||||
groupOffsetLeft={groupOffset}
|
||||
modifyRecord={updateRecord}
|
||||
updateRecords={updateRecords}
|
||||
onRowExpand={openRecordDetails}
|
||||
getCopiedRecordsAndColumnsFromRange={getCopiedRecordsAndColumnsFromRange}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
{...params}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -720,6 +720,7 @@ Records.propTypes = {
|
||||
renameColumn: PropTypes.func,
|
||||
deleteColumn: PropTypes.func,
|
||||
modifyColumnData: PropTypes.func,
|
||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Records;
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import CellMask from './cell-mask';
|
||||
|
||||
function DragMask({ draggedRange, getSelectedRangeDimensions, getSelectedDimensions }) {
|
||||
const { overRecordIdx, bottomRight } = draggedRange;
|
||||
const { idx: endColumnIdx, rowIdx: endRowIdx, groupRecordIndex: endGroupRowIndex } = bottomRight;
|
||||
if (overRecordIdx !== null && endRowIdx < overRecordIdx) {
|
||||
const className = 'react-grid-cell-dragged-over-down';
|
||||
let dimensions = getSelectedRangeDimensions(draggedRange);
|
||||
for (let currentRowIdx = endRowIdx + 1; currentRowIdx <= overRecordIdx; currentRowIdx++) {
|
||||
const { height } = getSelectedDimensions({ idx: endColumnIdx, rowIdx: currentRowIdx, groupRecordIndex: endGroupRowIndex });
|
||||
@@ -16,7 +14,7 @@ function DragMask({ draggedRange, getSelectedRangeDimensions, getSelectedDimensi
|
||||
return (
|
||||
<CellMask
|
||||
{...dimensions}
|
||||
className={className}
|
||||
className='react-grid-cell-dragged-over-down'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -72,17 +72,18 @@ class InteractionMasks extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribeSelectColumn = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_COLUMN, this.onColumnSelect);
|
||||
this.unsubscribeDragEnter = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.DRAG_ENTER, this.handleDragEnter);
|
||||
this.unsubscribeSelectCell = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_CELL, this.onSelectCell);
|
||||
this.unsubscribeSelectNone = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, this.selectNone);
|
||||
this.unsubscribeSelectStart = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_START, this.onSelectCellRangeStarted);
|
||||
this.unsubscribeSelectUpdate = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_UPDATE, this.onSelectCellRangeUpdated);
|
||||
this.unsubscribeSelectEnd = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_END, this.onSelectCellRangeEnded);
|
||||
this.unsubscribeOpenEditorEvent = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_EDITOR, this.onOpenEditorEvent);
|
||||
this.unsubscribeCloseEditorEvent = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.CLOSE_EDITOR, this.onCloseEditorEvent);
|
||||
this.unsubscribeCopy = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.COPY_CELLS, this.onCopy);
|
||||
this.unsubscribePaste = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.PASTE_CELLS, this.onPaste);
|
||||
const eventBus = window.sfMetadataContext.eventBus;
|
||||
this.unsubscribeSelectColumn = eventBus.subscribe(EVENT_BUS_TYPE.SELECT_COLUMN, this.onColumnSelect);
|
||||
this.unsubscribeDragEnter = eventBus.subscribe(EVENT_BUS_TYPE.DRAG_ENTER, this.handleDragEnter);
|
||||
this.unsubscribeSelectCell = eventBus.subscribe(EVENT_BUS_TYPE.SELECT_CELL, this.onSelectCell);
|
||||
this.unsubscribeSelectNone = eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, this.selectNone);
|
||||
this.unsubscribeSelectStart = eventBus.subscribe(EVENT_BUS_TYPE.SELECT_START, this.onSelectCellRangeStarted);
|
||||
this.unsubscribeSelectUpdate = eventBus.subscribe(EVENT_BUS_TYPE.SELECT_UPDATE, this.onSelectCellRangeUpdated);
|
||||
this.unsubscribeSelectEnd = eventBus.subscribe(EVENT_BUS_TYPE.SELECT_END, this.onSelectCellRangeEnded);
|
||||
this.unsubscribeOpenEditorEvent = eventBus.subscribe(EVENT_BUS_TYPE.OPEN_EDITOR, this.onOpenEditorEvent);
|
||||
this.unsubscribeCloseEditorEvent = eventBus.subscribe(EVENT_BUS_TYPE.CLOSE_EDITOR, this.onCloseEditorEvent);
|
||||
this.unsubscribeCopy = eventBus.subscribe(EVENT_BUS_TYPE.COPY_CELLS, this.onCopy);
|
||||
this.unsubscribePaste = eventBus.subscribe(EVENT_BUS_TYPE.PASTE_CELLS, this.onPaste);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@@ -111,7 +112,7 @@ class InteractionMasks extends React.Component {
|
||||
}
|
||||
|
||||
onColumnSelect = (column) => {
|
||||
let { columns, isGroupView } = this.props;
|
||||
let { columns, isGroupView, recordsCount } = this.props;
|
||||
if (isGroupView) return;
|
||||
let selectColumnIndex = 0;
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
@@ -120,13 +121,12 @@ class InteractionMasks extends React.Component {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const rowsCount = this.props.recordsCount;
|
||||
this.setState({
|
||||
selectedPosition: { ...this.state.selectedPosition, idx: selectColumnIndex, rowIdx: 0 },
|
||||
selectedRange: {
|
||||
startCell: { idx: selectColumnIndex, rowIdx: 0 },
|
||||
topLeft: { idx: selectColumnIndex, rowIdx: 0 },
|
||||
bottomRight: { idx: selectColumnIndex, rowIdx: rowsCount - 1 },
|
||||
bottomRight: { idx: selectColumnIndex, rowIdx: recordsCount - 1 },
|
||||
isDragging: false,
|
||||
}
|
||||
});
|
||||
|
@@ -3,6 +3,7 @@ import { UserService, LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_C
|
||||
EDITABLE_PRIVATE_COLUMN_KEYS, PREDEFINED_COLUMN_KEYS } from './_basic';
|
||||
import EventBus from '../../components/common/event-bus';
|
||||
import { username } from '../../utils/constants';
|
||||
import User from './model/user';
|
||||
|
||||
class Context {
|
||||
|
||||
@@ -14,6 +15,7 @@ class Context {
|
||||
this.eventBus = null;
|
||||
this.hasInit = false;
|
||||
this.permission = 'r';
|
||||
this.collaboratorsCache = {};
|
||||
}
|
||||
|
||||
async init({ otherSettings }) {
|
||||
@@ -128,9 +130,22 @@ class Context {
|
||||
return true;
|
||||
};
|
||||
|
||||
getCollaboratorsFromCache = () => {
|
||||
//
|
||||
};
|
||||
getCollaboratorFromCache(email) {
|
||||
return this.collaboratorsCache[email];
|
||||
}
|
||||
|
||||
getCollaboratorsFromCache() {
|
||||
const collaboratorsCache = this.collaboratorsCache;
|
||||
return Object.values(collaboratorsCache).filter(item => item.email !== 'anonymous');
|
||||
}
|
||||
|
||||
updateCollaboratorsCache(email, collaborator) {
|
||||
if (collaborator instanceof User) {
|
||||
this.collaboratorsCache[email] = collaborator;
|
||||
return;
|
||||
}
|
||||
this.collaboratorsCache[email] = new User(collaborator);
|
||||
}
|
||||
|
||||
restoreRows = () => {
|
||||
// todo
|
||||
|
@@ -0,0 +1,98 @@
|
||||
import { DateUtils, CellType, DEFAULT_DATE_FORMAT, getCollaboratorsName, getOptionName, getDateDisplayString, getLongtextDisplayString, getNumberDisplayString } from '../../_basic';
|
||||
|
||||
const getCellValueDisplayString = (row, type, key, { data, collaborators = [] } = {}) => {
|
||||
if (!row) return '';
|
||||
const cellValue = row[key];
|
||||
switch (type) {
|
||||
case CellType.LONG_TEXT: {
|
||||
return getLongtextDisplayString(cellValue);
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return getNumberDisplayString(cellValue, data);
|
||||
}
|
||||
case CellType.SINGLE_SELECT: {
|
||||
if (!data) return '';
|
||||
const { options } = data;
|
||||
return getOptionName(options, cellValue);
|
||||
}
|
||||
case CellType.DATE: {
|
||||
const { format = DEFAULT_DATE_FORMAT } = data || {};
|
||||
return getDateDisplayString(cellValue, format);
|
||||
}
|
||||
case CellType.CTIME:
|
||||
case CellType.MTIME: {
|
||||
return DateUtils.format(cellValue, 'YYYY-MM-DD HH:MM:SS');
|
||||
}
|
||||
case CellType.COLLABORATOR: {
|
||||
return getCollaboratorsName(collaborators, cellValue);
|
||||
}
|
||||
case CellType.CREATOR:
|
||||
case CellType.LAST_MODIFIER: {
|
||||
return cellValue === 'anonymous' ? cellValue : getCollaboratorsName(collaborators, [cellValue]);
|
||||
}
|
||||
default: {
|
||||
if (cellValue || typeof cellValue === 'boolean') {
|
||||
return String(cellValue);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getCellValueStringResult = (row, column, { collaborators = [] } = {}) => {
|
||||
if (!row || !column) return '';
|
||||
const { key, type, data } = column;
|
||||
let cellValue = row[key];
|
||||
switch (type) {
|
||||
case CellType.TEXT:
|
||||
case CellType.EMAIL:
|
||||
case CellType.URL:
|
||||
case CellType.AUTO_NUMBER: {
|
||||
return cellValue || '';
|
||||
}
|
||||
case CellType.RATE: { // number
|
||||
return cellValue ? String(cellValue) : '';
|
||||
}
|
||||
case CellType.CHECKBOX: {
|
||||
if (typeof cellValue === 'boolean') {
|
||||
return String(cellValue);
|
||||
}
|
||||
return cellValue === 'true' ? 'true' : 'false';
|
||||
}
|
||||
case CellType.LONG_TEXT: {
|
||||
return getLongtextDisplayString(cellValue);
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return getNumberDisplayString(cellValue, data);
|
||||
}
|
||||
case CellType.SINGLE_SELECT: {
|
||||
if (!data) return '';
|
||||
return getOptionName(data.options, cellValue);
|
||||
}
|
||||
case CellType.DATE: {
|
||||
const { format = DEFAULT_DATE_FORMAT } = data || {};
|
||||
// patch: compatible with previous format
|
||||
const normalizedFormat = format === 'D/M/YYYY' ? format.replace(/D\/M\/YYYY/, 'DD/MM/YYYY') : format;
|
||||
return getDateDisplayString(cellValue, normalizedFormat);
|
||||
}
|
||||
case CellType.CTIME:
|
||||
case CellType.MTIME: {
|
||||
return DateUtils.format(cellValue, 'YYYY-MM-DD HH:MM:SS');
|
||||
}
|
||||
case CellType.COLLABORATOR: {
|
||||
return getCollaboratorsName(collaborators, cellValue);
|
||||
}
|
||||
case CellType.CREATOR:
|
||||
case CellType.LAST_MODIFIER: {
|
||||
return cellValue === 'anonymous' ? cellValue : getCollaboratorsName(collaborators, [cellValue]);
|
||||
}
|
||||
default: {
|
||||
return cellValue ? String(cellValue) : '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
getCellValueDisplayString,
|
||||
getCellValueStringResult,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
getPrecisionNumber,
|
||||
getNumberDisplayString,
|
||||
replaceNumberNotAllowInput,
|
||||
} from './number';
|
||||
export {
|
||||
getCellValueDisplayString,
|
||||
getCellValueStringResult,
|
||||
} from './cell-value';
|
@@ -0,0 +1,189 @@
|
||||
import { DEFAULT_NUMBER_FORMAT } from '../../_basic';
|
||||
import { DISPLAY_INTERNAL_ERRORS } from '../../_basic/constants';
|
||||
import { round } from '../../_basic/utils/number';
|
||||
import { NPminus } from '../../_basic/utils/helper/number-precision';
|
||||
|
||||
const separatorMap = {
|
||||
comma: ',',
|
||||
dot: '.',
|
||||
no: '',
|
||||
space: ' ',
|
||||
};
|
||||
|
||||
const _getDecimalDigits = (number) => {
|
||||
if (Number.isInteger(number)) {
|
||||
return 0;
|
||||
}
|
||||
const valueArr = String(number).split('.');
|
||||
const digitsLength = valueArr[1] ? valueArr[1].length : 8;
|
||||
return digitsLength > 8 ? 8 : digitsLength;
|
||||
};
|
||||
|
||||
const getPrecisionNumber = (number, formats) => {
|
||||
const { precision = 2, enable_precision = false } = formats || {};
|
||||
const type = Object.prototype.toString.call(number);
|
||||
if (type !== '[object Number]') {
|
||||
if (type === '[object String]' && DISPLAY_INTERNAL_ERRORS.includes(number)) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const decimalDigits = enable_precision ? precision : _getDecimalDigits(number);
|
||||
return number.toFixed(decimalDigits);
|
||||
};
|
||||
|
||||
const removeZerosFromEnd = (sNumber) => {
|
||||
if (typeof sNumber !== 'string') return '';
|
||||
if (sNumber.endsWith('0')) {
|
||||
return sNumber.replace(/(?:\.0*|(\.\d+?)0+)$/, '$1');
|
||||
}
|
||||
return sNumber;
|
||||
};
|
||||
|
||||
const getDecimalDigitsFromNumber = (number) => {
|
||||
if (Number.isInteger(number)) {
|
||||
return 0;
|
||||
}
|
||||
const decimalPart = String(number).split('.')[1];
|
||||
const digitsLength = decimalPart ? decimalPart.length : 8;
|
||||
return digitsLength > 8 ? 8 : digitsLength;
|
||||
};
|
||||
|
||||
const toThousands = (number, { formats, isCurrency = true }) => {
|
||||
const {
|
||||
decimal = 'dot', thousands = 'no', precision = 2, enable_precision = false,
|
||||
} = formats || {};
|
||||
|
||||
// handle numbers in scientific notation
|
||||
if (String(number).includes('e')) {
|
||||
if (number < 1 && number > -1) {
|
||||
// 1.convert to non-scientific number
|
||||
let numericString = number.toFixed(enable_precision ? precision : 8);
|
||||
|
||||
// 2.remove 0 from end of the number which not set precision. e.g. 0.100000
|
||||
if (!enable_precision) {
|
||||
numericString = removeZerosFromEnd(numericString);
|
||||
}
|
||||
|
||||
// 3.remove minus from number which equal to 0. e.g. '-0.00'
|
||||
if (parseFloat(numericString) === 0) {
|
||||
return numericString.startsWith('-') ? numericString.substring(1) : numericString;
|
||||
}
|
||||
return numericString;
|
||||
}
|
||||
return String(number);
|
||||
}
|
||||
|
||||
const decimalString = separatorMap[decimal];
|
||||
const thousandsString = separatorMap[thousands];
|
||||
const decimalDigits = enable_precision ? precision : getDecimalDigitsFromNumber(number);
|
||||
const floatNumber = parseFloat(round(number, decimalDigits).toFixed(decimalDigits));
|
||||
const isMinus = floatNumber < 0;
|
||||
let integer = Math.trunc(floatNumber);
|
||||
|
||||
// format decimal part
|
||||
let decimalPart = String(Math.abs(NPminus(floatNumber, integer)).toFixed(decimalDigits)).slice(1);
|
||||
if (!enable_precision) {
|
||||
decimalPart = removeZerosFromEnd(decimalPart);
|
||||
}
|
||||
|
||||
if (isCurrency) {
|
||||
if (!enable_precision) {
|
||||
decimalPart = decimalPart.length === 2
|
||||
? decimalPart = decimalPart.padEnd(3, '0')
|
||||
: (decimalPart.substring(0, 3) || '.').padEnd(3, '0');
|
||||
}
|
||||
}
|
||||
decimalPart = decimalPart.replace(/./, decimalString);
|
||||
|
||||
// format integer part
|
||||
const integerNumbers = [];
|
||||
let counter = 0;
|
||||
integer = Math.abs(integer).toString();
|
||||
for (let i = integer.length - 1; i > -1; i--) {
|
||||
counter += 1;
|
||||
integerNumbers.unshift(integer[i]);
|
||||
if (!(counter % 3) && i !== 0) {
|
||||
integerNumbers.unshift(thousandsString);
|
||||
}
|
||||
}
|
||||
|
||||
return `${isMinus ? '-' : ''}${integerNumbers.join('')}${decimalPart}`;
|
||||
};
|
||||
|
||||
const getNumberDisplayString = (number, formats) => {
|
||||
const type = Object.prototype.toString.call(number);
|
||||
if (type !== '[object Number]') {
|
||||
// return formula internal errors directly.
|
||||
if (type === '[object String]' && number.startsWith('#')) {
|
||||
return number;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isNaN(number) || number === Infinity || number === -Infinity) return String(number);
|
||||
|
||||
// formats: old version maybe 'null'
|
||||
const { format = DEFAULT_NUMBER_FORMAT } = formats || {};
|
||||
switch (format) {
|
||||
case 'number': {
|
||||
return toThousands(number, { formats, isCurrency: false });
|
||||
}
|
||||
case 'percent': {
|
||||
return `${toThousands(Number.parseFloat((number * 100).toFixed(8)), { formats, isCurrency: false })}%`;
|
||||
}
|
||||
case 'yuan': {
|
||||
return `¥${toThousands(number, { formats })}`;
|
||||
}
|
||||
case 'dollar': {
|
||||
return `$${toThousands(number, { formats })}`;
|
||||
}
|
||||
case 'euro': {
|
||||
return `€${toThousands(number, { formats })}`;
|
||||
}
|
||||
case 'custom_currency': {
|
||||
if (formats.currency_symbol_position === 'after') {
|
||||
return `${toThousands(number, { formats })}${formats.currency_symbol || ''}`;
|
||||
}
|
||||
return `${formats.currency_symbol || ''}${toThousands(number, { formats })}`;
|
||||
}
|
||||
default: {
|
||||
return String(number);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const replaceNumberNotAllowInput = (sNum, format, currencySymbol) => {
|
||||
if (!sNum) {
|
||||
return '';
|
||||
}
|
||||
const fixedSNum = sNum.replace(/。/g, '.');
|
||||
switch (format) {
|
||||
case 'percent': {
|
||||
return fixedSNum.replace(/[^.-\d,%]/g, '');
|
||||
}
|
||||
case 'yuan': {
|
||||
return fixedSNum.replace(/[^.-\d¥¥,]/g, '');
|
||||
}
|
||||
case 'dollar': {
|
||||
return fixedSNum.replace(/[^.-\d$,]/g, '');
|
||||
}
|
||||
case 'euro': {
|
||||
return fixedSNum.replace(/[^.-\d€,]/g, '');
|
||||
}
|
||||
case 'custom_currency': {
|
||||
const reg = new RegExp('[^.-\\d' + currencySymbol + ',]', 'g');
|
||||
return fixedSNum.replace(reg, '');
|
||||
}
|
||||
default: {
|
||||
// default as number format
|
||||
return fixedSNum.replace(/[^.-\d,]/g, '');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
getPrecisionNumber,
|
||||
getNumberDisplayString,
|
||||
replaceNumberNotAllowInput,
|
||||
};
|
301
frontend/src/metadata/metadata-view/utils/convert-utils.js
Normal file
301
frontend/src/metadata/metadata-view/utils/convert-utils.js
Normal file
@@ -0,0 +1,301 @@
|
||||
import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString } from '../_basic';
|
||||
import { formatTextToDate } from './date';
|
||||
import { getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber } from '../_basic/utils/cell/column/number';
|
||||
|
||||
const SUPPORT_PASTE_FROM_COLUMN = {
|
||||
[CellType.NUMBER]: [CellType.TEXT, CellType.NUMBER],
|
||||
};
|
||||
|
||||
const reg_chinese_date_format = /(\d{4})年(\d{1,2})月(\d{1,2})日$/;
|
||||
|
||||
export function getSelectColumnOptions(column) {
|
||||
if (!column || !column.data || !Array.isArray(column.data.options)) {
|
||||
return [];
|
||||
}
|
||||
return column.data.options;
|
||||
}
|
||||
|
||||
function convertCellValue(cellValue, oldCellValue, targetColumn, fromColumn) {
|
||||
const { type: fromColumnType, data: fromColumnData } = fromColumn;
|
||||
const { type: targetColumnType, data: targetColumnData } = targetColumn;
|
||||
switch (targetColumnType) {
|
||||
case CellType.CHECKBOX: {
|
||||
return convert2Checkbox(cellValue, oldCellValue, fromColumnType);
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return convert2Number(cellValue, oldCellValue, fromColumnType, targetColumnData);
|
||||
}
|
||||
case CellType.DATE: {
|
||||
return convert2Date(cellValue, oldCellValue, fromColumnType, fromColumnData, targetColumnData);
|
||||
}
|
||||
case CellType.SINGLE_SELECT: {
|
||||
return convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn);
|
||||
}
|
||||
case CellType.LONG_TEXT: {
|
||||
return convert2LongText(cellValue, oldCellValue, fromColumn);
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
return convert2Text(cellValue, oldCellValue, fromColumn);
|
||||
}
|
||||
case CellType.COLLABORATOR: {
|
||||
return convert2Collaborator(cellValue, oldCellValue, fromColumnType);
|
||||
}
|
||||
default: {
|
||||
return oldCellValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convert2Checkbox(cellValue, oldCellValue, fromColumnType) {
|
||||
switch (fromColumnType) {
|
||||
case CellType.CHECKBOX: {
|
||||
if (typeof cellValue === 'boolean') {
|
||||
return cellValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
if (cellValue && typeof cellValue === 'string' && cellValue.toLocaleLowerCase() === 'true') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return cellValue > 0;
|
||||
}
|
||||
default: {
|
||||
return oldCellValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convert2Number(cellValue, oldCellValue, fromColumnType, targetColumnData) {
|
||||
if (!SUPPORT_PASTE_FROM_COLUMN[CellType.NUMBER].includes(fromColumnType)) {
|
||||
return oldCellValue;
|
||||
}
|
||||
if (cellValue === 0) {
|
||||
return cellValue;
|
||||
}
|
||||
if (!cellValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (fromColumnType) {
|
||||
case CellType.NUMBER:
|
||||
case CellType.RATE:
|
||||
case CellType.DURATION: {
|
||||
return cellValue;
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
if (cellValue.includes('%')) {
|
||||
return getFloatNumber(cellValue, 'percent');
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const stringCellVal = typeof cellValue === 'string' ? cellValue : cellValue.toString();
|
||||
return formatStringToNumber(stringCellVal, targetColumnData);
|
||||
}
|
||||
|
||||
function convert2Date(cellValue, oldCellValue, fromColumnType, fromColumnData, targetColumnData) {
|
||||
const currentFormat = targetColumnData?.format || DEFAULT_DATE_FORMAT;
|
||||
const fromFormat = fromColumnData?.format || DEFAULT_DATE_FORMAT;
|
||||
switch (fromColumnType) {
|
||||
case CellType.DATE: {
|
||||
const fromTimeIdx = fromFormat.indexOf('HH:mm');
|
||||
const currentTimeIdx = currentFormat.indexOf('HH:mm');
|
||||
if ((fromTimeIdx > -1 && currentTimeIdx > -1) || fromTimeIdx === currentTimeIdx) {
|
||||
return cellValue;
|
||||
}
|
||||
return formatTextToDate(cellValue, currentFormat);
|
||||
}
|
||||
case CellType.CTIME:
|
||||
case CellType.MTIME: {
|
||||
return formatTextToDate(cellValue, currentFormat);
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
if (cellValue.indexOf('年') > -1) {
|
||||
const zhCNDate = cellValue.replace(/\s*/g, '');
|
||||
if (!reg_chinese_date_format.test(zhCNDate)) {
|
||||
return '';
|
||||
}
|
||||
return formatTextToDate(zhCNDate.replace(reg_chinese_date_format, '$1-$2-$3'), currentFormat);
|
||||
}
|
||||
return formatTextToDate(cellValue, currentFormat);
|
||||
}
|
||||
default: {
|
||||
return oldCellValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn) {
|
||||
if (!cellValue) {
|
||||
return oldCellValue;
|
||||
}
|
||||
const { type: fromColumnType } = fromColumn;
|
||||
let fromOptionName;
|
||||
switch (fromColumnType) {
|
||||
case CellType.SINGLE_SELECT: {
|
||||
const fromOptions = getSelectColumnOptions(fromColumn);
|
||||
fromOptionName = getOptionName(fromOptions, cellValue) || '';
|
||||
break;
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
fromOptionName = cellValue;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fromOptionName) {
|
||||
return oldCellValue;
|
||||
}
|
||||
|
||||
const currentOptions = getSelectColumnOptions(targetColumn);
|
||||
const newOption = generatorCellOption(currentOptions, fromOptionName);
|
||||
return newOption.name;
|
||||
}
|
||||
|
||||
const LONG_TEXT_LENGTH_LIMIT = 10 * 10000;
|
||||
|
||||
export const isLongTextValueExceedLimit = (value) => {
|
||||
const { text } = value;
|
||||
return text ? text.length >= LONG_TEXT_LENGTH_LIMIT : false;
|
||||
};
|
||||
|
||||
export const getValidLongTextValue = (value) => {
|
||||
const newValue = { ...value };
|
||||
const { text, preview } = newValue;
|
||||
newValue.text = text ? text.slice(0, LONG_TEXT_LENGTH_LIMIT) : '';
|
||||
newValue.preview = preview ? preview.slice(0, LONG_TEXT_LENGTH_LIMIT) : '';
|
||||
return newValue;
|
||||
};
|
||||
|
||||
function convert2LongText(cellValue, oldCellValue, fromColumn) {
|
||||
const { type: fromColumnType, data: fromColumnData } = fromColumn;
|
||||
switch (fromColumnType) {
|
||||
case CellType.LONG_TEXT: {
|
||||
// cell value is a string value from dtable-db
|
||||
const value = { text: cellValue };
|
||||
if (isLongTextValueExceedLimit(value)) {
|
||||
const newValue = getValidLongTextValue(value);
|
||||
return newValue.text;
|
||||
}
|
||||
return cellValue || null;
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
return cellValue || null;
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return getNumberDisplayString(cellValue, fromColumnData) || null;
|
||||
}
|
||||
case CellType.DATE: {
|
||||
return getDateDisplayString(cellValue, fromColumnData.format || DEFAULT_DATE_FORMAT) || null;
|
||||
}
|
||||
default: {
|
||||
return oldCellValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convert2Text(cellValue, oldCellValue, fromColumn) {
|
||||
const { type: fromColumnType, data: fromColumnData } = fromColumn;
|
||||
switch (fromColumnType) {
|
||||
case CellType.TEXT: {
|
||||
if (isNumber(cellValue)) {
|
||||
return String(cellValue);
|
||||
}
|
||||
if (!cellValue) {
|
||||
return null;
|
||||
}
|
||||
if (typeof cellValue === 'string') {
|
||||
return cellValue.replace(/\n/g, '').trim();
|
||||
}
|
||||
return String(cellValue);
|
||||
}
|
||||
case CellType.URL:
|
||||
case CellType.EMAIL:
|
||||
case CellType.AUTO_NUMBER: {
|
||||
return cellValue;
|
||||
}
|
||||
case CellType.NUMBER: {
|
||||
return getNumberDisplayString(cellValue, fromColumnData);
|
||||
}
|
||||
case CellType.DATE: {
|
||||
return getDateDisplayString(cellValue, fromColumnData.format || DEFAULT_DATE_FORMAT);
|
||||
}
|
||||
case CellType.SINGLE_SELECT: {
|
||||
const options = getSelectColumnOptions(fromColumn);
|
||||
return getOptionName(options, cellValue) || null;
|
||||
}
|
||||
case CellType.COLLABORATOR: {
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
return getCollaboratorsName(collaborators, cellValue);
|
||||
}
|
||||
case CellType.CREATOR:
|
||||
case CellType.LAST_MODIFIER: {
|
||||
if (!cellValue) {
|
||||
return null;
|
||||
}
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
return getCollaboratorsName(collaborators, [cellValue]);
|
||||
}
|
||||
default: {
|
||||
return oldCellValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
|
||||
switch (fromColumnType) {
|
||||
case CellType.COLLABORATOR: {
|
||||
if (!Array.isArray(cellValue) || cellValue.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
let validEmailMap = {};
|
||||
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
||||
return cellValue.filter(email => !!validEmailMap[email]);
|
||||
}
|
||||
case CellType.TEXT: {
|
||||
if (!cellValue) {
|
||||
return oldCellValue;
|
||||
}
|
||||
const userNames = cellValue.split(',');
|
||||
if (userNames.length === 0) {
|
||||
return oldCellValue;
|
||||
}
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
let nameCollaboratorMap = {};
|
||||
collaborators.forEach(collaborator => nameCollaboratorMap[collaborator.name] = collaborator);
|
||||
const emails = userNames.map(name => {
|
||||
const collaborator = nameCollaboratorMap[name];
|
||||
return collaborator ? collaborator.email : null;
|
||||
}).filter(Boolean);
|
||||
if (emails.length === 0) {
|
||||
return oldCellValue;
|
||||
}
|
||||
return emails;
|
||||
}
|
||||
case CellType.CREATOR:
|
||||
case CellType.LAST_MODIFIER: {
|
||||
const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache();
|
||||
let validEmailMap = {};
|
||||
collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true);
|
||||
if (!cellValue || !validEmailMap[cellValue]) {
|
||||
return null;
|
||||
}
|
||||
return [cellValue];
|
||||
}
|
||||
default: {
|
||||
return oldCellValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { convertCellValue };
|
19
frontend/src/metadata/metadata-view/utils/date.js
Normal file
19
frontend/src/metadata/metadata-view/utils/date.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { DateUtils } from '../_basic';
|
||||
import { getFloatNumber } from '../_basic/utils/cell/column/number';
|
||||
|
||||
const formatTextToDate = (text, format = 'YYYY-MM-DD') => {
|
||||
if (typeof text !== 'string' || !text.trim()) return null;
|
||||
let isAllNumber = /^[0-9]+$/.test(text);
|
||||
let dateObj = {};
|
||||
if (isAllNumber) {
|
||||
dateObj = new Date(getFloatNumber(text));
|
||||
} else {
|
||||
dateObj = DateUtils.parseDateWithFormat(text, format);
|
||||
}
|
||||
if (format.indexOf('HH:mm') < 0) {
|
||||
return DateUtils.format(dateObj, 'YYYY-MM-DD') || null;
|
||||
}
|
||||
return DateUtils.format(dateObj, 'YYYY-MM-DD HH:MM') || null;
|
||||
};
|
||||
|
||||
export { formatTextToDate };
|
@@ -1,11 +1,9 @@
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
CellType,
|
||||
NOT_SUPPORT_EDIT_COLUMN_TYPE_MAP,
|
||||
} from '../_basic';
|
||||
import { CellType, NOT_SUPPORT_EDIT_COLUMN_TYPE_MAP } from '../_basic';
|
||||
import { getColumnByIndex } from './column-utils';
|
||||
import { NOT_SUPPORT_DRAG_COPY_COLUMN_TYPES, TRANSFER_TYPES } from '../constants';
|
||||
import { getGroupRecordByIndex } from './group-metrics';
|
||||
import { convertCellValue } from './convert-utils';
|
||||
|
||||
const NORMAL_RULE = ({ value }) => {
|
||||
return value;
|
||||
@@ -20,16 +18,15 @@ class GridUtils {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
getCopiedContent({ type, copied, isGroupView }) {
|
||||
getCopiedContent({ type, copied, isGroupView, columns }) {
|
||||
// copy from internal grid
|
||||
if (type === TRANSFER_TYPES.DTABLE_FRAGMENT) {
|
||||
const { shownColumns: columns } = this.tablePage.state;
|
||||
const { selectedRecordIds, copiedRange } = copied;
|
||||
|
||||
// copy from selected rows
|
||||
if (Array.isArray(selectedRecordIds) && selectedRecordIds.length > 0) {
|
||||
return {
|
||||
copiedRecords: selectedRecordIds.map(recordId => this.tablePage.recordGetterById(recordId)),
|
||||
copiedRecords: selectedRecordIds.map(recordId => this.api.recordGetterById(recordId)),
|
||||
copiedColumns: [...columns],
|
||||
};
|
||||
}
|
||||
@@ -42,7 +39,7 @@ class GridUtils {
|
||||
const { rowIdx: maxRecordIndex, idx: maxColumnIndex } = bottomRight;
|
||||
let currentGroupIndex = minGroupRecordIndex;
|
||||
for (let i = minRecordIndex; i <= maxRecordIndex; i++) {
|
||||
copiedRecords.push(this.tablePage.recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupIndex, recordIndex: i }));
|
||||
copiedRecords.push(this.api.recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupIndex, recordIndex: i }));
|
||||
if (isGroupView) {
|
||||
currentGroupIndex++;
|
||||
}
|
||||
@@ -58,8 +55,8 @@ class GridUtils {
|
||||
return { copiedRecords, copiedColumns };
|
||||
}
|
||||
|
||||
async paste({ copied, multiplePaste, pasteRange, isGroupView }) {
|
||||
const { row_ids: renderRecordIds, columns } = this.metadata;
|
||||
async paste({ copied, multiplePaste, pasteRange, isGroupView, columns }) {
|
||||
const { row_ids: renderRecordIds } = this.metadata;
|
||||
const { topLeft, bottomRight = {} } = pasteRange;
|
||||
const { rowIdx: startRecordIndex, idx: startColumnIndex, groupRecordIndex } = topLeft;
|
||||
const { rowIdx: endRecordIndex, idx: endColumnIndex } = bottomRight;
|
||||
@@ -72,6 +69,7 @@ class GridUtils {
|
||||
|
||||
// need expand records
|
||||
const startExpandRecordIndex = renderRecordsCount - startRecordIndex;
|
||||
|
||||
if ((copiedRecordsLen > startExpandRecordIndex)) return;
|
||||
|
||||
let updateRecordIds = [];
|
||||
@@ -80,8 +78,9 @@ class GridUtils {
|
||||
let idOldRecordData = {};
|
||||
let idOriginalOldRecordData = {};
|
||||
let currentGroupRecordIndex = groupRecordIndex;
|
||||
|
||||
for (let i = 0; i < pasteRecordsLen; i++) {
|
||||
const pasteRecord = this.tablePage.recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupRecordIndex, recordIndex: startRecordIndex + i });
|
||||
const pasteRecord = this.api.recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupRecordIndex, recordIndex: startRecordIndex + i });
|
||||
if (isGroupView) {
|
||||
currentGroupRecordIndex++;
|
||||
}
|
||||
@@ -93,6 +92,7 @@ class GridUtils {
|
||||
const copiedRecord = copiedRecords[copiedRecordIndex];
|
||||
let originalUpdate = {};
|
||||
let originalOldRecordData = {};
|
||||
|
||||
for (let j = 0; j < pasteColumnsLen; j++) {
|
||||
const pasteColumn = getColumnByIndex(j + startColumnIndex, columns);
|
||||
if (!pasteColumn || NOT_SUPPORT_EDIT_COLUMN_TYPE_MAP[pasteColumn.type]) {
|
||||
@@ -100,16 +100,16 @@ class GridUtils {
|
||||
}
|
||||
const copiedColumnIndex = j % copiedColumnsLen;
|
||||
const copiedColumn = getColumnByIndex(copiedColumnIndex, copiedColumns);
|
||||
const { key: pasteColumnKey } = pasteColumn;
|
||||
const { key: copiedColumnKey } = copiedColumn;
|
||||
const pasteCellValue = Object.prototype.hasOwnProperty.call(pasteRecord, pasteColumnKey) ? pasteRecord[pasteColumnKey] : null;
|
||||
const copiedCellValue = Object.prototype.hasOwnProperty.call(copiedRecord, copiedColumnKey) ? copiedRecord[copiedColumnKey] : null;
|
||||
const update = this.convertCellValue(copiedCellValue, pasteCellValue, pasteColumn, copiedColumn);
|
||||
const { name: pasteColumnName } = pasteColumn;
|
||||
const { name: copiedColumnName } = copiedColumn;
|
||||
const pasteCellValue = Object.prototype.hasOwnProperty.call(pasteRecord, pasteColumnName) ? pasteRecord[pasteColumnName] : null;
|
||||
const copiedCellValue = Object.prototype.hasOwnProperty.call(copiedRecord, copiedColumnName) ? copiedRecord[copiedColumnName] : null;
|
||||
const update = convertCellValue(copiedCellValue, pasteCellValue, pasteColumn, copiedColumn);
|
||||
if (update === pasteCellValue) {
|
||||
continue;
|
||||
}
|
||||
originalUpdate[pasteColumnKey] = update;
|
||||
originalOldRecordData[pasteColumnKey] = pasteCellValue;
|
||||
originalUpdate[pasteColumnName] = update;
|
||||
originalOldRecordData[pasteColumnName] = pasteCellValue;
|
||||
}
|
||||
|
||||
if (Object.keys(originalUpdate).length > 0) {
|
||||
@@ -124,7 +124,7 @@ class GridUtils {
|
||||
}
|
||||
|
||||
if (updateRecordIds.length === 0) return;
|
||||
this.modifyRecords(updateRecordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData, isCopyPaste);
|
||||
this.api.modifyRecords(updateRecordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData, isCopyPaste);
|
||||
}
|
||||
|
||||
getLinkedRowsIdsByNameColumn(linkedTableRows, linkColumnKey, cellValue, linkItem) {
|
||||
|
Reference in New Issue
Block a user