1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-12 13:24:52 +00:00

feat: metadata multiple select

This commit is contained in:
杨国璇
2024-08-16 18:56:41 +08:00
committed by 杨国璇
parent 0142fe448a
commit 7a47c647a2
46 changed files with 900 additions and 75 deletions

View File

@@ -19,7 +19,7 @@
"@seafile/sdoc-editor": "1.0.50",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "1.0.109",
"@seafile/sf-metadata-ui-component": "0.0.21",
"@seafile/sf-metadata-ui-component": "0.0.22",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"axios": "^1.7.3",
@@ -5093,9 +5093,9 @@
}
},
"node_modules/@seafile/sf-metadata-ui-component": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.21.tgz",
"integrity": "sha512-bskuoVgMXDY5sD++MlMx9864+J3BdJ69pXZKifu40op4ebpC6qtJLAdZQV/j/ZqadyzGp1r0T340ClzhUfBQWw==",
"version": "0.0.22",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.22.tgz",
"integrity": "sha512-sRFGl3JoD4m5+Hdmvxt9b6yer8HxT4mI5hHBPqF+AvKsyaWMQ2Dq108vKUUlADbZOLvFlx0YuVfYfOcxYJQeJQ==",
"dependencies": {
"@seafile/seafile-calendar": "0.0.24",
"@seafile/seafile-editor": "~1.0.102",

View File

@@ -14,7 +14,7 @@
"@seafile/sdoc-editor": "1.0.50",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "1.0.109",
"@seafile/sf-metadata-ui-component": "0.0.21",
"@seafile/sf-metadata-ui-component": "0.0.22",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"axios": "^1.7.3",

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1723777705850" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15418" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M128 128C73.6 128 32 169.6 32 224s41.6 96 96 96 96-41.6 96-96-41.6-96-96-96zM128 416c-54.4 0-96 41.6-96 96s41.6 96 96 96 96-41.6 96-96-41.6-96-96-96zM128 704c-54.4 0-96 41.6-96 96s41.6 96 96 96 96-41.6 96-96-41.6-96-96-96zM963.2 736H380.8c-16 0-28.8 12.8-28.8 32v64c0 19.2 12.8 32 28.8 32h582.4c16 0 28.8-12.8 28.8-32v-64c0-19.2-12.8-32-28.8-32zM963.2 160H380.8c-16 0-28.8 12.8-28.8 32v64c0 19.2 12.8 32 28.8 32h582.4c16 0 28.8-12.8 28.8-32V192c0-19.2-12.8-32-28.8-32zM963.2 448H380.8c-16 0-28.8 12.8-28.8 32v64c0 19.2 12.8 32 28.8 32h582.4c16 0 28.8-12.8 28.8-32v-64c0-19.2-12.8-32-28.8-32z" p-id="15419"></path></svg>

After

Width:  |  Height:  |  Size: 953 B

View File

@@ -17,7 +17,7 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
return [
{ key: 'extended-properties', value: gettext('Extended properties') }
];
}, [enableMetadataManagement, userPerm]);
}, [enableMetadataManagement, currentRepoInfo]);
const moreOperationClick = useCallback((operationKey) => {
if (operationKey === 'extended-properties') {

View File

@@ -9,7 +9,7 @@ import toaster from '../../components/toast';
import { gettext } from '../../utils/constants';
import { DetailEditor, CellFormatter } from '../metadata-view';
import { getColumnOriginName } from '../metadata-view/utils/column-utils';
import { CellType, getColumnOptions, getOptionName, PREDEFINED_COLUMN_KEYS } from '../metadata-view/_basic';
import { CellType, getColumnOptions, getOptionName, PREDEFINED_COLUMN_KEYS, getColumnOptionNamesByIds } from '../metadata-view/_basic';
import './index.css';
@@ -47,6 +47,8 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, emptyTip }) =
if (!PREDEFINED_COLUMN_KEYS.includes(field.key) && field.type === CellType.SINGLE_SELECT) {
const options = getColumnOptions(field);
update = { [fileName]: getOptionName(options, newValue) };
} else if (field.type === CellType.MULTIPLE_SELECT) {
update = { [fileName]: newValue ? getColumnOptionNamesByIds(field, newValue) : [] };
}
metadataAPI.modifyRecord(repoID, record._id, update, record._obj_id).then(res => {
const newMetadata = { ...metadata, record: { ...record, ...update } };
@@ -73,6 +75,9 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, emptyTip }) =
update = { [fileName]: newOption.id };
if (!PREDEFINED_COLUMN_KEYS.includes(fieldKey) && newField.type === CellType.SINGLE_SELECT) {
update = { [fileName]: getOptionName(options, newOption.id) };
} else if (newField.type === CellType.MULTIPLE_SELECT) {
const oldValue = getCellValueByColumn(record, newField) || [];
update = { [fileName]: [...oldValue, newOption.name] };
}
return metadataAPI.modifyRecord(repoID, record._id, update, record._obj_id);
}).then(res => {

View File

@@ -53,6 +53,7 @@ const NOT_SUPPORT_EDIT_COLUMN_TYPE_MAP = {
const MULTIPLE_CELL_VALUE_COLUMN_TYPE_MAP = {
[CellType.COLLABORATOR]: true,
[CellType.MULTIPLE_SELECT]: true,
};
const SINGLE_CELL_VALUE_COLUMN_TYPE_MAP = {
[CellType.TEXT]: true,

View File

@@ -13,6 +13,7 @@ const COLUMNS_ICON_CONFIG = {
[CellType.DATE]: 'date',
[CellType.LONG_TEXT]: 'long-text',
[CellType.SINGLE_SELECT]: 'single-select',
[CellType.MULTIPLE_SELECT]: 'multiple-select',
[CellType.NUMBER]: 'number',
[CellType.GEOLOCATION]: 'location',
};
@@ -30,6 +31,7 @@ const COLUMNS_ICON_NAME = {
[CellType.DATE]: 'Date',
[CellType.LONG_TEXT]: 'Long text',
[CellType.SINGLE_SELECT]: 'Single select',
[CellType.MULTIPLE_SELECT]: 'Multiple select',
[CellType.NUMBER]: 'Number',
[CellType.GEOLOCATION]: 'Geolocation',
};

View File

@@ -11,6 +11,7 @@ const CellType = {
DATE: 'date',
LONG_TEXT: 'long-text',
SINGLE_SELECT: 'single-select',
MULTIPLE_SELECT: 'multiple-select',
NUMBER: 'number',
GEOLOCATION: 'geolocation',
};

View File

@@ -72,6 +72,16 @@ const FILTER_COLUMN_OPTIONS = {
FILTER_PREDICATE_TYPE.NOT_EMPTY,
],
},
[CellType.MULTIPLE_SELECT]: {
filterPredicateList: [
FILTER_PREDICATE_TYPE.HAS_ANY_OF,
FILTER_PREDICATE_TYPE.HAS_ALL_OF,
FILTER_PREDICATE_TYPE.HAS_NONE_OF,
FILTER_PREDICATE_TYPE.IS_EXACTLY,
FILTER_PREDICATE_TYPE.EMPTY,
FILTER_PREDICATE_TYPE.NOT_EMPTY,
],
},
[CellType.CTIME]: {
filterPredicateList: datePredicates,
filterTermModifierList: dateTermModifiers,

View File

@@ -12,6 +12,7 @@ const SORT_COLUMN_OPTIONS = [
CellType.TEXT,
CellType.DATE,
CellType.SINGLE_SELECT,
CellType.MULTIPLE_SELECT,
CellType.COLLABORATOR,
CellType.CHECKBOX,
CellType.NUMBER,

View File

@@ -154,4 +154,6 @@ export {
isNumber,
getCellValueDisplayString,
getCellValueStringResult,
getColumnOptionNamesByIds,
getColumnOptionIdsByNames,
} from './utils';

View File

@@ -11,6 +11,8 @@ export {
export {
getOption,
getColumnOptionNameById,
getColumnOptionNamesByIds,
getColumnOptionIdsByNames,
getOptionName,
getMultipleOptionName,
} from './option';

View File

@@ -36,13 +36,46 @@ const getColumnOptionNameById = (column, optionId) => {
return getOptionName(options, optionId);
};
/**
* Get column option name by id
* @param {object} column e.g. { data: { options, ... }, ... }
* @param {array} optionIds
* @returns options name, array
*/
const getColumnOptionNamesByIds = (column, optionIds) => {
if (PRIVATE_COLUMN_KEYS.includes(column.key)) return optionIds;
if (!Array.isArray(optionIds) || optionIds.length === 0) return [];
const options = getColumnOptions(column);
if (!Array.isArray(options) || options.length === 0) return [];
return optionIds.map(optionId => getOptionName(options, optionId)).filter(name => name);
};
/**
* Get column option name by id
* @param {object} column e.g. { data: { options, ... }, ... }
* @param {array} option names
* @returns options id, array
*/
const getColumnOptionIdsByNames = (column, names) => {
if (PRIVATE_COLUMN_KEYS.includes(column.key)) return names;
if (!Array.isArray(names) || names.length === 0) return [];
const options = getColumnOptions(column);
if (!Array.isArray(options) || options.length === 0) return [];
return names.map(name => {
const option = getOption(options, name);
if (option) return option.id;
return null;
}).filter(name => name);
};
/**
* Get concatenated options names of given ids.
* @param {array} options e.g. [ { id, color, name, ... }, ... ]
* @param {array} targetOptionsIds e.g. [ option.id, ... ]
* @returns concatenated options names, string. e.g. 'name1, name2'
*/
const getMultipleOptionName = (options, targetOptionsIds) => {
const getMultipleOptionName = (column, targetOptionsIds) => {
const options = getColumnOptions(column);
if (!Array.isArray(targetOptionsIds) || !Array.isArray(options)) return '';
const selectedOptions = options.filter((option) => targetOptionsIds.includes(option.id));
if (selectedOptions.length === 0) return '';
@@ -53,5 +86,7 @@ export {
getOption,
getOptionName,
getColumnOptionNameById,
getColumnOptionNamesByIds,
getColumnOptionIdsByNames,
getMultipleOptionName,
};

View File

@@ -28,4 +28,6 @@ export {
getGeolocationByGranularity,
getFloatNumber,
isNumber,
getColumnOptionNamesByIds,
getColumnOptionIdsByNames,
} from './column';

View File

@@ -5,3 +5,4 @@ export { checkboxFilter } from './checkbox';
export { singleSelectFilter } from './single-select';
export { collaboratorFilter } from './collaborator';
export { numberFilter } from './number';
export { multipleSelectFilter } from './multiple-select';

View File

@@ -0,0 +1,54 @@
import { FILTER_PREDICATE_TYPE } from '../../../constants/filter/filter-predicate';
/**
* Filter multiple-select
* @param {array} optionIds e.g. [ option.id, ... ]
* @param {string} filter_predicate
* @param {array} filter_term option ids
* @returns bool
*/
const multipleSelectFilter = (optionIds, { filter_predicate, filter_term }) => {
switch (filter_predicate) {
case FILTER_PREDICATE_TYPE.HAS_ANY_OF: {
return (
filter_term.length === 0
|| (Array.isArray(optionIds) && optionIds.some((optionId) => filter_term.includes(optionId)))
);
}
case FILTER_PREDICATE_TYPE.HAS_ALL_OF: {
return (
filter_term.length === 0
|| (Array.isArray(optionIds) && filter_term.every((optionId) => optionIds.includes(optionId)))
);
}
case FILTER_PREDICATE_TYPE.HAS_NONE_OF: {
if (filter_term.length === 0 || !Array.isArray(optionIds) || optionIds.length === 0) {
return true;
}
return filter_term.every((optionId) => optionIds.indexOf(optionId) < 0);
}
case FILTER_PREDICATE_TYPE.IS_EXACTLY: {
if (filter_term.length === 0) {
return true;
}
if (!Array.isArray(optionIds)) {
return false;
}
const uniqueArr = (arr) => [...new Set(arr)].sort();
return uniqueArr(optionIds).toString() === uniqueArr(filter_term).toString();
}
case FILTER_PREDICATE_TYPE.EMPTY: {
return !Array.isArray(optionIds) || optionIds.length === 0;
}
case FILTER_PREDICATE_TYPE.NOT_EMPTY: {
return Array.isArray(optionIds) && optionIds.length > 0;
}
default: {
return false;
}
}
};
export {
multipleSelectFilter,
};

View File

@@ -10,6 +10,7 @@ import {
singleSelectFilter,
collaboratorFilter,
numberFilter,
multipleSelectFilter,
} from './filter-column';
import {
FILTER_CONJUNCTION_TYPE,
@@ -42,6 +43,9 @@ const getFilterResult = (row, filter, { username, userId }) => {
case CellType.SINGLE_SELECT: {
return singleSelectFilter(cellValue, filter);
}
case CellType.MULTIPLE_SELECT: {
return multipleSelectFilter(cellValue, filter);
}
case CellType.NUMBER: {
return numberFilter(cellValue, filter);
}

View File

@@ -5,6 +5,8 @@ import {
sortNumber,
sortCheckbox,
sortCollaborator,
sortSingleSelect,
sortMultipleSelect,
} from '../sort/sort-column';
import { MAX_GROUP_LEVEL } from '../../constants/group';
import {
@@ -45,6 +47,9 @@ const _getFormattedCellValue = (cellValue, groupby) => {
case CellType.SINGLE_SELECT: {
return cellValue || null;
}
case CellType.MULTIPLE_SELECT: {
return Array.isArray(cellValue) ? cellValue : [];
}
case CellType.COLLABORATOR: {
return Array.isArray(cellValue) ? cellValue : [];
}
@@ -98,8 +103,18 @@ const _findGroupIndex = (sCellValue, cellValue2GroupIndexMap, groupsLength) => {
const getSortedGroups = (groups, groupbys, level, collaborators = []) => {
const sortFlag = 0;
const { column, sort_type } = groupbys[level];
const { type: columnType } = column;
const { type: columnType, data: columnData } = column;
const normalizedSortType = sort_type || SORT_TYPE.UP;
let option_id_index_map = {};
if (columnType === CellType.SINGLE_SELECT || columnType === CellType.MULTIPLE_SELECT) {
const { options } = columnData || {};
if (Array.isArray(options)) {
options.forEach((option, index) => {
option_id_index_map[option.id] = index;
});
}
}
groups.sort((currGroupRow, nextGroupRow) => {
let { cell_value: currCellVal } = currGroupRow;
let { cell_value: nextCellVal } = nextGroupRow;
@@ -121,6 +136,10 @@ const getSortedGroups = (groups, groupbys, level, collaborators = []) => {
nextCollaborators = getCollaboratorsNames(nextCollaborators, collaborators);
}
sortResult = sortCollaborator(currCollaborators, nextCollaborators, normalizedSortType);
} else if (columnType === CellType.SINGLE_SELECT) {
sortResult = sortSingleSelect(currCellVal, nextCellVal, { sort_type: normalizedSortType, option_id_index_map });
} else if (columnType === CellType.MULTIPLE_SELECT) {
sortResult = sortMultipleSelect(currCellVal, nextCellVal, { sort_type: normalizedSortType, option_id_index_map });
}
return sortFlag || sortResult;
}

View File

@@ -25,6 +25,8 @@ export {
isNumber,
getCellValueDisplayString,
getCellValueStringResult,
getColumnOptionNamesByIds,
getColumnOptionIdsByNames,
} from './cell';
export {
getColumnType,

View File

@@ -61,7 +61,8 @@ const deleteInvalidSort = (sorts, columns) => {
let newSort = { ...sort, column: sortColumn };
const { type: columnType } = sortColumn;
switch (columnType) {
case CellType.SINGLE_SELECT: {
case CellType.SINGLE_SELECT:
case CellType.MULTIPLE_SELECT: {
const options = getColumnOptions(sortColumn);
let option_id_index_map = {};
options.forEach((option, index) => {

View File

@@ -13,6 +13,7 @@ export {
sortCollaborator,
sortNumber,
sortSingleSelect,
sortMultipleSelect,
} from './sort-column';
export {

View File

@@ -8,3 +8,4 @@ export { sortCheckbox } from './checkbox';
export { sortCollaborator } from './collaborator';
export { sortNumber } from './number';
export { sortSingleSelect } from './single-select';
export { sortMultipleSelect } from './multiple-select';

View File

@@ -0,0 +1,50 @@
import { getMultipleIndexesOrderbyOptions } from '../core';
import { SORT_TYPE } from '../../../constants/sort';
/**
* Sort multiple-select
* @param {array} leftOptionIds the ids of options
* @param {array} rightOptionIds
* @param {string} sort_type e.g. 'up' | 'down'
* @param {object} option_id_index_map e.g. { [option.id]: 0, ... }
* @returns number
*/
const sortMultipleSelect = (leftOptionIds, rightOptionIds, { sort_type, option_id_index_map }) => {
const emptyLeftOptionIds = !leftOptionIds || leftOptionIds.length === 0;
const emptyRightOptionIds = !rightOptionIds || rightOptionIds.length === 0;
if (emptyLeftOptionIds && emptyRightOptionIds) return 0;
if (emptyLeftOptionIds) return 1;
if (emptyRightOptionIds) return -1;
const leftOptionIndexes = getMultipleIndexesOrderbyOptions(leftOptionIds, option_id_index_map);
const rightOptionIndexes = getMultipleIndexesOrderbyOptions(rightOptionIds, option_id_index_map);
const leftOptionsLen = leftOptionIndexes.length;
const rightOptionsLen = rightOptionIndexes.length;
// current multiple select equal to next multiple select.
if (
leftOptionsLen === rightOptionsLen
&& (leftOptionsLen === 0 || leftOptionIndexes.join('') === rightOptionIndexes.join(''))
) {
return 0;
}
const len = Math.min(leftOptionsLen, rightOptionsLen);
for (let i = 0; i < len; i++) {
if (leftOptionIndexes[i] > rightOptionIndexes[i]) {
return sort_type === SORT_TYPE.UP ? 1 : -1;
}
if (leftOptionIndexes[i] < rightOptionIndexes[i]) {
return sort_type === SORT_TYPE.UP ? -1 : 1;
}
}
if (leftOptionsLen > rightOptionsLen) {
return sort_type === SORT_TYPE.UP ? 1 : -1;
}
return sort_type === SORT_TYPE.UP ? -1 : 1;
};
export {
sortMultipleSelect,
};

View File

@@ -6,6 +6,7 @@ import {
sortNumber,
sortCollaborator,
sortCheckbox,
sortMultipleSelect,
} from './sort-column';
import { CellType, DATE_COLUMN_OPTIONS } from '../../constants/column';
import { getCellValueByColumn, getCollaboratorsNames } from '../cell';
@@ -31,6 +32,8 @@ const sortRowsWithMultiSorts = (tableRows, sorts, { collaborators }) => {
initValue = initValue || sortSingleSelect(currCellVal, nextCellVal, sort);
} else if (NUMBER_SORTER_COLUMN_TYPES.includes(columnType)) {
initValue = initValue || sortNumber(currCellVal, nextCellVal, sort_type);
} else if (columnType === CellType.MULTIPLE_SELECT) {
initValue = initValue || sortMultipleSelect(currCellVal, nextCellVal, sort);
} else if (columnType === CellType.COLLABORATOR) {
let currValidCollaborators = currCellVal;
let nextValidCollaborators = nextCellVal;

View File

@@ -145,10 +145,9 @@ class ValidateFilter {
}
// Filter predicate should support: is_empty/is_not_empty(excludes checkbox and bool)
if (CHECK_EMPTY_PREDICATES.includes(predicate)) {
return true;
}
if (array_type === CellType.SINGLE_SELECT) {
if (CHECK_EMPTY_PREDICATES.includes(predicate)) return true;
if (array_type === CellType.SINGLE_SELECT || array_type === CellType.DEPARTMENT_SINGLE_SELECT) {
return this.validatePredicate(predicate, { type: CellType.MULTIPLE_SELECT });
}
if (COLLABORATOR_COLUMN_TYPES.includes(array_type)) {
@@ -272,6 +271,15 @@ class ValidateFilter {
// invalid filter_term if selected option is deleted
return !!options.find((option) => term === option.id);
}
case CellType.MULTIPLE_SELECT: {
if (!this.isValidTermType(term, TERM_TYPE_MAP.ARRAY)) {
return false;
}
// contains deleted option(s)
const options = getColumnOptions(filterColumn);
return this.isValidSelectedOptions(term, options);
}
default: {
return false;
}
@@ -299,9 +307,6 @@ class ValidateFilter {
type: CellType.MULTIPLE_SELECT, data: array_data,
});
}
if (array_type === CellType.DEPARTMENT_SINGLE_SELECT) {
return this.isValidTermType(term, TERM_TYPE_MAP.ARRAY);
}
if (COLLABORATOR_COLUMN_TYPES.includes(array_type)) {
return this.isValidTerm(term, predicate, modifier, { type: CellType.COLLABORATOR });
}

View File

@@ -2,8 +2,9 @@
background-color: #f6f6f6;
border-bottom: 1px solid #dde2ea;
border-radius: 3px 3px 0 0;
min-height: 34px;
min-height: 35px;
padding: 5px 10px;
line-height: 1;
}
.collaborator {
@@ -46,6 +47,11 @@
font-size: 12px;
}
.sf-metadata-delete-collaborator .collaborator {
margin-top: 2px;
margin-bottom: 2px;
}
.sf-metadata-delete-collaborator .collaborator .collaborator-remove {
height: 14px;
width: 14px;

View File

@@ -9,6 +9,7 @@ const POPUP_EDITOR_COLUMN_TYPES = [
CellType.DATE,
CellType.COLLABORATOR,
CellType.SINGLE_SELECT,
CellType.MULTIPLE_SELECT,
CellType.LONG_TEXT,
];

View File

@@ -2,15 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNameById, PRIVATE_COLUMN_KEYS } from '../../../_basic';
import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNameById, PRIVATE_COLUMN_KEYS,
getColumnOptionNamesByIds,
} from '../../../_basic';
import { isCellValueChanged } from '../../../utils/cell-comparer';
import { EVENT_BUS_TYPE } from '../../../constants';
import Editor from '../editor';
import { canEditCell } from '../../../utils/column-utils';
const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER,
CellType.FILE_NAME, CellType.COLLABORATOR, CellType.LONG_TEXT, CellType.SINGLE_SELECT,
CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER, CellType.FILE_NAME
];
class PopupEditorContainer extends React.Component {
@@ -146,6 +147,8 @@ class PopupEditorContainer extends React.Component {
let updated = columnType === CellType.DATE ? { [columnKey]: newValue } : newValue;
if (columnType === CellType.SINGLE_SELECT) {
updated[columnKey] = newValue[columnKey] ? getColumnOptionNameById(column, newValue[columnKey]) : '';
} else if (columnType === CellType.MULTIPLE_SELECT) {
updated[columnKey] = newValue[columnKey] ? getColumnOptionNamesByIds(column, newValue[columnKey]) : [];
}
this.commitData(updated, true);

View File

@@ -6,6 +6,7 @@ import FileNameEditor from './file-name-editor';
import TextEditor from './text-editor';
import NumberEditor from './number-editor';
import SingleSelectEditor from './single-select-editor';
import MultipleSelectEditor from './multiple-select-editor';
import CollaboratorEditor from './collaborator-editor';
// eslint-disable-next-line react/display-name
@@ -28,6 +29,9 @@ const Editor = React.forwardRef((props, ref) => {
case CellType.SINGLE_SELECT: {
return (<SingleSelectEditor ref={ref} {...props} />);
}
case CellType.MULTIPLE_SELECT: {
return (<MultipleSelectEditor ref={ref} {...props} />);
}
case CellType.COLLABORATOR: {
return (<CollaboratorEditor ref={ref} {...props} />);
}

View File

@@ -0,0 +1,25 @@
.sf-metadata-delete-select-options {
background-color: #f6f6f6;
border-bottom: 1px solid #dde2ea;
border-radius: 3px 3px 0 0;
min-height: 35px;
padding: 2px 10px;
line-height: 1;
}
.sf-metadata-delete-select-options .sf-metadata-delete-select-option {
margin-top: 5px;
margin-bottom: 5px;
align-items: center;
}
.sf-metadata-delete-select-options .sf-metadata-delete-select-option .sf-metadata-delete-select-remove {
height: 14px;
width: 14px;
margin-left: 2px;
}
.sf-metadata-delete-select-options .sf-metadata-delete-select-option .sf-metadata-icon-x-01 {
fill: inherit;
font-size: 12px;
}

View File

@@ -0,0 +1,59 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import { gettext } from '../../../../utils';
import { DELETED_OPTION_TIPS, DELETED_OPTION_BACKGROUND_COLOR } from '../../../../constants';
import './index.css';
const DeleteOption = ({ value, options, onDelete }) => {
const displayOptions = useMemo(() => {
if (!Array.isArray(value) || value.length === 0) return [];
const selectedOptions = options.filter((option) => value.includes(option.id) || value.includes(option.name));
const invalidOptionIds = value.filter(optionId => optionId && !options.find(o => o.id === optionId || o.name === optionId));
const invalidOptions = invalidOptionIds.map(optionId => ({
id: optionId,
name: gettext(DELETED_OPTION_TIPS),
color: DELETED_OPTION_BACKGROUND_COLOR,
}));
return [...selectedOptions, ...invalidOptions];
}, [options, value]);
if (displayOptions.length === 0) return null;
return (
<div className="sf-metadata-delete-select-options">
{displayOptions.map(option => {
if (!option) return null;
const { id, name } = option;
const style = {
display: 'inline-flex',
padding: '0px 10px',
height: '20px',
lineHeight: '20px',
textAlign: 'center',
borderRadius: '10px',
maxWidth: '250px',
fontSize: 13,
backgroundColor: option.color,
color: option.textColor || null,
fill: option.textColor || '#666',
};
return (
<div key={id} className="sf-metadata-delete-select-option" style={style}>
<span className="sf-metadata-delete-select-option-name text-truncate" title={name} aria-label={name}>{name}</span>
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(id, event)} iconName="x-01" />
</div>
);
})}
</div>
);
};
DeleteOption.propTypes = {
value: PropTypes.array.isRequired,
onDelete: PropTypes.func.isRequired
};
export default DeleteOption;

View File

@@ -0,0 +1,278 @@
import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
import { isFunction, getColumnOptions, getColumnOptionIdsByNames } from '../../../_basic';
import { generateNewOption } from '../../../utils/select-utils';
import { KeyCodes } from '../../../../../constants';
import { gettext } from '../../../../../utils/constants';
import DeleteOption from './delete-options';
import './index.css';
const MultipleSelectEditor = forwardRef(({
saveImmediately,
column,
value: oldValue,
onCommit,
onPressTab,
modifyColumnData,
}, ref) => {
const [value, setValue] = useState(getColumnOptionIdsByNames(column, oldValue));
const [searchValue, setSearchValue] = useState('');
const [highlightIndex, setHighlightIndex] = useState(-1);
const [maxItemNum, setMaxItemNum] = useState(0);
const itemHeight = 30;
const editorContainerRef = useRef(null);
const editorRef = useRef(null);
const selectItemRef = useRef(null);
const canEditData = window.sfMetadataContext.canModifyColumnData(column);
const options = useMemo(() => {
return getColumnOptions(column);
}, [column]);
const displayOptions = useMemo(() => {
if (!searchValue) return options;
const value = searchValue.toLowerCase().trim();
if (!value) return options;
return options.filter((item) => item.name && item.name.toLowerCase().indexOf(value) > -1);
}, [searchValue, options]);
const isShowCreateBtn = useMemo(() => {
if (!canEditData || !searchValue) return false;
return displayOptions.findIndex(option => option.name === searchValue) === -1 ? true : false;
}, [canEditData, displayOptions, searchValue]);
const style = useMemo(() => {
return { width: column.width };
}, [column]);
const blur = useCallback(() => {
onCommit && onCommit(value);
}, [value, onCommit]);
const onChangeSearch = useCallback((newSearchValue) => {
if (searchValue === newSearchValue) return;
setSearchValue(newSearchValue);
}, [searchValue]);
const onSelectOption = useCallback((optionId) => {
const newValue = value.slice(0);
let optionIdx = value.indexOf(optionId);
if (optionIdx > -1) {
newValue.splice(optionIdx, 1);
} else {
newValue.push(optionId);
}
setValue(newValue);
if (saveImmediately) {
onCommit && onCommit(newValue);
}
}, [saveImmediately, value, onCommit]);
const onMenuMouseEnter = useCallback((highlightIndex) => {
setHighlightIndex(highlightIndex);
}, []);
const onMenuMouseLeave = useCallback((index) => {
setHighlightIndex(-1);
}, []);
const createOption = useCallback((event) => {
event && event.stopPropagation();
event && event.nativeEvent.stopImmediatePropagation();
const newOption = generateNewOption(options, searchValue?.trim() || '');
let newOptions = options.slice(0);
newOptions.push(newOption);
modifyColumnData(column.key, { options: newOptions }, { options: column.data.options || [] });
onSelectOption(newOption.id);
}, [column, searchValue, options, onSelectOption, modifyColumnData]);
const onDeleteOption = useCallback((optionId) => {
const newValue = value.slice(0);
const index = newValue.indexOf(optionId);
if (index > -1) {
newValue.splice(index, 1);
}
setValue(newValue);
if (saveImmediately) {
onCommit && onCommit(newValue);
}
}, [saveImmediately, value, onCommit]);
const getMaxItemNum = useCallback(() => {
let selectContainerStyle = getComputedStyle(editorContainerRef.current, null);
let selectItemStyle = getComputedStyle(selectItemRef.current, null);
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(selectItemStyle.height));
return maxSelectItemNum - 1;
}, [editorContainerRef, selectItemRef]);
const onEnter = useCallback((event) => {
event.preventDefault();
let option;
if (displayOptions.length === 1) {
option = displayOptions[0];
} else if (highlightIndex > -1) {
option = displayOptions[highlightIndex];
}
if (option) {
let newOptionId = option.id;
if (value === option.id) newOptionId = null;
onSelectOption(newOptionId);
return;
}
let isShowCreateBtn = false;
if (searchValue) {
isShowCreateBtn = canEditData && displayOptions.findIndex(option => option.name === searchValue) === -1 ? true : false;
}
if (!isShowCreateBtn || displayOptions.length === 0) return;
createOption();
}, [canEditData, displayOptions, highlightIndex, value, searchValue, onSelectOption, createOption]);
const onUpArrow = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
if (highlightIndex === 0) return;
setHighlightIndex(highlightIndex - 1);
if (highlightIndex > displayOptions.length - maxItemNum) {
editorContainerRef.current.scrollTop -= itemHeight;
}
}, [editorContainerRef, highlightIndex, maxItemNum, displayOptions, itemHeight]);
const onDownArrow = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
if (highlightIndex === displayOptions.length - 1) return;
setHighlightIndex(highlightIndex + 1);
if (highlightIndex >= maxItemNum) {
editorContainerRef.current.scrollTop += itemHeight;
}
}, [editorContainerRef, highlightIndex, maxItemNum, displayOptions, itemHeight]);
const onHotKey = useCallback((event) => {
if (event.keyCode === KeyCodes.Enter) {
onEnter(event);
} else if (event.keyCode === KeyCodes.UpArrow) {
onUpArrow(event);
} else if (event.keyCode === KeyCodes.DownArrow) {
onDownArrow(event);
} else if (event.keyCode === KeyCodes.Tab) {
if (isFunction(onPressTab)) {
onPressTab(event);
}
}
}, [onEnter, onUpArrow, onDownArrow, onPressTab]);
const onKeyDown = useCallback((event) => {
if (
event.keyCode === KeyCodes.ChineseInputMethod ||
event.keyCode === KeyCodes.Enter ||
event.keyCode === KeyCodes.LeftArrow ||
event.keyCode === KeyCodes.RightArrow
) {
event.stopPropagation();
}
}, []);
useEffect(() => {
if (editorRef.current) {
const { bottom } = editorRef.current.getBoundingClientRect();
if (bottom > window.innerHeight) {
editorRef.current.style.top = (parseInt(editorRef.current.style.top) - bottom + window.innerHeight) + 'px';
}
}
if (editorContainerRef.current && selectItemRef.current) {
setMaxItemNum(getMaxItemNum());
}
document.addEventListener('keydown', onHotKey, true);
return () => {
document.removeEventListener('keydown', onHotKey, true);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onHotKey]);
useEffect(() => {
const highlightIndex = displayOptions.length === 0 ? -1 : 0;
setHighlightIndex(highlightIndex);
}, [displayOptions]);
useImperativeHandle(ref, () => ({
getValue: () => {
const { key } = column;
return { [key]: value };
},
onBlur: () => blur(),
}), [column, value, blur]);
const renderOptions = useCallback(() => {
if (displayOptions.length === 0) {
const noOptionsTip = searchValue ? gettext('No options available') : gettext('No option');
return (<span className="none-search-result">{noOptionsTip}</span>);
}
return displayOptions.map((option, i) => {
const isSelected = value.includes(option.id);
return (
<div key={option.id} className="sf-metadata-single-select-item" ref={selectItemRef}>
<div
className={classnames('single-select-container', { 'single-select-container-highlight': i === highlightIndex })}
onMouseDown={() => onSelectOption(option.id)}
onMouseEnter={() => onMenuMouseEnter(i)}
onMouseLeave={() => onMenuMouseLeave(i)}
>
<div className="single-select">
<span
className="single-select-name"
style={{ backgroundColor: option.color, color: option.textColor || null }}
title={option.name}
aria-label={option.name}
>
{option.name}
</span>
</div>
<div className="single-select-check-icon">
{isSelected && (<Icon iconName="check-mark" />)}
</div>
</div>
</div>
);
});
}, [displayOptions, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]);
return (
<div className="sf-metadata-single-select-editor sf-metadata-multiple-select-editor" style={style} ref={editorRef}>
<DeleteOption value={value} options={options} onDelete={onDeleteOption} />
<div className="sf-metadata-search-single-select-options">
<SearchInput
placeholder={gettext('Search option')}
onKeyDown={onKeyDown}
onChange={onChangeSearch}
autoFocus={true}
className="sf-metadata-search-options"
/>
</div>
<div className="sf-metadata-single-select-editor-container" ref={editorContainerRef}>
{renderOptions()}
</div>
{isShowCreateBtn && (
<CustomizeAddTool
callBack={createOption}
footerName={`${gettext('Add option')} ${searchValue}`}
className="add-search-result"
/>
)}
</div>
);
});
MultipleSelectEditor.propTypes = {
column: PropTypes.object,
value: PropTypes.array,
onCommit: PropTypes.func,
onPressTab: PropTypes.func,
};
export default MultipleSelectEditor;

View File

@@ -5,6 +5,7 @@ import CheckboxEditor from './checkbox-editor';
import TextEditor from './text-editor';
import NumberEditor from './number-editor';
import SingleSelectEditor from './single-select-editor';
import MultipleSelectEditor from './multiple-select-editor';
import CollaboratorEditor from './collaborator-editor';
import DateEditor from './date-editor';
import { lang } from '../../../../utils/constants';
@@ -16,7 +17,6 @@ const DetailEditor = ({ field, onChange: onChangeAPI, ...props }) => {
onChangeAPI(field.key, newValue);
}, [field, onChangeAPI]);
switch (field.type) {
case CellType.CHECKBOX: {
return (<CheckboxEditor { ...props } field={field} onChange={onChange} />);
@@ -33,6 +33,9 @@ const DetailEditor = ({ field, onChange: onChangeAPI, ...props }) => {
case CellType.SINGLE_SELECT: {
return (<SingleSelectEditor { ...props } field={field} onChange={onChange} />);
}
case CellType.MULTIPLE_SELECT: {
return (<MultipleSelectEditor { ...props } field={field} onChange={onChange} />);
}
case CellType.COLLABORATOR: {
return (<CollaboratorEditor { ...props } field={field} onChange={onChange} />);
}

View File

@@ -0,0 +1,21 @@
.sf-metadata-multiple-select-property-detail-editor {
min-height: 34px;
width: 100%;
height: auto;
}
.sf-metadata-multiple-select-property-detail-editor .sf-metadata-delete-select-options {
min-height: 34px;
border-bottom: none;
background-color: inherit;
border-radius: unset;
padding: 2px 6px;
}
.sf-metadata-multiple-select-property-detail-editor .sf-metadata-delete-select-options .sf-metadata-delete-select-option {
margin-right: 10px;
}
.sf-metadata-multiple-select-property-editor-popover .sf-metadata-delete-select-options {
display: none;
}

View File

@@ -0,0 +1,106 @@
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Popover } from 'reactstrap';
import { getColumnOptionIdsByNames, getColumnOptions, KeyCodes } from '../../../_basic';
import { getEventClassName, gettext } from '../../../utils';
import Editor from '../../cell-editor/multiple-select-editor';
import DeleteOptions from '../../cell-editor/multiple-select-editor/delete-options';
import './index.css';
const MultipleSelectEditor = ({ field, value, record, fields, onChange, modifyColumnData }) => {
const ref = useRef(null);
const [showEditor, setShowEditor] = useState(false);
const options = useMemo(() => getColumnOptions(field), [field]);
const onClick = useCallback((event) => {
if (!event.target) return;
const className = getEventClassName(event);
if (className.indexOf('sf-metadata-search-options') > -1) return;
const dom = document.querySelector('.sf-metadata-multiple-select-editor');
if (!dom) return;
if (dom.contains(event.target)) return;
if (ref.current && !ref.current.contains(event.target) && showEditor) {
setShowEditor(false);
}
}, [showEditor]);
const onHotKey = useCallback((event) => {
if (event.keyCode === KeyCodes.Esc) {
if (showEditor) {
setShowEditor(false);
}
}
}, [showEditor]);
useEffect(() => {
document.addEventListener('mousedown', onClick);
document.addEventListener('keydown', onHotKey, true);
return () => {
document.removeEventListener('mousedown', onClick);
document.removeEventListener('keydown', onHotKey, true);
};
}, [onClick, onHotKey]);
const openEditor = useCallback(() => {
setShowEditor(true);
}, []);
const deleteOption = useCallback((id, event) => {
event && event.stopPropagation();
event && event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
const oldValue = getColumnOptionIdsByNames(field, value);
const newValue = oldValue.filter(c => c !== id);
onChange(newValue);
}, [field, value, onChange]);
const onCommit = useCallback((newValue) => {
onChange(newValue);
}, [onChange]);
const renderEditor = useCallback(() => {
if (!showEditor) return null;
const { width } = ref.current.getBoundingClientRect();
return (
<Popover
target={ref}
isOpen={true}
placement="bottom-end"
hideArrow={true}
fade={false}
className="sf-metadata-property-editor-popover sf-metadata-single-select-property-editor-popover sf-metadata-multiple-select-property-editor-popover"
boundariesElement={document.body}
>
<Editor
saveImmediately={true}
value={value}
column={{ ...field, width: Math.max(width - 2, 200) }}
columns={fields}
modifyColumnData={modifyColumnData}
record={record}
onCommit={onCommit}
/>
</Popover>
);
}, [showEditor, onCommit, record, value, modifyColumnData, fields, field]);
return (
<div
className="sf-metadata-property-detail-editor sf-metadata-single-select-property-detail-editor sf-metadata-multiple-select-property-detail-editor"
placeholder={gettext('Empty')}
ref={ref}
onClick={openEditor}
>
<DeleteOptions value={value} options={options} onDelete={deleteOption} />
{renderEditor()}
</div>
);
};
MultipleSelectEditor.propTypes = {
field: PropTypes.object.isRequired,
value: PropTypes.array,
onChange: PropTypes.func.isRequired,
};
export default MultipleSelectEditor;

View File

@@ -69,7 +69,7 @@ const ColumnPopover = ({ target, onChange }) => {
if (Object.keys(data).length === 0) {
data = null;
if (!column.unique) {
if (column.type === CellType.SINGLE_SELECT) {
if (column.type === CellType.SINGLE_SELECT || column.type === CellType.MULTIPLE_SELECT) {
data = { options: [] };
} else if (column.type === CellType.DATE) {
data = { format: DEFAULT_DATE_FORMAT };

View File

@@ -18,12 +18,13 @@ const COLUMNS = [
{ icon: COLUMNS_ICON_CONFIG[CellType.CHECKBOX], type: CellType.CHECKBOX, name: getColumnDisplayName(PRIVATE_COLUMN_KEY.FILE_EXPIRED), unique: true, key: PRIVATE_COLUMN_KEY.FILE_EXPIRED, canChangeName: false, groupby: 'predefined' },
{ icon: COLUMNS_ICON_CONFIG[CellType.SINGLE_SELECT], type: CellType.SINGLE_SELECT, name: getColumnDisplayName(PRIVATE_COLUMN_KEY.FILE_STATUS), unique: true, key: PRIVATE_COLUMN_KEY.FILE_STATUS, canChangeName: false, groupby: 'predefined' },
{ icon: COLUMNS_ICON_CONFIG[CellType.TEXT], type: CellType.TEXT, name: gettext(COLUMNS_ICON_NAME[CellType.TEXT]), canChangeName: true, key: CellType.TEXT, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.CHECKBOX], type: CellType.CHECKBOX, name: gettext(COLUMNS_ICON_NAME[CellType.CHECKBOX]), canChangeName: true, key: CellType.CHECKBOX, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.COLLABORATOR], type: CellType.COLLABORATOR, name: gettext(COLUMNS_ICON_NAME[CellType.COLLABORATOR]), canChangeName: true, key: CellType.COLLABORATOR, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.DATE], type: CellType.DATE, name: gettext(COLUMNS_ICON_NAME[CellType.DATE]), canChangeName: true, key: CellType.DATE, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.LONG_TEXT], type: CellType.LONG_TEXT, name: gettext(COLUMNS_ICON_NAME[CellType.LONG_TEXT]), canChangeName: true, key: CellType.LONG_TEXT, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.SINGLE_SELECT], type: CellType.SINGLE_SELECT, name: gettext(COLUMNS_ICON_NAME[CellType.SINGLE_SELECT]), canChangeName: true, key: CellType.SINGLE_SELECT, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.NUMBER], type: CellType.NUMBER, name: gettext(COLUMNS_ICON_NAME[CellType.NUMBER]), canChangeName: true, key: CellType.NUMBER, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.COLLABORATOR], type: CellType.COLLABORATOR, name: gettext(COLUMNS_ICON_NAME[CellType.COLLABORATOR]), canChangeName: true, key: CellType.COLLABORATOR, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.CHECKBOX], type: CellType.CHECKBOX, name: gettext(COLUMNS_ICON_NAME[CellType.CHECKBOX]), canChangeName: true, key: CellType.CHECKBOX, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.DATE], type: CellType.DATE, name: gettext(COLUMNS_ICON_NAME[CellType.DATE]), canChangeName: true, key: CellType.DATE, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.SINGLE_SELECT], type: CellType.SINGLE_SELECT, name: gettext(COLUMNS_ICON_NAME[CellType.SINGLE_SELECT]), canChangeName: true, key: CellType.SINGLE_SELECT, groupby: 'basics' },
{ icon: COLUMNS_ICON_CONFIG[CellType.MULTIPLE_SELECT], type: CellType.MULTIPLE_SELECT, name: gettext(COLUMNS_ICON_NAME[CellType.MULTIPLE_SELECT]), canChangeName: true, key: CellType.MULTIPLE_SELECT, groupby: 'basics' },
];
// eslint-disable-next-line react/display-name

View File

@@ -481,6 +481,10 @@ class FilterItem extends React.Component {
/>
);
}
case CellType.MULTIPLE_SELECT: {
let { options = [] } = filterColumn.data || {};
return this.renderMultipleSelectOption(options, filter_term, readOnly);
}
default: {
return null;
}
@@ -523,9 +527,9 @@ class FilterItem extends React.Component {
} else if (isCheckboxColumn(filterColumn)) {
_isCheckboxColumn = true;
}
const isContainPredicate = [FILTER_PREDICATE_TYPE.CONTAINS, FILTER_PREDICATE_TYPE.NOT_CONTAIN].includes(filter_predicate);
const isRenderErrorTips = this.isRenderErrorTips();
const showToolTip = isContainPredicate && !isRenderErrorTips;
// const isContainPredicate = [].includes(filterColumn.type) && [FILTER_PREDICATE_TYPE.CONTAINS, FILTER_PREDICATE_TYPE.NOT_CONTAIN].includes(filter_predicate);
// const isRenderErrorTips = this.isRenderErrorTips();
// const showToolTip = isContainPredicate && !isRenderErrorTips;
// current predicate is not empty
const isNeedShowTermModifier = !EMPTY_PREDICATE.includes(filter_predicate);
@@ -574,12 +578,14 @@ class FilterItem extends React.Component {
<div className="filter-term ml-2">
{this.renderFilterTerm(filterColumn)}
</div>
{showToolTip &&
{/* {showToolTip && (
<div className="ml-2" >
<span ref={this.filterToolTip} id="filter_tool_tip" aria-hidden="true" className="sf-metadata-font sf-metadata-icon-exclamation-triangle" style={{ color: '#FFC92C' }}></span>
{/* <UncontrolledTooltip placement="bottom" target={this.filterToolTip}></UncontrolledTooltip> */}
<IconBtn id={`filter-tool-tip-${filterColumn.key}`} iconName="exclamation-triangle" style={{ color: '#FFC92C' }} />
<UncontrolledTooltip placement="bottom" target={`filter-tool-tip-${filterColumn.key}`} >
{gettext('If there are multiple items in the cell, a random one will be chosen and be compared with the filter value.')}
</UncontrolledTooltip>
</div>
}
)} */}
{this.renderErrorMessage()}
</div>
</div>

View File

@@ -1,5 +1,6 @@
/* basic css */
.sf-metadata-single-select-option {
.sf-metadata-single-select-option,
.sf-metadata-multiple-select-option {
border-radius: 10px;
font-size: 13px;
line-height: 20px;
@@ -12,5 +13,3 @@
white-space: nowrap;
width: min-content;
}

View File

@@ -7,11 +7,11 @@ import GridUtils from '../../../utils/grid-utils';
import './index.css';
const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, searchResult, recordGetterByIndex, recordGetterById, ...params }) => {
const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, searchResult, recordGetterByIndex, recordGetterById, modifyColumnData, ...params }) => {
const gridUtils = useMemo(() => {
return new GridUtils(metadata, { modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById });
}, [metadata, modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById]);
return new GridUtils(metadata, { modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById, modifyColumnData });
}, [metadata, modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById, modifyColumnData]);
const groupbysCount = useMemo(() => {
const groupbys = metadata?.view?.groupbys || [];
@@ -63,6 +63,7 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
getCopiedRecordsAndColumnsFromRange={getCopiedRecordsAndColumnsFromRange}
recordGetterById={recordGetterById}
recordGetterByIndex={recordGetterByIndex}
modifyColumnData={modifyColumnData}
{...params}
/>
</div>

View File

@@ -45,6 +45,29 @@ const GroupTitle = ({ column, cellValue, originalCellValue }) => {
const optionName = selectedOption ? selectedOption.name : deletedOptionTip;
return (<div className="sf-metadata-single-select-option" style={style} key={cellValue} title={optionName}>{optionName}</div>);
}
case CellType.MULTIPLE_SELECT: {
const options = getColumnOptions(column);
if (options.length === 0 || !Array.isArray(originalCellValue) || originalCellValue.length === 0) return emptyTip;
const selectedOptions = options.filter((option) => originalCellValue.includes(option.id) || originalCellValue.includes(option.name));
const invalidOptionIds = originalCellValue.filter(optionId => optionId && !options.find(o => o.id === optionId || o.name === optionId));
const invalidOptions = invalidOptionIds.map(optionId => ({
id: optionId,
name: deletedOptionTip,
color: DELETED_OPTION_BACKGROUND_COLOR,
}));
return (
<>
{selectedOptions.map(option => {
const style = { backgroundColor: option.color, color: option.textColor };
return (<div className="sf-metadata-multiple-select-option" style={style} key={option.id} title={option.name}>{option.name}</div>);
})}
{invalidOptions.map(option => {
const style = { backgroundColor: option.color };
return (<div className="sf-metadata-multiple-select-option" style={style} key={option.id} title={option.name}>{option.name}</div>);
})}
</>
);
}
default: {
return cellValue || emptyTip;
}

View File

@@ -213,6 +213,14 @@
margin-right: 0;
}
.canvas-groups-rows .group-title .sf-metadata-multiple-select-option {
margin: 0 10px 0 0;
}
.canvas-groups-rows .group-title .sf-metadata-multiple-select-option:last-child {
margin-right: 0;
}
.canvas-groups-rows .collaborator-avatar,
.canvas-groups-rows .sf-metadata-ui.collaborator-item .collaborator-avatar {
height: 16px;

View File

@@ -181,6 +181,16 @@ const HeaderDropdownMenu = ({ column, renameColumn, modifyColumnData, deleteColu
/> */}
</>
)}
{type === CellType.MULTIPLE_SELECT && (
<DropdownItem
disabled={!canModifyColumnData}
target="sf-metadata-edit-column-options"
iconName="multiple-select"
title={gettext('Edit multiple select')}
tip={gettext('You do not have permission')}
onChange={openOptionPopover}
/>
)}
{type === CellType.NUMBER && (
<DropdownItem
disabled={!canModifyColumnData}
@@ -194,7 +204,7 @@ const HeaderDropdownMenu = ({ column, renameColumn, modifyColumnData, deleteColu
{type === CellType.DATE && (
<>{renderDateFormat(canModifyColumnData)}</>
)}
{[CellType.DATE, CellType.SINGLE_SELECT, CellType.NUMBER].includes(column.type) && (
{[CellType.DATE, CellType.SINGLE_SELECT, CellType.NUMBER, CellType.MULTIPLE_SELECT].includes(column.type) && (
<DefaultDropdownItem key="divider-item" divider />
)}
<DropdownItem

View File

@@ -1,45 +1,17 @@
import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString,
PREDEFINED_COLUMN_KEYS, getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber, getColumnOptions,
generatorCellOptions,
getColumnOptionNamesByIds,
} from '../_basic';
import { formatTextToDate } from './date';
const SUPPORT_PASTE_FROM_COLUMN = {
[CellType.MULTIPLE_SELECT]: [CellType.MULTIPLE_SELECT, CellType.TEXT, CellType.SINGLE_SELECT],
[CellType.NUMBER]: [CellType.TEXT, CellType.NUMBER],
};
const reg_chinese_date_format = /(\d{4})年(\d{1,2})月(\d{1,2})日$/;
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: {
@@ -138,6 +110,12 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn)
fromOptionName = getOptionName(fromOptions, cellValue) || '';
break;
}
case CellType.MULTIPLE_SELECT: {
const copiedOptions = getColumnOptions(fromColumn);
const copiedCellVal = cellValue[0];
fromOptionName = getOptionName(copiedOptions, copiedCellVal) || '';
break;
}
case CellType.TEXT: {
fromOptionName = cellValue;
break;
@@ -292,4 +270,94 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) {
}
}
const _getPasteMultipleSelect = (copiedCellVal, pasteCellVal, copiedColumn, pasteColumn) => {
const { type: copiedColumnType } = copiedColumn;
if (!copiedCellVal ||
(Array.isArray(copiedCellVal) && copiedCellVal.length === 0) ||
!SUPPORT_PASTE_FROM_COLUMN[CellType.MULTIPLE_SELECT].includes(copiedColumnType)
) {
return { selectedOptionIds: pasteCellVal };
}
let copiedOptionNames = [];
if (copiedColumnType === CellType.MULTIPLE_SELECT) {
const copiedOptions = getColumnOptions(copiedColumn);
copiedOptionNames = copiedOptions.filter((option) => copiedCellVal.includes(option.id) || copiedCellVal.includes(option.name))
.map((option) => option.name);
} else if (copiedColumnType === CellType.TEXT) {
const sCopiedCellVal = String(copiedCellVal);
// Pass excel test, wps test failed
copiedOptionNames = sCopiedCellVal.split('\n');
// get option names from string like 'a, b, c'
if (copiedOptionNames.length === 1) {
copiedOptionNames = sCopiedCellVal.split(',');
}
copiedOptionNames = copiedOptionNames.map(name => name.trim())
.filter(name => name !== '');
} else if (copiedColumnType === CellType.SINGLE_SELECT) {
const copiedOptions = getColumnOptions(copiedColumn);
copiedOptionNames = copiedOptions.filter((option) => option.id === copiedCellVal)
.map((option) => option.name);
}
if (copiedOptionNames.length === 0) {
return { selectedOptionIds: pasteCellVal };
}
const pasteOptions = getColumnOptions(pasteColumn);
const { cellOptions: newCellOptions, selectedOptionIds } = generatorCellOptions(pasteOptions, copiedOptionNames);
return { pasteOptions, newCellOptions, selectedOptionIds };
};
const convert2MultipleSelect = (copiedCellVal, pasteCellVal, copiedColumn, pasteColumn, api) => {
const { newCellOptions, pasteOptions, selectedOptionIds } = _getPasteMultipleSelect(copiedCellVal, pasteCellVal, copiedColumn, pasteColumn);
let newColumn = pasteColumn;
// the target column have no options with the same name
if (newCellOptions) {
if (!window.sfMetadataContext.canModifyColumnData(pasteColumn)) return null;
const updatedPasteOptions = [...pasteOptions, ...newCellOptions];
if (!newColumn.data) {
newColumn.data = {};
}
newColumn.data.options = updatedPasteOptions;
api.modifyColumnData(pasteColumn.key, { options: updatedPasteOptions }, pasteColumn.data);
}
return getColumnOptionNamesByIds(newColumn, selectedOptionIds);
};
function convertCellValue(cellValue, oldCellValue, targetColumn, fromColumn, api) {
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.MULTIPLE_SELECT: {
return convert2MultipleSelect(cellValue, oldCellValue, fromColumn, targetColumn, api);
}
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;
}
}
}
export { convertCellValue };

View File

@@ -107,7 +107,7 @@ class GridUtils {
const copiedColumnName = getColumnOriginName(copiedColumn);
const pasteCellValue = Object.prototype.hasOwnProperty.call(pasteRecord, pasteColumnName) ? getCellValueByColumn(pasteRecord, pasteColumn) : null;
const copiedCellValue = Object.prototype.hasOwnProperty.call(copiedRecord, copiedColumnName) ? getCellValueByColumn(copiedRecord, copiedColumn) : null;
const update = convertCellValue(copiedCellValue, pasteCellValue, pasteColumn, copiedColumn);
const update = convertCellValue(copiedCellValue, pasteCellValue, pasteColumn, copiedColumn, this.api);
if (update === pasteCellValue) {
continue;
}