diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 84152ca62c5..ff4fee2fae1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index 1ad2da4d87c..07174538527 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/assets/icons/multiple-select.svg b/frontend/src/assets/icons/multiple-select.svg
new file mode 100644
index 00000000000..c902a7dcfdd
--- /dev/null
+++ b/frontend/src/assets/icons/multiple-select.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/dir-view-mode/dir-views.js b/frontend/src/components/dir-view-mode/dir-views.js
index 686ddbac546..d1d24bae5ac 100644
--- a/frontend/src/components/dir-view-mode/dir-views.js
+++ b/frontend/src/components/dir-view-mode/dir-views.js
@@ -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') {
diff --git a/frontend/src/metadata/metadata-details/index.js b/frontend/src/metadata/metadata-details/index.js
index 400bec9bf5b..71a4b0b37f8 100644
--- a/frontend/src/metadata/metadata-details/index.js
+++ b/frontend/src/metadata/metadata-details/index.js
@@ -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 => {
diff --git a/frontend/src/metadata/metadata-view/_basic/constants/column/format.js b/frontend/src/metadata/metadata-view/_basic/constants/column/format.js
index d1bbbdd5089..cf41325264c 100644
--- a/frontend/src/metadata/metadata-view/_basic/constants/column/format.js
+++ b/frontend/src/metadata/metadata-view/_basic/constants/column/format.js
@@ -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,
diff --git a/frontend/src/metadata/metadata-view/_basic/constants/column/icon.js b/frontend/src/metadata/metadata-view/_basic/constants/column/icon.js
index a1d9b33935d..4715316b194 100644
--- a/frontend/src/metadata/metadata-view/_basic/constants/column/icon.js
+++ b/frontend/src/metadata/metadata-view/_basic/constants/column/icon.js
@@ -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',
};
diff --git a/frontend/src/metadata/metadata-view/_basic/constants/column/type.js b/frontend/src/metadata/metadata-view/_basic/constants/column/type.js
index 51cae4d2098..9861052e198 100644
--- a/frontend/src/metadata/metadata-view/_basic/constants/column/type.js
+++ b/frontend/src/metadata/metadata-view/_basic/constants/column/type.js
@@ -11,6 +11,7 @@ const CellType = {
DATE: 'date',
LONG_TEXT: 'long-text',
SINGLE_SELECT: 'single-select',
+ MULTIPLE_SELECT: 'multiple-select',
NUMBER: 'number',
GEOLOCATION: 'geolocation',
};
diff --git a/frontend/src/metadata/metadata-view/_basic/constants/filter/filter-column-options.js b/frontend/src/metadata/metadata-view/_basic/constants/filter/filter-column-options.js
index 121b042272d..f0736414b67 100644
--- a/frontend/src/metadata/metadata-view/_basic/constants/filter/filter-column-options.js
+++ b/frontend/src/metadata/metadata-view/_basic/constants/filter/filter-column-options.js
@@ -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,
diff --git a/frontend/src/metadata/metadata-view/_basic/constants/sort.js b/frontend/src/metadata/metadata-view/_basic/constants/sort.js
index 9fd5e79dcbc..3a98721bbcc 100644
--- a/frontend/src/metadata/metadata-view/_basic/constants/sort.js
+++ b/frontend/src/metadata/metadata-view/_basic/constants/sort.js
@@ -12,6 +12,7 @@ const SORT_COLUMN_OPTIONS = [
CellType.TEXT,
CellType.DATE,
CellType.SINGLE_SELECT,
+ CellType.MULTIPLE_SELECT,
CellType.COLLABORATOR,
CellType.CHECKBOX,
CellType.NUMBER,
diff --git a/frontend/src/metadata/metadata-view/_basic/index.js b/frontend/src/metadata/metadata-view/_basic/index.js
index f8a2a668e54..79e4c2298c8 100644
--- a/frontend/src/metadata/metadata-view/_basic/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/index.js
@@ -154,4 +154,6 @@ export {
isNumber,
getCellValueDisplayString,
getCellValueStringResult,
+ getColumnOptionNamesByIds,
+ getColumnOptionIdsByNames,
} from './utils';
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js b/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js
index a246a4784f6..d5d65b166b6 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js
@@ -11,6 +11,8 @@ export {
export {
getOption,
getColumnOptionNameById,
+ getColumnOptionNamesByIds,
+ getColumnOptionIdsByNames,
getOptionName,
getMultipleOptionName,
} from './option';
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/cell/column/option.js b/frontend/src/metadata/metadata-view/_basic/utils/cell/column/option.js
index 53b2a4eed0b..f17fc93bc60 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/cell/column/option.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/cell/column/option.js
@@ -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,
};
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js b/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js
index 3c864e66d8d..4ce9b8b72d0 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js
@@ -28,4 +28,6 @@ export {
getGeolocationByGranularity,
getFloatNumber,
isNumber,
+ getColumnOptionNamesByIds,
+ getColumnOptionIdsByNames,
} from './column';
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/index.js b/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/index.js
index 31232cfdde9..0410da434dd 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/index.js
@@ -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';
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/multiple-select.js b/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/multiple-select.js
new file mode 100644
index 00000000000..61e3578df55
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-column/multiple-select.js
@@ -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,
+};
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-row.js b/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-row.js
index 013bf58cb8b..20d1005f2ae 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-row.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/filter/filter-row.js
@@ -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);
}
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/group/group-row.js b/frontend/src/metadata/metadata-view/_basic/utils/group/group-row.js
index 7fedaca88fd..2988a419c90 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/group/group-row.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/group/group-row.js
@@ -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;
}
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/index.js b/frontend/src/metadata/metadata-view/_basic/utils/index.js
index 448782034e6..5fad636016e 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/index.js
@@ -25,6 +25,8 @@ export {
isNumber,
getCellValueDisplayString,
getCellValueStringResult,
+ getColumnOptionNamesByIds,
+ getColumnOptionIdsByNames,
} from './cell';
export {
getColumnType,
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/sort/core.js b/frontend/src/metadata/metadata-view/_basic/utils/sort/core.js
index 43df31159d8..011b2df6ae4 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/sort/core.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/sort/core.js
@@ -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) => {
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/sort/index.js b/frontend/src/metadata/metadata-view/_basic/utils/sort/index.js
index 95ac7f56f75..7848961ac4c 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/sort/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/sort/index.js
@@ -13,6 +13,7 @@ export {
sortCollaborator,
sortNumber,
sortSingleSelect,
+ sortMultipleSelect,
} from './sort-column';
export {
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/index.js b/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/index.js
index d1c39f5a99d..e1549b1f210 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/index.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/index.js
@@ -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';
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/multiple-select.js b/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/multiple-select.js
new file mode 100644
index 00000000000..8b411493c63
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-column/multiple-select.js
@@ -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,
+};
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-row.js b/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-row.js
index 66b31c3fe5a..efea0b1aea0 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-row.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/sort/sort-row.js
@@ -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;
diff --git a/frontend/src/metadata/metadata-view/_basic/utils/validate/filter.js b/frontend/src/metadata/metadata-view/_basic/utils/validate/filter.js
index 46c556814eb..af310fbbb38 100644
--- a/frontend/src/metadata/metadata-view/_basic/utils/validate/filter.js
+++ b/frontend/src/metadata/metadata-view/_basic/utils/validate/filter.js
@@ -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 });
}
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css
index ec9e9304ddf..e366ec798e0 100644
--- a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css
@@ -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;
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/index.js
index 2ca13495168..ccf9167a9e5 100644
--- a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/index.js
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/index.js
@@ -9,6 +9,7 @@ const POPUP_EDITOR_COLUMN_TYPES = [
CellType.DATE,
CellType.COLLABORATOR,
CellType.SINGLE_SELECT,
+ CellType.MULTIPLE_SELECT,
CellType.LONG_TEXT,
];
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js
index 9a5ef665eda..80d6d00675d 100644
--- a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js
@@ -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);
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor.js
index db30b1e1ea4..506413d00e3 100644
--- a/frontend/src/metadata/metadata-view/components/cell-editor/editor.js
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor.js
@@ -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 ();
}
+ case CellType.MULTIPLE_SELECT: {
+ return ();
+ }
case CellType.COLLABORATOR: {
return ();
}
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/delete-options/index.css b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/delete-options/index.css
new file mode 100644
index 00000000000..2f766cefb07
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/delete-options/index.css
@@ -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;
+}
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/delete-options/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/delete-options/index.js
new file mode 100644
index 00000000000..a0d92308126
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/delete-options/index.js
@@ -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 (
+
+ {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 (
+
+ {name}
+ onDelete(id, event)} iconName="x-01" />
+
+ );
+ })}
+
+ );
+};
+
+DeleteOption.propTypes = {
+ value: PropTypes.array.isRequired,
+ onDelete: PropTypes.func.isRequired
+};
+
+export default DeleteOption;
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/index.css b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/index.css
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/index.js
new file mode 100644
index 00000000000..d1c6fb6ce5f
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/components/cell-editor/multiple-select-editor/index.js
@@ -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 ({noOptionsTip});
+ }
+
+ return displayOptions.map((option, i) => {
+ const isSelected = value.includes(option.id);
+ return (
+
+
onSelectOption(option.id)}
+ onMouseEnter={() => onMenuMouseEnter(i)}
+ onMouseLeave={() => onMenuMouseLeave(i)}
+ >
+
+
+ {option.name}
+
+
+
+ {isSelected && ()}
+
+
+
+ );
+ });
+
+ }, [displayOptions, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]);
+
+ return (
+
+
+
+
+
+
+ {renderOptions()}
+
+ {isShowCreateBtn && (
+
+ )}
+
+ );
+});
+
+MultipleSelectEditor.propTypes = {
+ column: PropTypes.object,
+ value: PropTypes.array,
+ onCommit: PropTypes.func,
+ onPressTab: PropTypes.func,
+};
+
+export default MultipleSelectEditor;
diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/index.js
index b24c4f6b601..0adbf09e1fc 100644
--- a/frontend/src/metadata/metadata-view/components/detail-editor/index.js
+++ b/frontend/src/metadata/metadata-view/components/detail-editor/index.js
@@ -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 ();
@@ -33,6 +33,9 @@ const DetailEditor = ({ field, onChange: onChangeAPI, ...props }) => {
case CellType.SINGLE_SELECT: {
return ();
}
+ case CellType.MULTIPLE_SELECT: {
+ return ();
+ }
case CellType.COLLABORATOR: {
return ();
}
diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/multiple-select-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/multiple-select-editor/index.css
new file mode 100644
index 00000000000..deb896d0cf0
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/components/detail-editor/multiple-select-editor/index.css
@@ -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;
+}
diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/multiple-select-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/multiple-select-editor/index.js
new file mode 100644
index 00000000000..5361a8aa17a
--- /dev/null
+++ b/frontend/src/metadata/metadata-view/components/detail-editor/multiple-select-editor/index.js
@@ -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 (
+
+
+
+ );
+ }, [showEditor, onCommit, record, value, modifyColumnData, fields, field]);
+
+ return (
+
+
+ {renderEditor()}
+
+ );
+};
+
+MultipleSelectEditor.propTypes = {
+ field: PropTypes.object.isRequired,
+ value: PropTypes.array,
+ onChange: PropTypes.func.isRequired,
+};
+
+export default MultipleSelectEditor;
diff --git a/frontend/src/metadata/metadata-view/components/popover/column-popover/index.js b/frontend/src/metadata/metadata-view/components/popover/column-popover/index.js
index 00f8b417211..f2771448254 100644
--- a/frontend/src/metadata/metadata-view/components/popover/column-popover/index.js
+++ b/frontend/src/metadata/metadata-view/components/popover/column-popover/index.js
@@ -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 };
diff --git a/frontend/src/metadata/metadata-view/components/popover/column-popover/type/index.js b/frontend/src/metadata/metadata-view/components/popover/column-popover/type/index.js
index cd8b3a05d0d..beca8463a28 100644
--- a/frontend/src/metadata/metadata-view/components/popover/column-popover/type/index.js
+++ b/frontend/src/metadata/metadata-view/components/popover/column-popover/type/index.js
@@ -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.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.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.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.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
diff --git a/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js b/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js
index f6be555b8de..3a8734e6a12 100644
--- a/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js
+++ b/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js
@@ -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 {
{this.renderFilterTerm(filterColumn)}
- {showToolTip &&
-
-
- {/* */}
-
- }
+ {/* {showToolTip && (
+
+
+
+ {gettext('If there are multiple items in the cell, a random one will be chosen and be compared with the filter value.')}
+
+
+ )} */}
{this.renderErrorMessage()}
diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/index.css b/frontend/src/metadata/metadata-view/components/table/table-main/index.css
index 721c8fdef60..ba878d733a9 100644
--- a/frontend/src/metadata/metadata-view/components/table/table-main/index.css
+++ b/frontend/src/metadata/metadata-view/components/table/table-main/index.css
@@ -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;
}
-
-
diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/index.js
index 92cd0895a17..df9b03a7648 100644
--- a/frontend/src/metadata/metadata-view/components/table/table-main/index.js
+++ b/frontend/src/metadata/metadata-view/components/table/table-main/index.js
@@ -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}
/>
diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-title.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-title.js
index f3ddb6e9b47..ba35aa42280 100644
--- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-title.js
+++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/group-title.js
@@ -45,6 +45,29 @@ const GroupTitle = ({ column, cellValue, originalCellValue }) => {
const optionName = selectedOption ? selectedOption.name : deletedOptionTip;
return ({optionName}
);
}
+ 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 ({option.name}
);
+ })}
+ {invalidOptions.map(option => {
+ const style = { backgroundColor: option.color };
+ return ({option.name}
);
+ })}
+ >
+ );
+ }
default: {
return cellValue || emptyTip;
}
diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.css b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.css
index c81e11e4ac8..01bef350590 100644
--- a/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.css
+++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/group-body/group-container/index.css
@@ -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;
diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/records-header/cell/dropdown-menu/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/records-header/cell/dropdown-menu/index.js
index 90ee651f075..d6a57cf16b7 100644
--- a/frontend/src/metadata/metadata-view/components/table/table-main/records/records-header/cell/dropdown-menu/index.js
+++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/records-header/cell/dropdown-menu/index.js
@@ -181,6 +181,16 @@ const HeaderDropdownMenu = ({ column, renameColumn, modifyColumnData, deleteColu
/> */}
>
)}
+ {type === CellType.MULTIPLE_SELECT && (
+
+ )}
{type === CellType.NUMBER && (
{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) && (
)}
{
+ 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 };
diff --git a/frontend/src/metadata/metadata-view/utils/grid-utils.js b/frontend/src/metadata/metadata-view/utils/grid-utils.js
index a98977d374d..6cc93f9747f 100644
--- a/frontend/src/metadata/metadata-view/utils/grid-utils.js
+++ b/frontend/src/metadata/metadata-view/utils/grid-utils.js
@@ -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;
}