1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-02 07:27:04 +00:00

feat: metadata view part (#6289)

* feat: metadata view part

* feat: optimize code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-07-03 17:04:30 +08:00
committed by GitHub
parent 76af3d28ee
commit 8b6abc7855
59 changed files with 1823 additions and 815 deletions

View File

@@ -17,7 +17,7 @@
"@seafile/sdoc-editor": "1.0.7",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "1.0.99",
"@seafile/sf-metadata-ui-component": "0.0.7",
"@seafile/sf-metadata-ui-component": "0.0.8",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"chart.js": "2.9.4",
@@ -4953,9 +4953,9 @@
}
},
"node_modules/@seafile/sf-metadata-ui-component": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.7.tgz",
"integrity": "sha512-haLIkUnZaMqQQnLwjNnwb5rZoRAEv4qHE+G0xm51wBwDmFTQBkhnRJfxDlE7fxtkWGb6m5ea52U574ZnMWEarg==",
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.8.tgz",
"integrity": "sha512-5lHcPLC5HQtjfBXS47KUYfM3lSwW2KSH17S+eF1JBrEbeM7SGUN8p//sTMmUcSHwBn0VPs8BxWIvQWRQgVwz3g==",
"dependencies": {
"@seafile/seafile-calendar": "0.0.24",
"classnames": "2.3.2",
@@ -32160,9 +32160,9 @@
}
},
"@seafile/sf-metadata-ui-component": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.7.tgz",
"integrity": "sha512-haLIkUnZaMqQQnLwjNnwb5rZoRAEv4qHE+G0xm51wBwDmFTQBkhnRJfxDlE7fxtkWGb6m5ea52U574ZnMWEarg==",
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.8.tgz",
"integrity": "sha512-5lHcPLC5HQtjfBXS47KUYfM3lSwW2KSH17S+eF1JBrEbeM7SGUN8p//sTMmUcSHwBn0VPs8BxWIvQWRQgVwz3g==",
"requires": {
"@seafile/seafile-calendar": "0.0.24",
"classnames": "2.3.2",

View File

@@ -12,7 +12,7 @@
"@seafile/sdoc-editor": "1.0.7",
"@seafile/seafile-calendar": "0.0.12",
"@seafile/seafile-editor": "1.0.99",
"@seafile/sf-metadata-ui-component": "0.0.7",
"@seafile/sf-metadata-ui-component": "0.0.8",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/react-codemirror": "^4.19.4",
"chart.js": "2.9.4",

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="1719993327265" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15388" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M390.4 870.4c-41.6 0-86.4-16-112-48l-256-268.8c-35.2-32-25.6-89.6 16-121.6 35.2-25.6 86.4-25.6 118.4 9.6l230.4 246.4 480-499.2c35.2-32 92.8-32 128 9.6 35.2 32 35.2 83.2 9.6 115.2L499.2 819.2c-25.6 35.2-67.2 51.2-108.8 51.2z" fill="#949494" p-id="15389"></path></svg>

After

Width:  |  Height:  |  Size: 600 B

View File

@@ -1 +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="1718865369801" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16662" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M681.6 480c-9.6 0-19.2 6.4-19.2 16-44.8 16-83.2 41.6-115.2 76.8v-70.4c0-12.8-9.6-22.4-22.4-22.4h-70.4c-12.8 0-22.4 9.6-22.4 22.4v70.4c0 12.8 9.6 22.4 22.4 22.4H531.2c-25.6 32-41.6 70.4-48 112h-28.8c-12.8 0-22.4 9.6-22.4 22.4V800c0 12.8 9.6 22.4 22.4 22.4h32c9.6 41.6 28.8 80 57.6 112L124.8 928c-54.4 0-83.2-38.4-83.2-83.2V368h896v182.4c-48-44.8-112-70.4-182.4-70.4h-73.6zM300.8 704H230.4c-12.8 0-22.4 9.6-22.4 22.4v70.4c0 12.8 9.6 22.4 22.4 22.4h70.4c12.8 0 22.4-9.6 22.4-22.4v-70.4c0-12.8-9.6-22.4-22.4-22.4z m0-224H230.4c-12.8 0-22.4 9.6-22.4 22.4v70.4c0 12.8 9.6 22.4 22.4 22.4h70.4c12.8 0 22.4-9.6 22.4-22.4v-70.4c0-12.8-9.6-22.4-22.4-22.4z m48-448c16 0 28.8 12.8 28.8 28.8v83.2h227.2V60.8c0-16 12.8-28.8 28.8-28.8h57.6c16 0 28.8 12.8 28.8 28.8v83.2h140.8c48 0 80 38.4 80 83.2v83.2h-896V227.2c0-44.8 38.4-83.2 83.2-83.2h140.8V60.8c-3.2-16 9.6-28.8 25.6-28.8h54.4z" p-id="16663"></path><path d="M758.4 979.2c-60.8 0-118.4-22.4-156.8-64-41.6-41.6-64-99.2-64-156.8-3.2-124.8 99.2-227.2 224-227.2s224 99.2 224 224-105.6 224-227.2 224z m86.4-195.2c12.8 0 25.6-9.6 25.6-25.6s-9.6-25.6-25.6-25.6h-73.6v-118.4c0-12.8-9.6-25.6-25.6-25.6s-25.6 9.6-25.6 25.6v144c0 12.8 9.6 25.6 25.6 25.6h99.2z" fill="#949494" p-id="16664"></path></svg>
<?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="1718865369801" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16662" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M681.6 480c-9.6 0-19.2 6.4-19.2 16-44.8 16-83.2 41.6-115.2 76.8v-70.4c0-12.8-9.6-22.4-22.4-22.4h-70.4c-12.8 0-22.4 9.6-22.4 22.4v70.4c0 12.8 9.6 22.4 22.4 22.4H531.2c-25.6 32-41.6 70.4-48 112h-28.8c-12.8 0-22.4 9.6-22.4 22.4V800c0 12.8 9.6 22.4 22.4 22.4h32c9.6 41.6 28.8 80 57.6 112L124.8 928c-54.4 0-83.2-38.4-83.2-83.2V368h896v182.4c-48-44.8-112-70.4-182.4-70.4h-73.6zM300.8 704H230.4c-12.8 0-22.4 9.6-22.4 22.4v70.4c0 12.8 9.6 22.4 22.4 22.4h70.4c12.8 0 22.4-9.6 22.4-22.4v-70.4c0-12.8-9.6-22.4-22.4-22.4z m0-224H230.4c-12.8 0-22.4 9.6-22.4 22.4v70.4c0 12.8 9.6 22.4 22.4 22.4h70.4c12.8 0 22.4-9.6 22.4-22.4v-70.4c0-12.8-9.6-22.4-22.4-22.4z m48-448c16 0 28.8 12.8 28.8 28.8v83.2h227.2V60.8c0-16 12.8-28.8 28.8-28.8h57.6c16 0 28.8 12.8 28.8 28.8v83.2h140.8c48 0 80 38.4 80 83.2v83.2h-896V227.2c0-44.8 38.4-83.2 83.2-83.2h140.8V60.8c-3.2-16 9.6-28.8 25.6-28.8h54.4z" p-id="16663"></path><path d="M758.4 979.2c-60.8 0-118.4-22.4-156.8-64-41.6-41.6-64-99.2-64-156.8-3.2-124.8 99.2-227.2 224-227.2s224 99.2 224 224-105.6 224-227.2 224z m86.4-195.2c12.8 0 25.6-9.6 25.6-25.6s-9.6-25.6-25.6-25.6h-73.6v-118.4c0-12.8-9.6-25.6-25.6-25.6s-25.6 9.6-25.6 25.6v144c0 12.8 9.6 25.6 25.6 25.6h99.2z" p-id="16664"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

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="1719975260555" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16923" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 32C249.6 32 32 246.4 32 512s217.6 480 480 480 480-214.4 480-480S777.6 32 512 32z m-32 755.2c-19.2 0-35.2-16-35.2-35.2v-275.2c0-19.2 16-35.2 35.2-35.2h67.2c19.2 0 35.2 16 35.2 35.2v275.2c0 19.2-16 35.2-35.2 35.2H480z m0-412.8c-19.2 0-35.2-16-35.2-35.2V272c0-19.2 16-35.2 35.2-35.2h67.2c19.2 0 35.2 16 35.2 35.2v67.2c0 19.2-16 35.2-35.2 35.2H480z" p-id="16924"></path></svg>

After

Width:  |  Height:  |  Size: 711 B

View File

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -1 +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="1718865237398" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15810" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M316.8 768l-80 172.8c-19.2 41.6-67.2 60.8-112 41.6-41.6-16-51.2-67.2-35.2-108.8v-3.2L438.4 105.6c19.2-41.6 60.8-60.8 105.6-44.8 25.6 9.6 41.6 35.2 54.4 60.8l320 755.2c16 41.6-3.2 86.4-41.6 102.4h-3.2c-41.6 16-89.6-3.2-108.8-44.8L691.2 768H316.8z m67.2-150.4h243.2L512 342.4l-128 275.2z" fill="#979797" p-id="15811"></path></svg>
<?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="1718865237398" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15810" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M316.8 768l-80 172.8c-19.2 41.6-67.2 60.8-112 41.6-41.6-16-51.2-67.2-35.2-108.8v-3.2L438.4 105.6c19.2-41.6 60.8-60.8 105.6-44.8 25.6 9.6 41.6 35.2 54.4 60.8l320 755.2c16 41.6-3.2 86.4-41.6 102.4h-3.2c-41.6 16-89.6-3.2-108.8-44.8L691.2 768H316.8z m67.2-150.4h243.2L512 342.4l-128 275.2z" p-id="15811"></path></svg>

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 647 B

View File

@@ -43,6 +43,11 @@ class MetadataManagerAPI {
}
}
getCollaborators = (repoID) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/related-users/';
return this.req.get(url);
};
getMetadataStatus(repoID) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/';
return this.req.get(url);
@@ -60,7 +65,7 @@ class MetadataManagerAPI {
getMetadata(repoID, params) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/records/';
return this.req.get(url, {params: params});
return this.req.get(url, { params: params });
}
addMetadataRecords(repoID, parentDir, name) {

View File

@@ -41,6 +41,9 @@ const FILTER_COLUMN_OPTIONS = {
[CellType.TEXT]: {
filterPredicateList: textPredicates,
},
[CellType.FILE_NAME]: {
filterPredicateList: textPredicates,
},
[CellType.CTIME]: {
filterPredicateList: datePredicates,
filterTermModifierList: dateTermModifiers,

View File

@@ -28,24 +28,24 @@ const FILTER_TERM_MODIFIER_SHOW = {
[FILTER_TERM_MODIFIER_TYPE.TODAY]: gettext('Today'),
[FILTER_TERM_MODIFIER_TYPE.TOMORROW]: gettext('Tomorrow'),
[FILTER_TERM_MODIFIER_TYPE.YESTERDAY]: gettext('Yesterday'),
[FILTER_TERM_MODIFIER_TYPE.ONE_WEEK_AGO]: gettext('One_week_ago'),
[FILTER_TERM_MODIFIER_TYPE.ONE_WEEK_FROM_NOW]: gettext('One_week_from_now'),
[FILTER_TERM_MODIFIER_TYPE.ONE_MONTH_AGO]: gettext('One_month_ago'),
[FILTER_TERM_MODIFIER_TYPE.ONE_MONTH_FROM_NOW]: gettext('One_month_from_now'),
[FILTER_TERM_MODIFIER_TYPE.NUMBER_OF_DAYS_AGO]: gettext('Number_of_days_ago'),
[FILTER_TERM_MODIFIER_TYPE.NUMBER_OF_DAYS_FROM_NOW]: gettext('Number_of_days_from_now'),
[FILTER_TERM_MODIFIER_TYPE.EXACT_DATE]: gettext('Exact_date'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_WEEK]: gettext('The_past_week'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_MONTH]: gettext('The_past_month'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_YEAR]: gettext('The_past_year'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_WEEK]: gettext('The_next_week'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_MONTH]: gettext('The_next_month'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_YEAR]: gettext('The_next_year'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_NUMBERS_OF_DAYS]: gettext('The_next_numbers_of_days'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_NUMBERS_OF_DAYS]: gettext('The_past_numbers_of_days'),
[FILTER_TERM_MODIFIER_TYPE.THIS_WEEK]: gettext('This_week'),
[FILTER_TERM_MODIFIER_TYPE.THIS_MONTH]: gettext('This_month'),
[FILTER_TERM_MODIFIER_TYPE.THIS_YEAR]: gettext('This_year'),
[FILTER_TERM_MODIFIER_TYPE.ONE_WEEK_AGO]: gettext('One week ago'),
[FILTER_TERM_MODIFIER_TYPE.ONE_WEEK_FROM_NOW]: gettext('One week from now'),
[FILTER_TERM_MODIFIER_TYPE.ONE_MONTH_AGO]: gettext('One month ago'),
[FILTER_TERM_MODIFIER_TYPE.ONE_MONTH_FROM_NOW]: gettext('One month from now'),
[FILTER_TERM_MODIFIER_TYPE.NUMBER_OF_DAYS_AGO]: gettext('Number of days ago'),
[FILTER_TERM_MODIFIER_TYPE.NUMBER_OF_DAYS_FROM_NOW]: gettext('Number of days from now'),
[FILTER_TERM_MODIFIER_TYPE.EXACT_DATE]: gettext('Exact date'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_WEEK]: gettext('The past week'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_MONTH]: gettext('The past month'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_YEAR]: gettext('The past year'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_WEEK]: gettext('The next week'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_MONTH]: gettext('The next month'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_YEAR]: gettext('The next year'),
[FILTER_TERM_MODIFIER_TYPE.THE_NEXT_NUMBERS_OF_DAYS]: gettext('The next numbers of days'),
[FILTER_TERM_MODIFIER_TYPE.THE_PAST_NUMBERS_OF_DAYS]: gettext('The past numbers of days'),
[FILTER_TERM_MODIFIER_TYPE.THIS_WEEK]: gettext('This week'),
[FILTER_TERM_MODIFIER_TYPE.THIS_MONTH]: gettext('This month'),
[FILTER_TERM_MODIFIER_TYPE.THIS_YEAR]: gettext('This year'),
};
export {

View File

@@ -1,3 +1,5 @@
import { gettext } from '../../../../../utils/constants';
const FILTER_PREDICATE_TYPE = {
CONTAINS: 'contains',
NOT_CONTAIN: 'does_not_contain',
@@ -27,28 +29,29 @@ const FILTER_PREDICATE_TYPE = {
};
const FILTER_PREDICATE_SHOW = {
[FILTER_PREDICATE_TYPE.CONTAINS]: 'contains',
[FILTER_PREDICATE_TYPE.NOT_CONTAIN]: 'does not contain',
[FILTER_PREDICATE_TYPE.IS]: 'is',
[FILTER_PREDICATE_TYPE.IS_NOT]: 'is not',
[FILTER_PREDICATE_TYPE.CONTAINS]: gettext('contains'),
[FILTER_PREDICATE_TYPE.NOT_CONTAIN]: gettext('does not contain'),
[FILTER_PREDICATE_TYPE.IS]: gettext('is'),
[FILTER_PREDICATE_TYPE.IS_NOT]: gettext('is not'),
[FILTER_PREDICATE_TYPE.EQUAL]: '\u003d',
[FILTER_PREDICATE_TYPE.NOT_EQUAL]: '\u2260',
[FILTER_PREDICATE_TYPE.LESS]: '\u003C',
[FILTER_PREDICATE_TYPE.GREATER]: '\u003E',
[FILTER_PREDICATE_TYPE.LESS_OR_EQUAL]: '\u2264',
[FILTER_PREDICATE_TYPE.GREATER_OR_EQUAL]: '\u2265',
[FILTER_PREDICATE_TYPE.EMPTY]: 'is empty',
[FILTER_PREDICATE_TYPE.NOT_EMPTY]: 'is not empty',
[FILTER_PREDICATE_TYPE.IS_WITHIN]: 'is within...',
[FILTER_PREDICATE_TYPE.IS_BEFORE]: 'is before...',
[FILTER_PREDICATE_TYPE.IS_AFTER]: 'is after...',
[FILTER_PREDICATE_TYPE.IS_ON_OR_BEFORE]: 'is on or before...',
[FILTER_PREDICATE_TYPE.IS_ON_OR_AFTER]: 'is on or after...',
[FILTER_PREDICATE_TYPE.HAS_ANY_OF]: 'has any of...',
[FILTER_PREDICATE_TYPE.HAS_ALL_OF]: 'has all of...',
[FILTER_PREDICATE_TYPE.HAS_NONE_OF]: 'has none of...',
[FILTER_PREDICATE_TYPE.IS_EXACTLY]: 'is exactly...',
[FILTER_PREDICATE_TYPE.IS_CURRENT_USER_ID]: 'is current user\'s ID',
[FILTER_PREDICATE_TYPE.EMPTY]: gettext('is empty'),
[FILTER_PREDICATE_TYPE.NOT_EMPTY]: gettext('is not empty'),
[FILTER_PREDICATE_TYPE.IS_WITHIN]: gettext('is within...'),
[FILTER_PREDICATE_TYPE.IS_BEFORE]: gettext('is before...'),
[FILTER_PREDICATE_TYPE.IS_AFTER]: gettext('is after...'),
[FILTER_PREDICATE_TYPE.IS_ON_OR_BEFORE]: gettext('is on or before...'),
[FILTER_PREDICATE_TYPE.IS_ON_OR_AFTER]: gettext('is on or after...'),
[FILTER_PREDICATE_TYPE.HAS_ANY_OF]: gettext('has any of...'),
[FILTER_PREDICATE_TYPE.HAS_ALL_OF]: gettext('has all of...'),
[FILTER_PREDICATE_TYPE.HAS_NONE_OF]: gettext('has none of...'),
[FILTER_PREDICATE_TYPE.IS_EXACTLY]: gettext('is exactly...'),
[FILTER_PREDICATE_TYPE.IS_CURRENT_USER_ID]: gettext('is current user\'s ID'),
[FILTER_PREDICATE_TYPE.INCLUDE_ME]: gettext('include the current user')
};
export {

View File

@@ -6,6 +6,7 @@ const SORT_TYPE = {
};
const SORT_COLUMN_OPTIONS = [
CellType.FILE_NAME,
CellType.CTIME,
CellType.MTIME,
CellType.TEXT,

View File

@@ -52,6 +52,7 @@ export {
export {
getColumnType,
getColumnsByType,
getColumnByKey,
isDateColumn,
isSupportDateColumnFormat,
getValidFilters,
@@ -66,11 +67,12 @@ export {
textFilter,
filterRow,
filterRows,
getFilteredRows,
deleteInvalidGroupby,
isValidGroupby,
getValidGroupbys,
groupTableRows,
groupViewRows,
getGroupRows,
isTableRows,
updateTableRowsWithRowsData,
isValidSort,

View File

@@ -21,7 +21,13 @@ const getColumnsByType = (columns, columnType) => {
return columns.filter((column) => column.type === columnType);
};
const getColumnByKey = (columns, columnKey) => {
if (!Array.isArray(columns) || !columnKey) return null;
return columns.find((column) => column.key === columnKey);
};
export {
getColumnType,
getColumnsByType,
getColumnByKey,
};

View File

@@ -1,6 +1,7 @@
export {
getColumnType,
getColumnsByType,
getColumnByKey,
} from './core';
export {
isDateColumn,

View File

@@ -1,5 +1,6 @@
import {
getFormattedFilters,
deleteInvalidFilter,
} from './core';
import {
creatorFilter,
@@ -21,6 +22,7 @@ const getFilterResult = (row, filter, { username, userId }) => {
cellValue = DateUtils.format(cellValue, DATE_FORMAT_MAP.YYYY_MM_DD_HH_MM_SS);
return dateFilter(cellValue, filter);
}
case CellType.FILE_NAME:
case CellType.TEXT: {
return textFilter(cellValue, filter, userId);
}
@@ -80,7 +82,38 @@ const filterRows = (filterConjunction, filters, rows, { username, userId }) => {
return filteredRows;
};
/**
* Filter rows without formula calculation
* The "formulaRows" need to be provided if you want to filter formula, link columns etc.
* @param {object} table e.g. { columns, ... }
* @param {array} rows e.g. [{ _id, .... }, ...]
* @param {string} filterConjunction e.g. 'And' | 'Or'
* @param {array} filters e.g. [{ column_key, filter_predicate, ... }, ...]
* @param {string} username
* @param {string} userId
* @returns filtered rows: row_ids and error message: error_message, object
*/
const getFilteredRows = (table, rows, filterConjunction, filters, { username = null, userId = null } = {}) => {
const { columns } = table;
let validFilters = [];
try {
validFilters = deleteInvalidFilter(filters, columns);
} catch (err) {
return { row_ids: [], error_message: err.message };
}
let filteredRows = [];
if (validFilters.length === 0) {
filteredRows = rows.map((row) => row._id);
} else {
filteredRows = filterRows(filterConjunction, validFilters, rows, { username, userId });
}
return { row_ids: filteredRows, error_message: null };
};
export {
filterRow,
filterRows,
getFilteredRows,
};

View File

@@ -17,4 +17,5 @@ export {
export {
filterRow,
filterRows,
getFilteredRows,
} from './filter-row';

View File

@@ -1,4 +1,3 @@
import { getRowsByIds } from '../table/row';
import { DateUtils } from '../date';
import {
sortDate,
@@ -16,6 +15,7 @@ import {
SORT_TYPE,
TEXT_SORTER_COLUMN_TYPES,
} from '../../constants/sort';
import { deleteInvalidGroupby } from './core';
const _getCellValue = (row, groupby) => {
const { column_key } = groupby;
@@ -236,15 +236,19 @@ const groupTableRows = (groupbys, rows) => {
* cell_value, original_cell_value, column_key,
row_ids, subgroups, summaries, ...}, ...], array
*/
const groupViewRows = (groupbys, table, rowsIds) => {
if (rowsIds.length === 0) {
return [];
const getGroupRows = (table, rows, groupbys) => {
if (rows.length === 0) return [];
if (groupbys.length === 0) return rows;
let validGroupbys = [];
try {
validGroupbys = deleteInvalidGroupby(groupbys, table.columns);
} catch (err) {
validGroupbys = [];
}
let rowsData = getRowsByIds(table, rowsIds);
return groupTableRows(groupbys, rowsData);
return groupTableRows(validGroupbys, rows);
};
export {
groupTableRows,
groupViewRows,
getGroupRows,
};

View File

@@ -6,5 +6,5 @@ export {
export {
groupTableRows,
groupViewRows,
getGroupRows,
} from './group-row';

View File

@@ -4,6 +4,7 @@ import LocalStorage from './local-storage';
export {
getColumnType,
getColumnsByType,
getColumnByKey,
isDateColumn,
isSupportDateColumnFormat,
} from './column';
@@ -20,13 +21,14 @@ export {
textFilter,
filterRow,
filterRows,
getFilteredRows,
} from './filter';
export {
deleteInvalidGroupby,
isValidGroupby,
getValidGroupbys,
groupTableRows,
groupViewRows,
getGroupRows,
} from './group';
export {
isTableRows,

View File

@@ -37,7 +37,8 @@ const sortRowsWithMultiSorts = (tableRows, sorts) => {
* @param {object} value e.g. { collaborators, ... }
* @returns sorted rows ids, array
*/
const sortTableRows = (sorts, rows, columns) => {
const sortTableRows = (table, rows, sorts) => {
const { columns } = table;
if (!Array.isArray(rows) || rows.length === 0) return [];
const sortRows = rows.slice(0);
const validSorts = deleteInvalidSort(sorts, columns);

View File

@@ -16,7 +16,7 @@ const TERM_TYPE_MAP = {
ARRAY: 'array',
};
const TEXT_COLUMN_TYPES = [CellType.TEXT, CellType.STRING];
const TEXT_COLUMN_TYPES = [CellType.TEXT, CellType.FILE_NAME];
const CHECK_EMPTY_PREDICATES = [FILTER_PREDICATE_TYPE.EMPTY, FILTER_PREDICATE_TYPE.NOT_EMPTY];
@@ -222,7 +222,8 @@ class ValidateFilter {
static isValidTerm(term, predicate, modifier, filterColumn) {
switch (filterColumn.type) {
case CellType.TEXT: {
case CellType.TEXT:
case CellType.FILE_NAME: {
return this.isValidTermType(term, TERM_TYPE_MAP.STRING);
}

View File

@@ -1,11 +1,90 @@
import React from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import deepCopy from 'deep-copy';
import { Icon } from '@seafile/sf-metadata-ui-component';
import { getValidFilters, CommonlyUsedHotkey } from '../../_basic';
import { gettext } from '../../../../utils/constants';
import { FilterPopover } from '../popover';
const propTypes = {
const FilterSetter = ({ columns,
wrapperClass,
filters: propsFilters,
isNeedSubmit,
isPre,
collaborators,
filtersClassName,
target,
filterConjunction,
modifyFilters
}) => {
const [isShowSetter, setShowSetter] = useState(false);
const filters = useMemo(() => {
return deepCopy(getValidFilters(propsFilters || [], columns));
}, [propsFilters, columns]);
const filterMessage = useMemo(() => {
const filtersLength = filters.length;
if (filtersLength === 1) return isNeedSubmit ? gettext('1 preset filter') : gettext('1 filter');
if (filtersLength > 1) return filtersLength + ' ' + (isNeedSubmit ? gettext('Preset filters') : gettext('Filters'));
return isNeedSubmit ? gettext('Preset filter') : gettext('Filter');
}, [isNeedSubmit, filters]);
const onSetterToggle = useCallback(() => {
setShowSetter(!isShowSetter);
}, [isShowSetter]);
const onKeyDown = useCallback((event) => {
event.stopPropagation();
if (CommonlyUsedHotkey.isEnter(event) || CommonlyUsedHotkey.isSpace(event)) onSetterToggle();
}, [onSetterToggle]);
const onChange = useCallback((update) => {
const { filters, filter_conjunction } = update || {};
const validFilters = getValidFilters(filters, columns);
modifyFilters(validFilters, filter_conjunction);
}, [columns, modifyFilters]);
if (!columns) return null;
const className = classnames(wrapperClass, { 'active': filters.length > 0 });
return (
<>
<div className={classnames('setting-item', { 'mr-2': className, 'mb-1': !className })}>
<div
className={classnames('setting-item-btn filters-setting-btn', className)}
onClick={onSetterToggle}
role="button"
onKeyDown={onKeyDown}
title={filterMessage}
aria-label={filterMessage}
tabIndex={0}
id={target}
>
<Icon iconName='filter' />
<span>{filterMessage}</span>
</div>
</div>
{isShowSetter &&
<FilterPopover
filtersClassName={filtersClassName}
target={target}
isNeedSubmit={isNeedSubmit}
columns={columns}
collaborators={collaborators}
filterConjunction={filterConjunction}
filters={filters}
hidePopover={onSetterToggle}
update={onChange}
isPre={isPre}
/>
}
</>
);
};
FilterSetter.propTypes = {
wrapperClass: PropTypes.string,
filtersClassName: PropTypes.string,
target: PropTypes.string,
@@ -13,82 +92,14 @@ const propTypes = {
filterConjunction: PropTypes.string,
filters: PropTypes.array,
columns: PropTypes.array,
onFiltersChange: PropTypes.func,
modifyFilters: PropTypes.func,
collaborators: PropTypes.array,
isPre: PropTypes.bool,
};
class FilterSetter extends React.Component {
static defaultProps = {
FilterSetter.defaultProps = {
target: 'sf-metadata-filter-popover',
isNeedSubmit: false,
};
constructor(props) {
super(props);
this.state = {
isShowFilterSetter: false,
};
}
onKeyDown = (e) => {
e.stopPropagation();
if (CommonlyUsedHotkey.isEnter(e) || CommonlyUsedHotkey.isSpace(e)) this.onFilterSetterToggle();
};
onFilterSetterToggle = () => {
this.setState({ isShowFilterSetter: !this.state.isShowFilterSetter });
};
update = (update) => {
const { filters, filter_conjunction } = update || {};
const { columns } = this.props;
const valid_filters = getValidFilters(filters, columns);
this.props.onFiltersChange(valid_filters, filter_conjunction);
};
render() {
const {
wrapperClass, filters, columns, isNeedSubmit,
// collaborators, filtersClassName, filterConjunction,
} = this.props;
if (!columns) return null;
// const { isShowFilterSetter } = this.state;
const validFilters = deepCopy(getValidFilters(filters || [], columns));
const filtersLength = validFilters ? validFilters.length : 0;
let filterMessage = isNeedSubmit ? gettext('Preset filter') : gettext('Filter');
if (filtersLength === 1) {
filterMessage = isNeedSubmit ? gettext('1 preset filter') : gettext('1 filter');
} else if (filtersLength > 1) {
filterMessage = isNeedSubmit ? gettext('Preset filters') : gettext('Filters');
filterMessage = filtersLength + ' ' + filterMessage;
}
let labelClass = wrapperClass || '';
labelClass = (labelClass && filtersLength > 0) ? labelClass + ' active' : labelClass;
return (
<>
<div className={`setting-item ${labelClass ? 'mr-2' : 'mb-1'}`}>
<div
className={`setting-item-btn filters-setting-btn ${labelClass}`}
onClick={this.onFilterSetterToggle}
role="button"
onKeyDown={this.onKeyDown}
title={filterMessage}
aria-label={filterMessage}
tabIndex={0}
id={this.props.target}
>
<Icon iconName='filter' />
<span>{filterMessage}</span>
</div>
</div>
</>
);
}
}
FilterSetter.propTypes = propTypes;
};
export default FilterSetter;

View File

@@ -1,8 +1,73 @@
import React, { Component } from 'react';
import React, { useCallback, useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '@seafile/sf-metadata-ui-component';
import { getValidSorts, CommonlyUsedHotkey } from '../../_basic';
import { gettext } from '../../utils';
import { SortPopover } from '../popover';
const SortSetter = ({ target, sorts: propsSorts, columns, isNeedSubmit, wrapperClass, modifySorts }) => {
const [isShowSetter, setShowSetter] = useState(false);
const sorts = useMemo(() => {
return getValidSorts(propsSorts || [], columns);
}, [propsSorts, columns]);
const sortMessage = useMemo(() => {
const sortsLength = sorts.length;
if (sortsLength === 1) return isNeedSubmit ? gettext('1 preset sort') : gettext('1 sort');
if (sortsLength > 1) return sortsLength + ' ' + (isNeedSubmit ? gettext('preset sorts') : gettext('sorts'));
return isNeedSubmit ? gettext('Preset sort') : gettext('Sort');
}, [isNeedSubmit, sorts]);
const onSetterToggle = useCallback(() => {
setShowSetter(!isShowSetter);
}, [isShowSetter]);
const onKeyDown = useCallback((event) => {
event.stopPropagation();
if (CommonlyUsedHotkey.isEnter(event) || CommonlyUsedHotkey.isSpace(event)) onSetterToggle();
}, [onSetterToggle]);
const onChange = useCallback((update) => {
const { sorts } = update || {};
modifySorts(sorts);
}, [modifySorts]);
if (!columns) return null;
const className = classnames(wrapperClass, { 'active': sorts.length > 0 });
return (
<>
<div className={classnames('setting-item', { 'mb-1': !className })}>
<div
className={classnames('mr-2 setting-item-btn filters-setting-btn', className)}
onClick={onSetterToggle}
role="button"
onKeyDown={onKeyDown}
title={sortMessage}
aria-label={sortMessage}
tabIndex={0}
id={target}
>
<Icon iconName="sort" />
<span>{sortMessage}</span>
</div>
</div>
{isShowSetter && (
<SortPopover
isNeedSubmit={isNeedSubmit}
target={target}
columns={columns}
sorts={sorts}
onSortComponentToggle={onSetterToggle}
update={onChange}
/>
)}
</>
);
};
const propTypes = {
wrapperClass: PropTypes.string,
@@ -10,75 +75,14 @@ const propTypes = {
isNeedSubmit: PropTypes.bool,
sorts: PropTypes.array,
columns: PropTypes.array,
onSortsChange: PropTypes.func,
modifySorts: PropTypes.func,
};
class SortSetter extends Component {
static defaultProps = {
target: 'sf-metadata-sort-popover',
isNeedSubmit: false,
};
constructor(props) {
super(props);
this.state = {
isSortPopoverShow: false,
};
}
onSortToggle = () => {
this.setState({ isSortPopoverShow: !this.state.isSortPopoverShow });
};
onKeyDown = (e) => {
e.stopPropagation();
if (CommonlyUsedHotkey.isEnter(e) || CommonlyUsedHotkey.isSpace(e)) this.onSortToggle();
};
update = (update) => {
const { sorts } = update || {};
this.props.onSortsChange(sorts);
};
render() {
const { sorts, columns, isNeedSubmit, wrapperClass } = this.props;
if (!columns) return null;
const validSorts = getValidSorts(sorts || [], columns);
const sortsLength = validSorts ? validSorts.length : 0;
let sortMessage = isNeedSubmit ? gettext('Preset sort') : gettext('Sort');
if (sortsLength === 1) {
sortMessage = isNeedSubmit ? gettext('1 preset sort') : gettext('1 sort');
} else if (sortsLength > 1) {
sortMessage = isNeedSubmit ? gettext('xxx preset sorts') : gettext('xxx sorts');
sortMessage = sortMessage.replace('xxx', sortsLength);
}
let labelClass = wrapperClass || '';
labelClass = (labelClass && sortsLength > 0) ? labelClass + ' active' : labelClass;
return (
<>
<div className={`setting-item ${labelClass ? '' : 'mb-1'}`}>
<div
className={`mr-2 setting-item-btn filters-setting-btn ${labelClass}`}
onClick={this.onSortToggle}
role="button"
onKeyDown={this.onKeyDown}
title={sortMessage}
aria-label={sortMessage}
tabIndex={0}
id={this.props.target}
>
<Icon iconName="sort" />
<span>{sortMessage}</span>
</div>
</div>
</>
);
}
}
SortSetter.propTypes = propTypes;
SortSetter.defaultProps = {
target: 'sf-metadata-sort-popover',
isNeedSubmit: false,
};
export default SortSetter;

View File

@@ -2,29 +2,3 @@
max-width: none;
min-width: 300px;
}
.filter-popover .popover-add-tool {
border-top: none;
color: #666666;
}
.filter-popover .popover-add-tool.disabled {
color: #c2c2c2;
}
.filter-popover .popover-add-tool.disabled:hover {
cursor: not-allowed;
background: #fff;
}
.filter-popover .popover-add-tool .popover-add-icon {
margin-right: 14px;
}
.filter-popover .filter-popover-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1rem;
border-top: 1px solid #e9ecef;
}

View File

@@ -1,32 +1,25 @@
import React, { Component, Fragment } from 'react';
import React, { Fragment, useMemo } from 'react';
import PropTypes from 'prop-types';
import intl from 'react-intl-universal';
import { CustomizeSelect } from '@seafile/sf-metadata-ui-component';
import { CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
import { FILTER_PREDICATE_TYPE } from '../../../../_basic';
import { gettext } from '../../../../utils';
const propTypes = {
filterIndex: PropTypes.number,
filterTerm: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), // Make the current bug execution the correct code, this can restore in this Component
filter_predicate: PropTypes.string,
collaborators: PropTypes.array,
onSelectCollaborator: PropTypes.func,
isLocked: PropTypes.bool,
placeholder: PropTypes.string,
};
class CollaboratorFilter extends Component {
constructor(props) {
super(props);
this.supportMultipleSelectOptions = [
const CollaboratorFilter = ({ isLocked, filterIndex, filterTerm, collaborators, placeholder, filter_predicate, onSelectCollaborator }) => {
const supportMultipleSelectOptions = useMemo(() => {
return [
FILTER_PREDICATE_TYPE.HAS_ANY_OF,
FILTER_PREDICATE_TYPE.HAS_ALL_OF,
FILTER_PREDICATE_TYPE.HAS_NONE_OF,
FILTER_PREDICATE_TYPE.IS_EXACTLY,
];
}
}, []);
createCollaboratorOptions = (filterIndex, collaborators, filterTerm) => {
const isSupportMultipleSelect = useMemo(() => {
return supportMultipleSelectOptions.indexOf(filter_predicate) > -1 ? true : false;
}, [supportMultipleSelectOptions, filter_predicate]);
const options = useMemo(() => {
if (!Array.isArray(filterTerm)) return [];
return collaborators.map((collaborator) => {
let isSelected = filterTerm.findIndex(item => item === collaborator.email) > -1;
return {
@@ -49,24 +42,17 @@ class CollaboratorFilter extends Component {
</div>
</div>
<div className='collaborator-check-icon'>
{isSelected && <i className="option-edit sf-metadata-font sf-metadata-icon-check-mark"></i>}
{isSelected && (<Icon iconName="check-mark" />)}
</div>
</div>
</Fragment>
)
};
});
};
}, [filterIndex, collaborators, filterTerm]);
onClick = (e, collaborator) => {
e.stopPropagation();
this.props.onSelectCollaborator({ columnOption: collaborator });
};
render() {
let { filterIndex, filterTerm, collaborators, placeholder, filter_predicate } = this.props;
let isSupportMultipleSelect = this.supportMultipleSelectOptions.indexOf(filter_predicate) > -1 ? true : false;
let selectedCollaborators = Array.isArray(filterTerm) && filterTerm.length > 0 && filterTerm.map((item) => {
const selectValue = useMemo(() => {
return Array.isArray(filterTerm) && filterTerm.length > 0 && filterTerm.map((item) => {
let collaborator = collaborators.find(c => c.email === item);
if (!collaborator) return null;
return (
@@ -80,35 +66,36 @@ class CollaboratorFilter extends Component {
aria-label={collaborator.name}
>{collaborator.name}
</span>
<span className="remove-container">
<span className="remove-icon" onClick={(e) => {
this.onClick(e, collaborator);
}}>
<i className="sf-metadata-font sf-metadata-icon-fork-number"></i>
</span>
</span>
</div>
);
});
let value = selectedCollaborators ? { label: (<>{selectedCollaborators}</>) } : {};
let options = Array.isArray(filterTerm) ? this.createCollaboratorOptions(filterIndex, collaborators, filterTerm) : [];
}, [filterTerm, collaborators]);
return (
<CustomizeSelect
className="selector-collaborator"
value={value}
onSelectOption={this.props.onSelectCollaborator}
value={selectValue ? { label: selectValue } : {}}
onSelectOption={onSelectCollaborator}
options={options}
placeholder={placeholder}
isLocked={this.props.isLocked}
isLocked={isLocked}
supportMultipleSelect={isSupportMultipleSelect}
searchable={true}
searchPlaceholder={intl.get('Search_collaborator')}
searchPlaceholder={gettext('Search collaborator')}
isShowSelected={false}
noOptionsPlaceholder={gettext('No collaborators')}
/>
);
}
}
};
CollaboratorFilter.propTypes = propTypes;
CollaboratorFilter.propTypes = {
filterIndex: PropTypes.number,
filterTerm: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), // Make the current bug execution the correct code, this can restore in this Component
filter_predicate: PropTypes.string,
collaborators: PropTypes.array,
onSelectCollaborator: PropTypes.func,
isLocked: PropTypes.bool,
placeholder: PropTypes.string,
};
export default CollaboratorFilter;

View File

@@ -1,184 +1,27 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import Calendar from '@seafile/seafile-calendar';
import DatePicker from '@seafile/seafile-calendar/lib/Picker';
import { translateCalendar } from '../../../../utils/date-translate';
import { Calendar } from '@seafile/sf-metadata-ui-component';
import { getDateColumnFormat } from '../../../../utils/column-utils';
import dayjs from '../../../../utils/dayjs';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en-gb';
import '@seafile/seafile-calendar/assets/index.css';
const FilterCalendar = ({ value, filterColumn, isReadOnly, onChange }) => {
const format = getDateColumnFormat(filterColumn).trim();
const lang = window.sfMetadataContext.getSetting('lang');
return (
<Calendar
isReadOnly={isReadOnly}
format={format}
lang={lang}
value={value}
onChange={onChange}
/>
);
};
let now = dayjs();
const propTypes = {
FilterCalendar.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
filterColumn: PropTypes.object.isRequired,
isReadOnly: PropTypes.bool,
};
class FilterCalendar extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
value: null
};
const DataFormat = getDateColumnFormat(props.filterColumn).trim();
// Minutes and seconds are not supported at present
this.columnDataFormat = DataFormat.split(' ')[0];
this.calendarContainerRef = React.createRef();
this.defaultCalendarValue = null;
}
componentDidMount() {
const iszhcn = (window.app && window.app.config && window.app.config.lang === 'zh-cn');
if (iszhcn) {
now = now.locale('zh-cn');
} else {
now = now.locale('en-gb');
}
this.defaultCalendarValue = now.clone();
const { value } = this.props;
if (value && dayjs(value).isValid()) {
let validValue = dayjs(value).isValid() ? dayjs(value) : dayjs(this.defaultCalendarValue);
this.setState({
value: iszhcn ? dayjs(validValue).locale('zh-cn') : dayjs(validValue).locale('en-gb')
});
}
}
handleMouseDown = (e) => {
e.preventDefault();
};
onChange = (value) => {
const { onChange } = this.props;
const searchFormat = 'YYYY-MM-DD';
this.setState({
value
}, () => {
if (this.state.value) {
onChange(this.state.value.format(searchFormat));
}
});
};
onClear = () => {
this.setState({
value: null
}, () => {
this.setState({
open: true
});
});
};
onOpenChange = (open) => {
this.setState({
open,
});
};
onReadOnlyFocus = () => {
if (!this.state.open && this.state.isMouseDown) {
this.setState({
isMouseDown: false,
});
} else {
this.setState({
open: true,
});
}
};
getCalendarContainer = () => {
return this.calendarContainerRef.current;
};
getCalendarFormat = () => {
let calendarFormat = [];
if (this.columnDataFormat.indexOf('YYYY-MM-DD') > -1) {
let newColumnDataFormat = this.columnDataFormat.replace('YYYY-MM-DD', 'YYYY-M-D');
calendarFormat = [this.columnDataFormat, newColumnDataFormat];
} else if (this.columnDataFormat.indexOf('DD/MM/YYYY') > -1) {
let newColumnDataFormat = this.columnDataFormat.replace('DD/MM/YYYY', 'D/M/YYYY');
calendarFormat = [this.columnDataFormat, newColumnDataFormat];
} else {
calendarFormat = [this.columnDataFormat];
}
return calendarFormat;
};
render() {
const { isReadOnly } = this.props;
const state = this.state;
if (isReadOnly) return (
<input
className="ant-calendar-picker-input ant-input form-control"
value={state.value ? state.value.format(this.columnDataFormat) : ''}
disabled={true}
/>
);
const calendarFormat = this.getCalendarFormat();
const clearStyle = {
position: 'absolute',
top: '15px',
left: '225px',
color: 'gray',
fontSize: '12px'
};
const clearIcon = React.createElement('i', { className: 'item-icon sf-metadata-font sf-metadata-icon-x', style: clearStyle });
const calendar = (
<Calendar
className="sf-metadata-rc-calendar"
locale={translateCalendar()}
style={{ zIndex: 1001 }}
dateInputPlaceholder={('please enter date')}
format={calendarFormat}
defaultValue={this.defaultCalendarValue}
showDateInput={true}
focusablePanel={false}
onClear={this.onClear}
clearIcon={clearIcon}
/>
);
return (
<div className="date-picker-container">
<DatePicker
calendar={calendar}
value={state.value}
onChange={this.onChange}
getCalendarContainer={this.getCalendarContainer}
onOpenChange={this.onOpenChange}
open={state.open}
style={{ zIndex: 1001 }}
>
{
({ value }) => {
return (
<span tabIndex="0" onFocus={this.onReadOnlyFocus}>
<input
tabIndex="-1"
readOnly
className="ant-calendar-picker-input ant-input form-control"
value={value ? value.format(this.columnDataFormat) : ''}
onMouseDown={this.handleMouseDown}
/>
<div ref={this.calendarContainerRef} />
</span>
);
}
}
</DatePicker>
</div>
);
}
}
FilterCalendar.propTypes = propTypes;
export default FilterCalendar;

View File

@@ -1,6 +1,6 @@
import React, { Fragment } from 'react';
import { Icon } from '@seafile/sf-metadata-ui-component';
import { COLUMNS_ICON_CONFIG, FILTER_TERM_MODIFIER_SHOW } from '../../../../_basic';
import { COLUMNS_ICON_CONFIG, FILTER_PREDICATE_SHOW, FILTER_TERM_MODIFIER_SHOW } from '../../../../_basic';
import { gettext } from '../../../../utils';
class FilterItemUtils {
@@ -22,7 +22,7 @@ class FilterItemUtils {
static generatorPredicateOption(filterPredicate) {
return {
value: { filterPredicate },
label: <span className='select-option-name'>{gettext(filterPredicate)}</span>
label: <span className='select-option-name'>{FILTER_PREDICATE_SHOW[filterPredicate]}</span>
};
}

View File

@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { UncontrolledTooltip } from 'reactstrap';
import { CustomizeSelect, IconBtn, SearchInput } from '@seafile/sf-metadata-ui-component';
import { CustomizeSelect, IconBtn, SearchInput, Icon } from '@seafile/sf-metadata-ui-component';
import {
CellType,
FILTER_PREDICATE_TYPE,
@@ -381,6 +381,7 @@ class FilterItem extends React.Component {
}
switch (type) {
case CellType.FILE_NAME:
case CellType.TEXT:
case CellType.URL: { // The data in the formula column is a date type that has been excluded
if (filter_predicate === FILTER_PREDICATE_TYPE.IS_CURRENT_USER_ID) {
@@ -455,7 +456,7 @@ class FilterItem extends React.Component {
return (
<div className="filter-item">
<div className="delete-filter" onClick={this.onDeleteFilter}>
<i className="sf-metadata-font sf-metadata-icon-fork-number"></i>
<Icon iconName="fork-number"/>
</div>
<div className="condition">
<div className="filter-conjunction">

View File

@@ -44,7 +44,7 @@
max-width: 300px;
}
.filters-list .filter-item .filter-term .option-group-content .option.option-active .sf-metadata-font {
.filters-list .filter-item .filter-term .option-group-content .option.option-active .sf-metadata-icon {
color: #798d99;
}
@@ -64,11 +64,13 @@
.filters-list .sf-metadata-select .selected-option-show {
width: calc(100% - 20px);
height: 20px;
line-height: 20px;
}
.filters-list .sf-metadata-select .selected-option {
width: auto;
overflow-x: auto;
line-height: 1.5;
}
.filters-list .sf-metadata-select .sf-metadata-icon-drop-down {
@@ -202,10 +204,14 @@
cursor: pointer;
}
.filters-list .delete-filter .sf-metadata-icon-fork-number {
.delete-filter .sf-metadata-icon-fork-number {
display: inline-block;
font-size: 12px;
color: #999;
fill: #999;
}
.delete-filter .sf-metadata-icon-fork-number:hover {
fill: #666;
}
.filters-list .multiple-option-name {
@@ -246,19 +252,17 @@
.filters-list .collaborator-avatar-container {
width: 16px;
height: 16px;
display: flex;
align-items: center;
}
.filters-list .collaborator-avatar {
width: 16px;
height: 16px;
transform: translateY(-1px);
border-radius: 50%;
}
.filters-list .option .collaborator-avatar {
transform: translateY(-2px);
}
.filters-list .collaborator-name {
margin-left: 5px;
max-width: 200px;
@@ -270,6 +274,8 @@
.filters-list .collaborator-container {
flex: 1;
display: flex;
align-items: center;
}
.filters-list .popover-add-tool {
@@ -286,10 +292,6 @@
overflow: auto;
}
.filters-list .filter-item .sf-metadata-icon-fork-number:hover {
color: #666666;
}
.filters-list .filter-container-readonly .sf-metadata-select .selected-option-show,
.filters-list .filter-conjunction-readonly .sf-metadata-select .selected-option-show {
width: 100%;
@@ -314,8 +316,17 @@
margin-left: -0.3125rem;
}
.filter-header-icon .sf-metadata-font {
.filter-header-icon .sf-metadata-icon {
font-size: 14px;
color: #aaa;
fill: #aaa;
cursor: default;
}
.option:hover .filter-header-icon .sf-metadata-icon,
.option.option-active .filter-header-icon .sf-metadata-icon {
fill: #fff;
}
.option:not(.option-active):hover .filter-header-icon .sf-metadata-icon {
fill: #aaa;
}

View File

@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
import {
FILTER_COLUMN_OPTIONS,
ValidateFilter,
getColumnByKey,
} from '../../../../_basic';
import FilterItemUtils from './filter-item-utils';
import FilterItem from './filter-item';
import { getColumnByKey } from '../../../../utils/column-utils';
import './index.css';
@@ -115,7 +115,7 @@ class FiltersList extends Component {
filters.map((filter, index) => {
const { column_key } = filter;
const { error_message } = ValidateFilter.validate(filter, columns);
const filterColumn = getColumnByKey(column_key, columns) || {};
const filterColumn = getColumnByKey(columns, column_key) || {};
return this.renderFilterItem(filter, index, error_message, filterColumn);
})
}

View File

@@ -1,6 +1,5 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import intl from 'react-intl-universal';
import { UncontrolledPopover, Button } from 'reactstrap';
import {
COLUMNS_ICON_CONFIG,
@@ -8,17 +7,18 @@ import {
DISPLAY_GROUP_GEOLOCATION_GRANULARITY,
MAX_GROUP_LEVEL,
SORT_TYPE,
getColumnByKey,
} from 'sf-metadata-utils';
import CommonAddTool from '../../common/common-add-tool';
import GroupbyItem from '../groupby-popover-widgets/groupby-item';
import GroupbyService from '../../services/groupby-service';
import { isEsc } from '../../utils/hotkey';
import { getColumnByKey } from '../../../utils/column-utils';
import { getEventClassName } from '../../utils/utils';
import { generateDefaultGroupby, getDefaultCountType, getGroupbyColumns } from '../../../utils/groupby-utils';
import eventBus from '../../../utils/event-bus';
import { GROUPBY_ACTION_TYPE, GROUPBY_DATE_GRANULARITY_LIST, GROUPBY_GEOLOCATION_GRANULARITY_LIST } from '../../constants/groupby';
import { EVENT_BUS_TYPE } from '../../../constants';
import { gettext } from '../../../utils';
import './index.css';
@@ -101,7 +101,7 @@ class GroupbyPopover extends Component {
}
return {
value: { countType: granularity },
label: <span className='select-option-name'>{intl.get(displayGranularity)}</span>,
label: <span className='select-option-name'>{gettext(displayGranularity)}</span>,
};
}).filter(Boolean);
};
@@ -114,7 +114,7 @@ class GroupbyPopover extends Component {
}
return {
value: { countType: granularity },
label: <span className='select-option-name'>{intl.get(DISPLAY_GROUP_DATE_GRANULARITY[granularity])}</span>,
label: <span className='select-option-name'>{gettext(DISPLAY_GROUP_DATE_GRANULARITY[granularity])}</span>,
};
}).filter(Boolean);
};
@@ -123,11 +123,11 @@ class GroupbyPopover extends Component {
return [
{
value: { sortType: SORT_TYPE.UP },
label: <span className='select-option-name'>{intl.get(SORT_TYPE.UP)}</span>
label: <span className='select-option-name'>{gettext(SORT_TYPE.UP)}</span>
},
{
value: { sortType: SORT_TYPE.DOWN },
label: <span className='select-option-name'>{intl.get(SORT_TYPE.DOWN)}</span>
label: <span className='select-option-name'>{gettext(SORT_TYPE.DOWN)}</span>
},
];
};
@@ -223,7 +223,7 @@ class GroupbyPopover extends Component {
const { columns } = this.props;
const { groupbys } = this.state;
return groupbys.map((groupby, index) => {
const column = getColumnByKey(groupby.column_key, columns) || {};
const column = getColumnByKey(columns, groupby.column_key) || {};
return (
<GroupbyItem
key={'groupby-item-' + index}
@@ -274,28 +274,28 @@ class GroupbyPopover extends Component {
>
<div className={`groupbys ${isEmpty ? 'empty-groupbys-container' : ''}`} >
{isEmpty ?
<div className="empty-groupbys">{intl.get('No_groupings')}</div> :
<div className="empty-groupbys">{gettext('No_groupings')}</div> :
this.renderGroupbys(scheduleUpdate)
}
</div>
{groupbysLen < MAX_GROUP_LEVEL &&
<CommonAddTool
callBack={() => this.addGroupby(scheduleUpdate)}
footerName={intl.get('Add_group')}
footerName={gettext('Add group')}
className='popover-add-tool'
addIconClassName='popover-add-icon'
/>
}
{!isEmpty &&
<div className="groupbys-tools">
<span className="groupbys-tool-item" onClick={this.onHideAllGroups}>{intl.get('Collapse_all')}</span>
<span className="groupbys-tool-item" onClick={this.onShowAllGroups}>{intl.get('Expand_all')}</span>
<span className="groupbys-tool-item" onClick={this.onHideAllGroups}>{gettext('Collapse all')}</span>
<span className="groupbys-tool-item" onClick={this.onShowAllGroups}>{gettext('Expand all')}</span>
</div>
}
{this.isNeedSubmit() && (
<div className="d-flex align-items-center justify-content-end p-4 border-top">
<Button className="mr-2" onClick={this.props.onGroupbyPopoverToggle}>{intl.get('Cancel')}</Button>
<Button color='primary' onClick={this.submitDefaultGroupbys}>{intl.get('Submit')}</Button>
<Button className="mr-2" onClick={this.props.onGroupbyPopoverToggle}>{gettext('Cancel')}</Button>
<Button color='primary' onClick={this.submitDefaultGroupbys}>{gettext('Submit')}</Button>
</div>
)}
</div>

View File

@@ -0,0 +1,30 @@
.filter-popover-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1rem;
}
.popover-add-tool.sf-metadata-ui.add-item-btn {
border-top: none;
color: #666666;
}
.popover-add-tool.disabled:hover {
cursor: not-allowed;
background: #fff;
}
.popover-add-tool .popover-add-icon {
fill: #666;
font-size: 14px;
margin-right: 14px;
}
.popover-add-tool.disabled .popover-add-icon {
fill: #c2c2c2;
}
.filter-popover .popover-add-tool.disabled {
color: #c2c2c2;
}

View File

@@ -1,13 +1,13 @@
import FilterPopover from './filter-popover';
import SortPopover from './sort-popover';
import GroupbyPopover from './groupby-popover';
import HideColumnPopover from './hide-column-popover';
import ColumnPermissionPopover from './column-permission-popover';
// import GroupbyPopover from './groupby-popover';
// import HideColumnPopover from './hide-column-popover';
import './index.css';
export {
FilterPopover,
SortPopover,
GroupbyPopover,
HideColumnPopover,
ColumnPermissionPopover
// GroupbyPopover,
// HideColumnPopover,
};

View File

@@ -57,27 +57,10 @@
.delete-sort .sf-metadata-icon-fork-number {
display: inline-block;
font-size: 12px;
color: #999;
fill: #999;
cursor: pointer;
}
.sorts-list .delete-sort .sf-metadata-icon-fork-number:hover {
color: #666666;
}
.sort-popover .popover-add-tool {
border-top: none;
color: #666666;
}
.sort-popover .popover-add-tool .popover-add-icon {
margin-right: 14px;
}
.sort-popover .sort-popover-footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 1rem;
border-top: 1px solid #e9ecef;
fill: #666666;
}

View File

@@ -1,19 +1,16 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import intl from 'react-intl-universal';
import isHotkey from 'is-hotkey';
import { Button, UncontrolledPopover } from 'reactstrap';
import { CustomizeAddTool, CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
import {
COLUMNS_ICON_CONFIG,
SORT_COLUMN_OPTIONS,
SORT_TYPE,
} from 'sf-metadata-utils';
import { DTableCustomizeSelect } from 'sf-metadata-ui-component';
import CommonAddTool from '../../common/common-add-tool';
import { execSortsOperation, getDisplaySorts, isSortsEmpty, SORT_OPERATION } from '../sort-popover-widgets/sort-utils';
import { getEventClassName } from '../../utils/utils';
import { getColumnByKey } from '../../../utils/column-utils';
import eventBus from '../../../utils/event-bus';
getColumnByKey,
} from '../../../_basic';
import { execSortsOperation, getDisplaySorts, isSortsEmpty, SORT_OPERATION } from './utils';
import { getEventClassName, gettext } from '../../../utils';
import { EVENT_BUS_TYPE } from '../../../constants';
import './index.css';
@@ -50,7 +47,7 @@ class SortPopover extends Component {
componentDidMount() {
document.addEventListener('click', this.hideDTablePopover, true);
document.addEventListener('keydown', this.onHotKey);
this.unsubscribeOpenSelect = eventBus.subscribe(EVENT_BUS_TYPE.OPEN_SELECT, this.setSelectStatus);
this.unsubscribeOpenSelect = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_SELECT, this.setSelectStatus);
}
componentWillUnmount() {
@@ -158,7 +155,7 @@ class SortPopover extends Component {
value: { column },
label: (
<Fragment>
<span className="filter-header-icon"><i className={COLUMNS_ICON_CONFIG[type]}></i></span>
<span className="filter-header-icon"><Icon iconName={COLUMNS_ICON_CONFIG[type]} /></span>
<span className='select-option-name'>{name}</span>
</Fragment>
)
@@ -170,7 +167,7 @@ class SortPopover extends Component {
return SORT_TYPES.map(sortType => {
return {
value: { sortType },
label: <span className='select-option-name'>{intl.get(sortType)}</span>
label: <span className='select-option-name'>{gettext(sortType)}</span>
};
});
};
@@ -179,7 +176,7 @@ class SortPopover extends Component {
const { columns } = this.props;
const { sorts } = this.state;
return sorts.map((sort, index) => {
const column = getColumnByKey(sort.column_key, columns) || {};
const column = getColumnByKey(columns, sort.column_key) || {};
return this.renderSortItem(column, sort, index);
});
};
@@ -190,7 +187,7 @@ class SortPopover extends Component {
let selectedColumn = {
label: (
<Fragment>
<span className="filter-header-icon"><i className={COLUMNS_ICON_CONFIG[type]}></i></span>
<span className="filter-header-icon"><Icon iconName={COLUMNS_ICON_CONFIG[type]} /></span>
<span className='select-option-name' title={name} aria-label={name}>{name}</span>
</Fragment>
)
@@ -198,30 +195,30 @@ class SortPopover extends Component {
let selectedTypeShow = sort.sort_type;
let selectedSortType = selectedTypeShow && {
label: <span className='select-option-name'>{intl.get(selectedTypeShow)}</span>
label: <span className='select-option-name'>{gettext(selectedTypeShow)}</span>
};
return (
<div key={'sort-item-' + index} className="sort-item">
{!readonly &&
<div className="delete-sort" onClick={(event) => this.deleteSort(event, index)}>
<i className="sf-metadata-font sf-metadata-icon-fork-number"></i>
<Icon iconName="fork-number"/>
</div>
}
<div className="condition">
<div className="sort-column">
<DTableCustomizeSelect
<CustomizeSelect
isLocked={readonly}
value={selectedColumn}
onSelectOption={(value) => this.onSelectColumn(value, index)}
options={this.columnsOptions}
searchable={true}
searchPlaceholder={intl.get('Search_column')}
noOptionsPlaceholder={intl.get('No_results')}
searchPlaceholder={gettext('Search column')}
noOptionsPlaceholder={gettext('No results')}
/>
</div>
<div className="sort-predicate ml-2">
<DTableCustomizeSelect
<CustomizeSelect
isLocked={readonly}
value={selectedSortType}
onSelectOption={(value) => this.onSelectSortType(value, index)}
@@ -254,22 +251,22 @@ class SortPopover extends Component {
<div ref={ref => this.sortPopoverRef = ref} onClick={this.onPopoverInsideClick}>
<div className={`sorts-list ${isEmpty ? 'empty-sorts-container' : ''}`} >
{isEmpty ?
<div className="empty-sorts-list">{intl.get('No_sorts')}</div> :
<div className="empty-sorts-list">{gettext('No sorts')}</div> :
this.renderSortsList()
}
</div>
{!readonly &&
<CommonAddTool
<CustomizeAddTool
callBack={this.addSort}
footerName={intl.get('Add_sort')}
footerName={gettext('Add sort')}
className="popover-add-tool"
addIconClassName="popover-add-icon"
/>
}
{(this.isNeedSubmit() && !readonly) && (
<div className='sort-popover-footer'>
<Button className='mr-2' onClick={this.onClosePopover}>{intl.get('Cancel')}</Button>
<Button color="primary" disabled={this.state.isSubmitDisabled} onClick={this.onSubmitSorts}>{intl.get('Submit')}</Button>
<Button className='mr-2' onClick={this.onClosePopover}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={this.state.isSubmitDisabled} onClick={this.onSubmitSorts}>{gettext('Submit')}</Button>
</div>
)}
</div>

View File

@@ -12,7 +12,7 @@ import './index.css';
const Container = () => {
const [isLoadingMore, setLoadingMore] = useState(false);
const { metadata, errorMsg, extendMetadataRows } = useMetadata();
const { metadata, errorMsg, store } = useMetadata();
const containerRef = useRef(null);
const onKeyDown = useCallback((event) => {
@@ -33,26 +33,21 @@ const Container = () => {
// todo
}, []);
const tableChanged = useCallback(() => {
// todo
}, []);
const handleTableError = useCallback((error) => {
const errorMsg = getErrorMsg(error);
toaster.danger(gettext(errorMsg));
}, []);
const updateMetadata = useCallback(() => {
// todo
}, []);
const loadMore = useCallback(() => {
const loadMore = useCallback(async () => {
if (!metadata.hasMore) return;
setLoadingMore(true);
extendMetadataRows((flag) => {
try {
await store.loadMore();
setLoadingMore(false);
});
}, [metadata, extendMetadataRows]);
} catch (error) {
const errorMsg = getErrorMsg(error);
toaster.danger(gettext(errorMsg));
setLoadingMore(false);
return;
}
}, [metadata, store]);
const modifyRecords = useCallback((rowIds, idRowUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, isCopyPaste = false) => {
// todo: store op
@@ -88,12 +83,14 @@ const Container = () => {
}, [metadata]);
const modifyFilters = useCallback((filters, filterConjunction) => {
// modifyFilters
}, []);
store.modifyFilters(filterConjunction, filters);
store.saveView();
}, [store]);
const modifySorts = useCallback((sorts) => {
// modifySorts
}, []);
store.modifySorts(sorts);
store.saveView();
}, [store]);
const modifyGroupbys = useCallback(() => {
// modifyGroupbys
@@ -137,17 +134,9 @@ const Container = () => {
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
const unsubscribeSelectCell = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_CELL, onSelectCell);
const unsubscribeServerTableChanged = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED, tableChanged);
const unsubscribeTableChanged = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_TABLE_CHANGED, tableChanged);
const unsubscribeHandleTableError = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TABLE_ERROR, handleTableError);
const unsubscribeUpdateRows = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_TABLE_ROWS, updateMetadata);
return () => {
document.removeEventListener('keydown', onKeyDown);
unsubscribeSelectCell();
unsubscribeServerTableChanged();
unsubscribeTableChanged();
unsubscribeHandleTableError();
unsubscribeUpdateRows();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -155,7 +144,7 @@ const Container = () => {
return (
<>
<div className="sf-metadata-wrapper">
<TableTool modifyFilters={modifyFilters} modifySorts={modifySorts} modifyGroupbys={modifyGroupbys} modifyHiddenColumns={modifyHiddenColumns} />
<TableTool view={metadata.view} columns={metadata.columns} modifyFilters={modifyFilters} modifySorts={modifySorts} modifyGroupbys={modifyGroupbys} modifyHiddenColumns={modifyHiddenColumns} />
<div className="sf-metadata-main">
{errorMsg && (<div className="d-center-middle error">{gettext(errorMsg)}</div>)}
{!errorMsg && (

View File

@@ -33,10 +33,10 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
<div className={classnames('table-main-container container-fluid p-0', { [`group-level-${groupbysCount + 1}`]: groupbysCount > 0 })}>
<Records
columns={metadata.columns}
recordIds={metadata.row_ids || []}
recordIds={metadata.view.rows || []}
groups={metadata.groups}
groupbys={metadata.groupbys}
recordsCount={metadata.recordsCount}
recordsCount={metadata?.view?.rows?.length || 0}
table={metadata}
hasMore={metadata.hasMore}
gridUtils={gridUtils}

View File

@@ -30,10 +30,24 @@
margin-left: 0 !important;
}
.sf-metadata-tool .custom-filter-label {
.sf-metadata-tool .sf-metadata-tool-left-operations .custom-filter-label,
.sf-metadata-tool .sf-metadata-tool-left-operations .custom-sort-label {
padding: 0 .5rem;
}
.sf-metadata-tool .sf-metadata-tool-left-operations .custom-filter-label.active:hover,
.sf-metadata-tool .sf-metadata-tool-left-operations .custom-sort-label.active:hover {
box-shadow: inset 0 0 0 2px #0000001a;
}
.sf-metadata-tool .sf-metadata-tool-left-operations .custom-filter-label.active {
background-color: #d1f7c4;
}
.sf-metadata-tool .sf-metadata-tool-left-operations .custom-sort-label.active {
background-color: #f5d9bc;
}
.sf-metadata-tool .setting-item-btn {
border-radius: 4px;
cursor: pointer;

View File

@@ -4,10 +4,13 @@ import classnames from 'classnames';
import { FilterSetter, GroupbySetter, SortSetter, HideColumnSetter } from '../../data-process-setter';
import { Z_INDEX } from '../../../_basic';
import { EVENT_BUS_TYPE } from '../../../constants';
import { useCollaborators } from '../../../hooks';
import './index.css';
const TableTool = ({ searcherActive, onFiltersChange, onSortsChange, modifyGroupbys, modifyHiddenColumns }) => {
const TableTool = ({ searcherActive, view, columns, modifyFilters, modifySorts, modifyGroupbys, modifyHiddenColumns }) => {
const { collaborators } = useCollaborators();
const onHeaderClick = useCallback(() => {
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
@@ -21,21 +24,21 @@ const TableTool = ({ searcherActive, onFiltersChange, onSortsChange, modifyGroup
>
<div className="sf-metadata-tool-left-operations">
<FilterSetter
wrapperClass={'custom-tool-label custom-filter-label'}
wrapperClass="custom-tool-label custom-filter-label"
filtersClassName="sf-metadata-filters"
target={'sf-metadata-filter-popover'}
filterConjunction={''}
filters={[]}
columns={[]}
onFiltersChange={onFiltersChange}
collaborators={[]}
target="sf-metadata-filter-popover"
filterConjunction={view.filter_conjunction}
filters={view.filters}
columns={columns}
modifyFilters={modifyFilters}
collaborators={collaborators}
/>
<SortSetter
wrapperClass={'custom-tool-label custom-sort-label'}
target={'sf-metadata-sort-popover'}
sorts={[]}
columns={[]}
onSortsChange={onSortsChange}
wrapperClass="custom-tool-label custom-sort-label"
target="sf-metadata-sort-popover"
sorts={view.sorts}
columns={columns}
modifySorts={modifySorts}
/>
<GroupbySetter
wrapperClass={'custom-tool-label custom-groupby-label'}
@@ -58,8 +61,10 @@ const TableTool = ({ searcherActive, onFiltersChange, onSortsChange, modifyGroup
TableTool.propTypes = {
searcherActive: PropTypes.bool,
onFiltersChange: PropTypes.func,
onSortsChange: PropTypes.func,
view: PropTypes.object,
columns: PropTypes.array,
modifyFilters: PropTypes.func,
modifySorts: PropTypes.func,
modifyGroupbys: PropTypes.func,
modifyHiddenColumns: PropTypes.func,
};

View File

@@ -97,6 +97,8 @@ export const DEFAULT_COLUMNS = [
{ name: 'Is_dir', type: CellType.TEXT, width: 200, editable: false, key: 'is_dir' },
];
export const PER_PAGE_COUNT = 1000;
export {
EVENT_BUS_TYPE,
TRANSFER_TYPES,

View File

@@ -1,6 +1,7 @@
import metadataAPI from '../api';
import { UserService, LocalStorage } from './_basic';
import EventBus from './utils/event-bus';
import { username } from '../../utils/constants';
class Context {
@@ -54,9 +55,20 @@ class Context {
this.settings[key] = value;
};
getUsername = () => {
return username;
};
// collaborators
getCollaborators = () => {
const repoID = this.settings['repoID'];
return this.metadataAPI.getCollaborators(repoID);
};
// metadata
getMetadata = (repoID) => {
return this.metadataAPI.getMetadata(repoID);
getMetadata = (params) => {
const repoID = this.settings['repoID'];
return this.metadataAPI.getMetadata(repoID, params);
};
canModifyCell = (column) => {
@@ -74,6 +86,27 @@ class Context {
getCollaboratorsFromCache = () => {
//
};
restoreRows = () => {
// todo
};
updateRows = () => {
// todo
};
lockRowViaButton = () => {
// todo
};
updateRowViaButton = () => {
// todo
};
getRowsByIds = () => {
// todo
};
}
export default Context;

View File

@@ -1,23 +1,27 @@
/* eslint-disable react/prop-types */
import React, { useContext, useState, useRef, useCallback } from 'react';
import React, { useContext, useState, useRef, useCallback, useEffect } from 'react';
import { useMetadata } from './metadata';
const CollaboratorsContext = React.createContext(null);
export const CollaboratorsProvider = ({
collaborators,
collaboratorsCache: propsCollaboratorsCache,
updateCollaboratorsCache: propsUpdateCollaboratorsCache,
children,
}) => {
const collaboratorsCacheRef = useRef(propsCollaboratorsCache || {});
const [collaboratorsCache, setCollaboratorsCache] = useState(propsCollaboratorsCache || {});
const collaboratorsCacheRef = useRef({});
const [collaboratorsCache, setCollaboratorsCache] = useState({});
const [collaborators, setCollaborators ] = useState([]);
const { store } = useMetadata();
useEffect(() => {
setCollaborators(store?.collaborators || []);
}, [store?.collaborators]);
const updateCollaboratorsCache = useCallback((user) => {
const newCollaboratorsCache = { ...collaboratorsCacheRef.current, [user.email]: user };
collaboratorsCacheRef.current = newCollaboratorsCache;
setCollaboratorsCache(newCollaboratorsCache);
propsUpdateCollaboratorsCache && propsUpdateCollaboratorsCache(user);
}, [propsUpdateCollaboratorsCache]);
}, []);
return (
<CollaboratorsContext.Provider value={{ collaborators, collaboratorsCache, updateCollaboratorsCache }}>

View File

@@ -1,10 +1,11 @@
/* eslint-disable react/prop-types */
import React, { useCallback, useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState, useCallback } from 'react';
import { toaster } from '@seafile/sf-metadata-ui-component';
import { Metadata } from '../model';
import { gettext } from '../../../utils/constants';
import { getErrorMsg, CellType, PRIVATE_COLUMN_KEY, NOT_DISPLAY_COLUMN_KEYS } from '../_basic';
import { getErrorMsg } from '../_basic';
import Context from '../context';
import Store from '../store';
import { EVENT_BUS_TYPE } from '../constants';
const MetadataContext = React.createContext(null);
@@ -13,132 +14,56 @@ export const MetadataProvider = ({
...params
}) => {
const [isLoading, setLoading] = useState(true);
const [metadata, setMetadata] = useState({ records: [], columns: [] });
const [metadata, setMetadata] = useState({ rows: [], columns: [] });
const storeRef = useRef(null);
const getColumnName = useCallback((key, name) => {
switch (key) {
case PRIVATE_COLUMN_KEY.CTIME:
return gettext('Created time');
case PRIVATE_COLUMN_KEY.MTIME:
return gettext('Last modified time');
case PRIVATE_COLUMN_KEY.CREATOR:
return gettext('Creator');
case PRIVATE_COLUMN_KEY.LAST_MODIFIER:
return gettext('Last modifier');
case PRIVATE_COLUMN_KEY.FILE_CREATOR:
return gettext('File creator');
case PRIVATE_COLUMN_KEY.FILE_MODIFIER:
return gettext('File modifier');
case PRIVATE_COLUMN_KEY.FILE_CTIME:
return gettext('File created time');
case PRIVATE_COLUMN_KEY.FILE_MTIME:
return gettext('File last modified time');
case PRIVATE_COLUMN_KEY.IS_DIR:
return gettext('Is dir');
case PRIVATE_COLUMN_KEY.PARENT_DIR:
return gettext('Parent dir');
case PRIVATE_COLUMN_KEY.FILE_NAME:
return gettext('File name');
default:
return name;
}
const tableChanged = useCallback(() => {
setMetadata(storeRef.current.data);
}, []);
const getColumnType = useCallback((key, type) => {
switch (key) {
case PRIVATE_COLUMN_KEY.CTIME:
case PRIVATE_COLUMN_KEY.FILE_CTIME:
return CellType.CTIME;
case PRIVATE_COLUMN_KEY.MTIME:
case PRIVATE_COLUMN_KEY.FILE_MTIME:
return CellType.MTIME;
case PRIVATE_COLUMN_KEY.CREATOR:
case PRIVATE_COLUMN_KEY.FILE_CREATOR:
return CellType.CREATOR;
case PRIVATE_COLUMN_KEY.LAST_MODIFIER:
case PRIVATE_COLUMN_KEY.FILE_MODIFIER:
return CellType.LAST_MODIFIER;
case PRIVATE_COLUMN_KEY.FILE_NAME:
return CellType.FILE_NAME;
default:
return type;
}
const handleTableError = useCallback((error) => {
const errorMsg = getErrorMsg(error);
toaster.danger(gettext(errorMsg));
}, []);
const getColumns = useCallback((columns) => {
if (!Array.isArray(columns) || columns.length === 0) return [];
const validColumns = columns.map((column) => {
const { type, key, name, ...params } = column;
return {
key,
type: getColumnType(key, type),
name: getColumnName(key, name),
...params,
width: 200,
};
}).filter(column => !NOT_DISPLAY_COLUMN_KEYS.includes(column.key));
let displayColumns = [];
validColumns.forEach(column => {
if (column.key === '_name') {
displayColumns.unshift(column);
} else if (column.key === PRIVATE_COLUMN_KEY.PARENT_DIR) {
const nameColumnIndex = displayColumns.findIndex(column => column.key === PRIVATE_COLUMN_KEY.PARENT_DIR);
if (nameColumnIndex === -1) {
displayColumns.unshift(column);
} else {
displayColumns.splice(nameColumnIndex, 0, column);
}
} else {
displayColumns.push(column);
}
});
return displayColumns;
}, [getColumnType, getColumnName]);
const updateMetadata = useCallback((data) => {
setMetadata(data);
}, []);
// init
useEffect(() => {
const init = async () => {
// init context
const context = new Context();
window.sfMetadataContext = context;
await window.sfMetadataContext.init({ otherSettings: params });
const repoID = window.sfMetadataContext.getSetting('repoID');
window.sfMetadataContext.getMetadata(repoID).then(res => {
setMetadata(new Metadata({ rows: res?.data?.results || [], columns: getColumns(res?.data?.metadata) }));
window.sfMetadataContext.init({ otherSettings: params });
const repoId = window.sfMetadataContext.getSetting('repoID');
storeRef.current = new Store({ context: window.sfMetadataContext, repoId });
storeRef.current.initStartIndex();
storeRef.current.loadData().then(() => {
setMetadata(storeRef.current.data);
setLoading(false);
}).catch(error => {
const errorMsg = getErrorMsg(error);
toaster.danger(gettext(errorMsg));
});
};
init();
const unsubscribeServerTableChanged = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED, tableChanged);
const unsubscribeTableChanged = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_TABLE_CHANGED, tableChanged);
const unsubscribeHandleTableError = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TABLE_ERROR, handleTableError);
const unsubscribeUpdateRows = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_TABLE_ROWS, updateMetadata);
return () => {
window.sfMetadataContext.destroy();
unsubscribeServerTableChanged();
unsubscribeTableChanged();
unsubscribeHandleTableError();
unsubscribeUpdateRows();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const extendMetadataRows = useCallback((callback) => {
const repoID = window.sfMetadataContext.getSetting('repoID');
window.sfMetadataContext.getMetadata(repoID).then(res => {
const rows = res?.data?.results || [];
metadata.extendRows(rows);
setMetadata(metadata);
callback && callback(true);
}).catch(error => {
const errorMsg = getErrorMsg(error);
toaster.danger(gettext(errorMsg));
callback && callback(false);
});
}, [metadata]);
return (
<MetadataContext.Provider value={{ isLoading, metadata, extendMetadataRows }}>
<MetadataContext.Provider value={{ isLoading, metadata, store: storeRef.current }}>
{children}
</MetadataContext.Provider>
);
@@ -149,6 +74,6 @@ export const useMetadata = () => {
if (!context) {
throw new Error('\'MetadataContext\' is null');
}
const { isLoading, metadata, extendMetadataRows } = context;
return { isLoading, metadata, extendMetadataRows };
const { isLoading, metadata, store } = context;
return { isLoading, metadata, store };
};

View File

@@ -3,16 +3,11 @@ import PropTypes from 'prop-types';
import { MetadataProvider, CollaboratorsProvider, RecordDetailsProvider } from './hooks/index';
import { Table } from './components/index';
const SeafileMetadata = ({ collaborators, collaboratorsCache, updateCollaboratorsCache, ...params }) => {
const collaboratorsProviderProps = {
collaborators,
collaboratorsCache,
updateCollaboratorsCache,
};
const SeafileMetadata = ({ ...params }) => {
return (
<MetadataProvider { ...params }>
<CollaboratorsProvider { ...collaboratorsProviderProps }>
<CollaboratorsProvider >
<RecordDetailsProvider>
<Table />
</RecordDetailsProvider>

View File

@@ -1,3 +1,6 @@
import { PER_PAGE_COUNT } from '../../constants';
import View from './view';
class Metadata {
constructor(object) {
this.columns = object.columns || [];
@@ -9,25 +12,12 @@ class Metadata {
this.id_row_map[record._id] = record;
});
this.hasMore = object.hasMore || false;
this.hasMore = this.rows.length === PER_PAGE_COUNT;
this.recordsCount = object.recordsCount || this.row_ids.length;
this.page = 1;
this.perPageCount = 1000;
this.view = new View(object.view || {});
}
extendRows = (rows) => {
if (!Array.isArray(rows) || rows.length === 0) {
this.hasMore = false;
return;
}
this.rows.push(...rows);
rows.forEach(record => {
this.row_ids.push(record._id);
this.id_row_map[record._id] = record;
});
};
}
export default Metadata;

View File

@@ -0,0 +1,14 @@
class View {
constructor(object) {
this.filters = object.filters || [];
this.filter_conjunction = object.filter_conjunction || 'Or';
this.sorts = object.sorts || [];
this.groupbys = object.groupbys || [];
this.groups = object.groups;
this.rows = object.rows || [];
// this.available_columns = object.available_columns || [];
}
}
export default View;

View File

@@ -0,0 +1,247 @@
import {
getRowsByIds,
sortTableRows,
isTableRows,
isFilterView,
isSortView,
isGroupView,
getFilteredRows,
getGroupRows,
getColumnByKey,
} from '../_basic';
import { username } from '../../../utils/constants';
import { OPERATION_TYPE } from './operations';
// const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = {
// isUpdateSummaries: true,
// isUpdateColumnColors: true,
// };
// generate formula_rows
// get rendered rows depend on filters/sorts etc.
class DataProcessor {
static getFilteredRows(table, rows, filterConjunction, filters) {
const tableRows = isTableRows(rows) ? rows : getRowsByIds(table, rows);
const { row_ids } = getFilteredRows(table, tableRows, filterConjunction, filters, { username });
return row_ids;
}
static getSortedRows(table, rows, sorts) {
const tableRows = isTableRows(rows) ? rows : getRowsByIds(table, rows);
return sortTableRows(table, tableRows, sorts);
}
static getGroupedRows(table, rows, groupbys) {
const tableRows = isTableRows(rows) ? rows : getRowsByIds(table, rows);
const groups = getGroupRows(table, tableRows, groupbys);
// todo update summaries
return groups;
}
static updateSummaries(table, rows) {
// const tableRows = isTableRows(rows) ? rows : getRowsByIds(table, rows);
// todo
}
static hasRelatedGroupby(groupbys, updatedColumnKeyMap) {
return groupbys.some(groupby => updatedColumnKeyMap[groupby.column_key]);
}
static deleteGroupRows(groups, idDeletedRecordMap) {
groups.forEach(group => {
const { subgroups, row_ids } = group;
if (Array.isArray(subgroups) && subgroups.length > 0) {
this.deleteGroupRows(subgroups, idDeletedRecordMap);
} else if (row_ids) {
group.row_ids = row_ids.filter(rowId => !idDeletedRecordMap[rowId]);
}
});
}
static deleteEmptyGroups = (groups) => {
return groups.filter(group => {
const { subgroups, row_ids } = group;
if (subgroups && subgroups.length > 0) {
const validSubGroups = this.deleteEmptyGroups(subgroups);
if (validSubGroups.length === 0) {
return false;
}
return true;
}
if (!row_ids || row_ids.length === 0) {
return false;
}
return true;
});
};
static run(table) {
const rows = table.rows;
const { filters, filter_conjunction, sorts, groupbys } = table.view;
const availableColumns = table.view.available_columns || table.columns;
const _isFilterView = isFilterView({ filters }, availableColumns);
const _isSortView = isSortView({ sorts }, availableColumns);
const _isGroupView = isGroupView({ groupbys }, availableColumns);
if (!_isFilterView && !_isSortView && !_isGroupView) {
table.view.rows = table.rows.map(row => row._id);
return;
}
let renderedRows = rows;
if (_isFilterView) {
renderedRows = this.getFilteredRows(table, renderedRows, filter_conjunction, filters);
}
if (_isSortView) {
renderedRows = this.getSortedRows(table, renderedRows, sorts);
}
const groups = _isGroupView ? this.getGroupedRows(table, renderedRows, groupbys) : [];
const row_ids = isTableRows(renderedRows) ? renderedRows.map(row => row._id) : renderedRows;
table.view.rows = row_ids;
table.view.groups = groups;
}
static updateDataWithModifyRecords(table, relatedColumnKeyMap, ) {
// todo
}
static handleReloadedRecords(table, reloadedRecords, relatedColumnKeyMap) {
const idReloadedRecordMap = reloadedRecords.reduce((map, record) => {
map[record._id] = record;
return map;
}, {});
table.rows.forEach((row, index) => {
const rowId = row._id;
const reloadedRecord = idReloadedRecordMap[rowId];
const newRecord = Object.assign({}, table.rows[index], reloadedRecord);
if (reloadedRecord) {
table.rows[index] = newRecord;
table.id_row_map[rowId] = newRecord;
}
});
this.updateDataWithModifyRecords();
this.updateSummaries();
}
static handleNotExistRecords(table, idRecordNotExistMap) {
let notExistRecords = [];
let existRecords = [];
table.rows.forEach((record) => {
const recordId = record._id;
if (idRecordNotExistMap[recordId]) {
notExistRecords.push(record);
delete table.id_row_map[recordId];
} else {
existRecords.push(record);
}
});
table.rows = table.rows.filter((record) => !idRecordNotExistMap[record._id]);
table.view.rows = table.rows.filter((recordId) => !idRecordNotExistMap[recordId]);
this.updateSummaries();
}
static updatePageDataWithCommonOperations(table, value) {
// todo
}
static syncOperationOnData(table, operation) {
switch (operation.op_type) {
case OPERATION_TYPE.MODIFY_RECORD:
case OPERATION_TYPE.MODIFY_RECORDS: {
const { available_columns } = table.view;
const { id_original_row_updates, row_ids } = operation;
let relatedColumnKeyMap = {};
let relatedColumnKeys = [];
row_ids.forEach(rowId => {
const id_original_row_update = id_original_row_updates[rowId];
if (id_original_row_update) {
relatedColumnKeys.push(...Object.keys(id_original_row_update));
}
});
relatedColumnKeys.forEach(columnKey => {
if (!relatedColumnKeyMap[columnKey]) {
const column = getColumnByKey(available_columns, columnKey);
if (column) {
relatedColumnKeyMap[columnKey] = true;
}
}
});
this.updateDataWithModifyRecords();
this.updateSummaries();
break;
}
case OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON: {
const { available_columns } = table.view;
const { original_updates } = operation;
const relatedColumnKeyMap = {};
for (let columnKey in original_updates) {
const column = getColumnByKey(available_columns, columnKey);
if (column) {
relatedColumnKeyMap[columnKey] = true;
}
}
this.updateDataWithModifyRecords();
this.updateSummaries();
break;
}
case OPERATION_TYPE.RESTORE_RECORDS: {
const { rows_data, upper_row_ids } = operation;
const { rows } = table.view;
const insertRowIds = rows_data.map(rowData => rowData._id);
let updatedRowIds = [...rows];
if (!Array.isArray(upper_row_ids) || upper_row_ids.length === 0) {
updatedRowIds.push(...insertRowIds);
} else {
upper_row_ids.forEach((upperRowId, index) => {
const insertRowId = insertRowIds[index];
const upperRowIndex = updatedRowIds.indexOf(upperRowId);
if (upperRowIndex < 0) {
updatedRowIds.push(insertRowId);
} else {
updatedRowIds.splice(upperRowIndex + 1, 0, insertRowId);
}
});
}
table.view.rows = updatedRowIds;
this.updatePageDataWithCommonOperations();
this.updateSummaries();
break;
}
case OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS:
case OPERATION_TYPE.MODIFY_FILTERS: {
this.run(table);
break;
}
case OPERATION_TYPE.MODIFY_SORTS: {
const { sorts, rows } = table.view;
const availableColumns = table.view.available_columns || table.columns;
if (!isSortView({ sorts }, availableColumns)) {
this.run(table);
break;
}
table.view.rows = this.getSortedRows(table, rows, sorts);
this.updatePageDataWithCommonOperations();
break;
}
case OPERATION_TYPE.MODIFY_GROUPBYS: {
const { available_columns, groupbys, rows } = table.view;
if (!isGroupView({ groupbys }, available_columns)) {
table.view.groups = [];
break;
}
table.view.groups = this.getGroupedRows(table, rows, groupbys);
break;
}
default: {
break;
}
}
}
}
export default DataProcessor;

View File

@@ -0,0 +1,354 @@
import deepCopy from 'deep-copy';
import {
getRowById,
getRowsByIds,
} from '../_basic';
import { Operation, LOCAL_APPLY_OPERATION_TYPE, NEED_APPLY_AFTER_SERVER_OPERATION, OPERATION_TYPE, UNDO_OPERATION_TYPE } from './operations';
import { EVENT_BUS_TYPE } from '../constants';
import DataProcessor from './data-processor';
import ServerOperator from './server-operator';
import { getColumns } from '../utils/column-utils';
import { Metadata, User } from '../model';
import { PER_PAGE_COUNT } from '../constants';
class Store {
constructor(props) {
this.repoId = props.repoId;
this.data = null;
this.context = props.context;
this.startIndex = 1;
this.redos = [];
this.undos = [];
this.pendingOperations = [];
this.isSendingOperation = false;
this.isTableReadonly = false;
this.serverOperator = new ServerOperator();
this.collaborators = [];
}
initStartIndex = () => {
this.startIndex = 1;
};
saveView = () => {
const { filters, sorts, gropbys, filter_conjunction } = this.data.view;
const view = { filters, sorts, gropbys, filter_conjunction };
this.context.localStorage.setItem('view', view);
};
async loadData() {
const res = await this.context.getMetadata({ page: this.startIndex });
const view = this.context.localStorage.getItem('view');
let data = new Metadata({ rows: res?.data?.results || [], columns: getColumns(res?.data?.metadata), view });
data.view.rows = data.row_ids;
this.data = data;
this.startIndex += 1;
const collaboratorsRes = await this.context.getCollaborators();
this.collaborators = Array.isArray(collaboratorsRes?.data?.user_list) ? collaboratorsRes.data.user_list.map(user => new User(user)) : [];
DataProcessor.run(this.data);
}
async loadMore() {
if (!this.data) return;
const res = await this.context.getMetadata(this.repoId, { page: this.startIndex });
const rows = res?.data?.results || [];
if (!Array.isArray(rows) || rows.length === 0) {
this.hasMore = false;
return;
}
this.data.rows.push(...rows);
rows.forEach(record => {
this.data.row_ids.push(record._id);
this.data.id_row_map[record._id] = record;
});
this.data.hasMore = rows.length === PER_PAGE_COUNT;
this.startIndex += 1;
DataProcessor.run(this.data);
this.context.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_TABLE_CHANGED);
}
async updateRowData(newRowId) {
const res = await this.context.getRowsByIds(this.repoId, [newRowId]);
if (!res || !res.data) {
return;
}
const newRow = res.data.results[0];
const rowIndex = this.data.rows.findIndex(row => row._id === newRowId);
this.data.id_row_map[newRowId] = newRow;
this.data.rows[rowIndex] = newRow;
DataProcessor.run(this.data);
}
createOperation(op) {
return new Operation(op);
}
applyOperation(operation, undoRedoHandler = { handleUndo: true }) {
const { op_type } = operation;
if (!NEED_APPLY_AFTER_SERVER_OPERATION.includes(op_type)) {
this.handleUndoRedos(undoRedoHandler, operation);
this.data = deepCopy(operation.apply(this.data));
this.syncOperationOnData(operation);
this.context.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_TABLE_CHANGED);
}
if (LOCAL_APPLY_OPERATION_TYPE.includes(op_type)) {
return;
}
this.addPendingOperations(operation, undoRedoHandler);
}
addPendingOperations(operation, undoRedoHandler) {
this.pendingOperations.push(operation);
this.startSendOperation(undoRedoHandler);
}
startSendOperation(undoRedoHandler) {
if (this.isSendingOperation || this.pendingOperations.length === 0) {
return;
}
this.isSendingOperation = true;
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SAVING_APP_DATA);
this.sendNextOperation(undoRedoHandler);
}
sendNextOperation(undoRedoHandler) {
if (this.pendingOperations.length === 0) {
this.isSendingOperation = false;
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SAVED_APP_DATA);
return;
}
const operation = this.pendingOperations.shift();
this.serverOperator.applyOperation(operation, this.sendOperationCallback.bind(this, undoRedoHandler));
}
sendOperationCallback = (undoRedoHandler, { operation, error }) => {
if (error) {
operation.fail_callback && operation.fail_callback();
this.context.eventBus.dispatch(EVENT_BUS_TYPE.TABLE_ERROR, { error });
this.sendNextOperation(undoRedoHandler);
return;
}
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(operation.op_type)) {
this.handleUndoRedos(undoRedoHandler, operation);
this.data = operation.apply(this.data);
this.syncOperationOnData(operation);
}
operation.success_callback && operation.success_callback();
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED);
// need reload records if has related formula columns
this.serverOperator.handleReloadRecords(operation, ({ reloadedRecords, idRecordNotExistMap, relatedColumnKeyMap }) => {
if (reloadedRecords.length > 0) {
DataProcessor.handleReloadedRecords(this.data, reloadedRecords, relatedColumnKeyMap);
}
if (Object.keys(idRecordNotExistMap).length > 0) {
DataProcessor.handleNotExistRecords(this.data, idRecordNotExistMap);
}
this.context.eventBus.dispatch(EVENT_BUS_TYPE.SERVER_TABLE_CHANGED);
});
this.sendNextOperation(undoRedoHandler);
};
handleUndoRedos(undoRedoHandler, operation){
const { handleUndo, asyncUndoRedo } = undoRedoHandler;
if (handleUndo) {
if (this.redos.length > 0) {
this.redos = [];
}
if (this.undos.length > 10) {
this.undos = this.undos.slice(-10);
}
if (UNDO_OPERATION_TYPE.includes(operation.op_type)) {
this.undos.push(operation);
}
}
asyncUndoRedo && asyncUndoRedo(operation);
}
undoOperation() {
if (this.isTableReadonly || this.undos.length === 0) return;
const lastOperation = this.undos.pop();
const lastInvertOperation = lastOperation.invert();
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastInvertOperation.op_type)) {
this.applyOperation(lastInvertOperation, { handleUndo: false, asyncUndoRedo: (operation) => {
if (operation.op_type === OPERATION_TYPE.INSERT_RECORD) {
lastOperation.row_id = operation.row_data._id;
}
this.redos.push(lastOperation);
} });
return;
}
this.redos.push(lastOperation);
this.applyOperation(lastInvertOperation, { handleUndo: false });
}
redoOperation() {
if (this.isTableReadonly || this.redos.length === 0) return;
let lastOperation = this.redos.pop();
if (NEED_APPLY_AFTER_SERVER_OPERATION.includes(lastOperation.op_type)) {
this.applyOperation(lastOperation, { handleUndo: false, asyncUndoRedo: (operation) => {
if (operation.op_type === OPERATION_TYPE.INSERT_RECORD) {
lastOperation = operation;
}
this.undos.push(lastOperation);
} });
return;
}
this.undos.push(lastOperation);
this.applyOperation(lastOperation, { handleUndo: false });
}
syncOperationOnData(operation) {
DataProcessor.syncOperationOnData(this.data, operation);
}
/**
* @param {String} row_id target row id
* @param {Object} updates { [column.name]: cell_value }
* @param {Object} original_updates { [column.key]: cell_value }
* @param {Object} old_row_data { [column.name]: cell_value }
* @param {Object} original_old_row_data { [column.key]: cell_value }
*/
modifyRecord(row_id, updates, old_row_data, original_updates, original_old_row_data) {
const row = getRowById(this.data, row_id);
if (!row || !this.context.eventBus.canModifyRow(row)) {
return;
}
const type = OPERATION_TYPE.MODIFY_RECORD;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_id,
updates,
old_row_data,
original_updates,
original_old_row_data,
});
this.applyOperation(operation);
}
modifyRecords(row_ids, id_row_updates, id_original_row_updates, id_old_row_data, id_original_old_row_data, is_copy_paste) {
const originalRows = getRowsByIds(this.data, row_ids);
let valid_row_ids = [];
let valid_id_row_updates = {};
let valid_id_original_row_updates = {};
let valid_id_old_row_data = {};
let valid_id_original_old_row_data = {};
originalRows.forEach(row => {
if (!row || !this.context.canModifyRow(row)) {
return;
}
const rowId = row._id;
valid_row_ids.push(rowId);
valid_id_row_updates[rowId] = id_row_updates[rowId];
valid_id_original_row_updates[rowId] = id_original_row_updates[rowId];
valid_id_old_row_data[rowId] = id_old_row_data[rowId];
valid_id_original_old_row_data[rowId] = id_original_old_row_data[rowId];
});
const type = OPERATION_TYPE.MODIFY_RECORDS;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_ids: valid_row_ids,
id_row_updates: valid_id_row_updates,
id_original_row_updates: valid_id_original_row_updates,
id_old_row_data: valid_id_old_row_data,
id_original_old_row_data: valid_id_original_old_row_data,
is_copy_paste,
});
this.applyOperation(operation);
}
reloadRecords(row_ids) {
const type = OPERATION_TYPE.RELOAD_RECORDS;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_ids,
});
this.applyOperation(operation);
}
lockRecordViaButton(row_id, button_column_key, { success_callback, fail_callback }) {
const type = OPERATION_TYPE.LOCK_RECORD_VIA_BUTTON;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_id,
button_column_key,
success_callback,
fail_callback,
});
this.applyOperation(operation);
}
/**
* @param {String} row_id target row id
* @param {Object} updates { [column.name]: cell_value }
* @param {Object} original_updates { [column.key]: cell_value }
* @param {Object} old_row_data { [column.name]: cell_value }
* @param {Object} original_old_row_data { [column.key]: cell_value }
* @param {String} button_column_key button column key
*/
modifyRecordViaButton(row_id, updates, old_row_data, original_updates, original_old_row_data, button_column_key, { success_callback, fail_callback }) {
const row = getRowById(this.data, row_id);
if (!row) {
return;
}
const type = OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON;
const operation = this.createOperation({
type,
repo_id: this.repoId,
row_id,
updates,
old_row_data,
original_updates,
original_old_row_data,
button_column_key,
success_callback,
fail_callback,
});
this.applyOperation(operation);
}
modifyFilters(filterConjunction, filters) {
const type = OPERATION_TYPE.MODIFY_FILTERS;
const operation = this.createOperation({
type, filter_conjunction: filterConjunction, filters,
});
this.applyOperation(operation);
}
modifySorts(sorts) {
const type = OPERATION_TYPE.MODIFY_SORTS;
const operation = this.createOperation({
type, sorts,
});
this.applyOperation(operation);
}
modifyGroupbys(groupbys) {
const type = OPERATION_TYPE.MODIFY_GROUPBYS;
const operation = this.createOperation({
type, groupbys,
});
this.applyOperation(operation);
}
modifyHiddenColumns(shown_column_keys) {
const type = OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS;
const operation = this.createOperation({
type, shown_column_keys
});
this.applyOperation(operation);
}
}
export default Store;

View File

@@ -0,0 +1,125 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { UTC_FORMAT_DEFAULT } from '../../_basic';
import { OPERATION_TYPE } from './constants';
dayjs.extend(utc);
export default function apply(data, operation) {
const { op_type } = operation;
switch (op_type) {
case OPERATION_TYPE.MODIFY_RECORD: {
const { row_id, original_updates } = operation;
const { rows } = data;
const updatedRowIndex = rows.findIndex(row => row_id === row._id);
if (updatedRowIndex < 0) {
return data;
}
const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT);
const modifier = window.sfMetadataContext.getUsername();
const updatedRow = Object.assign({},
rows[updatedRowIndex],
original_updates,
{ '_mtime': modifyTime, '_last_modifier': modifier },
);
data.rows[updatedRowIndex] = updatedRow;
data.id_row_map[row_id] = updatedRow;
return data;
}
case OPERATION_TYPE.MODIFY_RECORDS: {
const { id_original_row_updates } = operation;
const { rows } = data;
const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT);
const modifier = window.sfMetadataContext.getUsername();
let updatedRows = [...rows];
rows.forEach((row, index) => {
const rowId = row._id;
const rowUpdates = id_original_row_updates[rowId];
if (!rowUpdates) {
return;
}
const updatedRow = Object.assign({}, row, rowUpdates, {
'_mtime': modifyTime,
'_last_modifier': modifier,
});
updatedRows[index] = updatedRow;
data.id_row_map[rowId] = updatedRow;
});
data.rows = updatedRows;
return data;
}
case OPERATION_TYPE.RESTORE_RECORDS: {
const { original_rows } = operation;
const currentTime = dayjs().utc().format(UTC_FORMAT_DEFAULT);
const username = window.sfMetadataContext.getUsername();
let insertRows = [];
original_rows.forEach(row => {
const insertRow = {
...row,
_ctime: currentTime,
_mtime: currentTime,
_creator: username,
_last_modifier: username,
};
insertRows.push(insertRow);
data.id_row_map[row._id] = insertRow;
});
data.rows.push(insertRows);
return data;
}
case OPERATION_TYPE.LOCK_RECORD_VIA_BUTTON: {
const { row_id } = operation;
const { rows } = data;
const updatedRowIndex = rows.findIndex(row => row_id === row._id);
if (updatedRowIndex < 0) return data;
const updatedRow = { ...rows[updatedRowIndex], _locked: true };
data.rows[updatedRowIndex] = updatedRow;
data.id_row_map[row_id] = updatedRow;
return data;
}
case OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON: {
const { row_id, original_updates } = operation;
const { rows } = data;
const updatedRowIndex = rows.findIndex(row => row_id === row._id);
if (updatedRowIndex < 0) {
return data;
}
const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT);
const modifier = window.sfMetadataContext.getUsername();
const updatedRow = Object.assign({},
rows[updatedRowIndex],
original_updates,
{ '_mtime': modifyTime, '_last_modifier': modifier },
);
data.rows[updatedRowIndex] = updatedRow;
data.id_row_map[row_id] = updatedRow;
return data;
}
case OPERATION_TYPE.MODIFY_FILTERS: {
const { filter_conjunction, filters } = operation;
data.view.filter_conjunction = filter_conjunction;
data.view.filters = filters;
return data;
}
case OPERATION_TYPE.MODIFY_SORTS: {
const { sorts } = operation;
data.view.sorts = sorts;
return data;
}
case OPERATION_TYPE.MODIFY_GROUPBYS: {
const { groupbys } = operation;
data.view.groupbys = groupbys;
return data;
}
case OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS: {
const { shown_column_keys } = operation;
data.view.shown_column_keys = shown_column_keys;
return data;
}
default: {
return data;
}
}
}

View File

@@ -0,0 +1,45 @@
export const OPERATION_TYPE = {
MODIFY_RECORD: 'modify_record',
MODIFY_RECORDS: 'modify_records',
RESTORE_RECORDS: 'restore_records',
RELOAD_RECORDS: 'reload_records',
MODIFY_FILTERS: 'modify_filters',
MODIFY_SORTS: 'modify_sorts',
MODIFY_GROUPBYS: 'modify_groupbys',
MODIFY_HIDDEN_COLUMNS: 'modify_hidden_columns',
LOCK_RECORD_VIA_BUTTON: 'lock_record_via_button',
MODIFY_RECORD_VIA_BUTTON: 'modify_record_via_button'
};
export const OPERATION_ATTRIBUTES = {
[OPERATION_TYPE.MODIFY_RECORD]: ['repo_id', 'row_id', 'updates', 'old_row_data', 'original_updates', 'original_old_row_data'],
[OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste'],
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
[OPERATION_TYPE.MODIFY_FILTERS]: ['filter_conjunction', 'filters'],
[OPERATION_TYPE.MODIFY_SORTS]: ['sorts'],
[OPERATION_TYPE.MODIFY_GROUPBYS]: ['groupbys'],
[OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS]: ['shown_column_keys'],
[OPERATION_TYPE.LOCK_RECORD_VIA_BUTTON]: ['repo_id', 'row_id', 'button_column_key'],
[OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON]: ['repo_id', 'row_id', 'updates', 'old_row_data', 'original_updates', 'original_old_row_data', 'button_column_key'],
};
export const UNDO_OPERATION_TYPE = [
OPERATION_TYPE.MODIFY_RECORD,
OPERATION_TYPE.MODIFY_RECORDS,
OPERATION_TYPE.RESTORE_RECORDS,
];
// only apply operation on the local
export const LOCAL_APPLY_OPERATION_TYPE = [
OPERATION_TYPE.MODIFY_FILTERS,
OPERATION_TYPE.MODIFY_SORTS,
OPERATION_TYPE.MODIFY_GROUPBYS,
OPERATION_TYPE.MODIFY_HIDDEN_COLUMNS,
];
// apply operation after exec operation on the server
export const NEED_APPLY_AFTER_SERVER_OPERATION = [
OPERATION_TYPE.INSERT_RECORD,
OPERATION_TYPE.INSERT_RECORDS,
];

View File

@@ -0,0 +1,17 @@
import apply from './apply';
import invert from './invert';
import Operation from './model';
export {
OPERATION_TYPE,
OPERATION_ATTRIBUTES,
UNDO_OPERATION_TYPE,
LOCAL_APPLY_OPERATION_TYPE,
NEED_APPLY_AFTER_SERVER_OPERATION,
} from './constants';
export {
apply,
invert,
Operation,
};

View File

@@ -0,0 +1,57 @@
import deepCopy from 'deep-copy';
import Operation from './model';
import { OPERATION_TYPE } from './constants';
function createOperation(op) {
return new Operation(op);
}
export default function invert(operation) {
const { op_type } = operation.clone();
switch (op_type) {
case OPERATION_TYPE.MODIFY_RECORD: {
const { page_id, row_id, updates, old_row_data, original_updates, original_old_row_data } = operation;
return createOperation({
type: OPERATION_TYPE.MODIFY_RECORD,
page_id,
row_id,
updates: deepCopy(old_row_data),
old_row_data: deepCopy(updates),
original_updates: deepCopy(original_old_row_data),
original_old_row_data: deepCopy(original_updates),
});
}
case OPERATION_TYPE.MODIFY_RECORDS: {
const {
page_id, is_copy_paste, row_ids, id_row_updates, id_original_row_updates,
id_old_row_data, id_original_old_row_data,
} = operation;
return createOperation({
type: OPERATION_TYPE.MODIFY_RECORDS,
page_id,
is_copy_paste,
row_ids: deepCopy(row_ids),
id_row_updates: deepCopy(id_old_row_data),
id_original_row_updates: deepCopy(id_original_old_row_data),
id_old_row_data: deepCopy(id_row_updates),
id_original_old_row_data: deepCopy(id_original_row_updates),
});
}
case OPERATION_TYPE.RESTORE_RECORDS: {
const { page_id, rows_data, original_rows, link_infos, upper_row_ids, } = operation;
const row_ids = rows_data.map(rowData => rowData._id);
return createOperation({
type: OPERATION_TYPE.DELETE_RECORDS,
page_id,
row_ids,
deleted_rows: deepCopy(rows_data),
original_deleted_rows: deepCopy(original_rows),
deleted_link_infos: deepCopy(link_infos),
upper_row_ids: deepCopy(upper_row_ids),
});
}
default: {
break;
}
}
}

View File

@@ -0,0 +1,37 @@
import deepCopy from 'deep-copy';
import { OPERATION_ATTRIBUTES } from './constants';
import apply from './apply';
import invert from './invert';
class Operation {
constructor(operation) {
const newOperation = deepCopy(operation);
const type = newOperation.type || newOperation.op_type;
const attributes = OPERATION_ATTRIBUTES[type];
this.op_type = type;
attributes.forEach((param) => {
this[param] = newOperation[param];
});
this.success_callback = newOperation.success_callback;
this.fail_callback = newOperation.fail_callback;
}
clone() {
return new Operation(this);
}
apply(pageData) {
return apply(pageData, this);
}
invert() {
return invert(this);
}
set(key, value) {
this[key] = value;
}
}
export default Operation;

View File

@@ -0,0 +1,241 @@
import { OPERATION_TYPE } from './operations';
import {
getColumnByKey
} from '../_basic';
const MAX_LOAD_RECORDS = 100;
class ServerOperator {
applyOperation(operation, callback) {
const { op_type } = operation;
switch (op_type) {
case OPERATION_TYPE.MODIFY_RECORD: {
const { repo_id, row_id, updates } = operation;
const rowsData = [{ row_id, row: updates }];
window.sfMetadataContext.updateRows(repo_id, rowsData).then(res => {
callback({ operation });
}).catch(error => {
callback({ error: 'Failed_to_modify_record' });
});
break;
}
case OPERATION_TYPE.MODIFY_RECORDS: {
const { repo_id, row_ids, id_row_updates, is_copy_paste } = operation;
const rowsData = row_ids.map(rowId => {
return { row_id: rowId, row: id_row_updates[rowId] };
});
window.sfMetadataContext.updateRows(repo_id, rowsData, is_copy_paste).then(res => {
callback({ operation });
}).catch(error => {
callback({ error: 'Failed_to_modify_record' });
});
break;
}
case OPERATION_TYPE.RESTORE_RECORDS: {
const { repo_id, rows_data } = operation;
if (!Array.isArray(rows_data) || rows_data.length === 0) {
callback({ error: 'Failed_to_insert_records' });
break;
}
window.sfMetadataContext.restoreRows(repo_id, rows_data).then(res => {
callback({ operation });
}).catch(error => {
callback({ error: 'Failed_to_insert_records' });
});
break;
}
case OPERATION_TYPE.RELOAD_RECORDS: {
callback({ operation });
break;
}
case OPERATION_TYPE.LOCK_RECORD_VIA_BUTTON: {
const { repo_id, row_id, button_column_key } = operation;
window.sfMetadataContext.lockRowViaButton(repo_id, row_id, button_column_key).then(res => {
callback({ operation });
}).catch(error => {
callback({ error: 'Failed_to_lock_row_via_button' });
});
break;
}
case OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON: {
const { repo_id, row_id, button_column_key, updates } = operation;
window.sfMetadataContext.updateRowViaButton(repo_id, row_id, button_column_key, updates).then(res => {
callback({ operation });
}).catch(error => {
callback({ error: 'Failed_to_modify_row_via_button' });
});
break;
}
default: {
break;
}
}
}
checkReloadRecordsOperation = (operation) => {
const { op_type } = operation;
switch (op_type) {
case OPERATION_TYPE.RELOAD_RECORDS: {
return true;
}
default: {
return false;
}
}
};
handleReloadRecords(operation, callback) {
const { repo_id: repoId } = operation;
const { relatedColumnKeyMap } = this.getOperationRelatedColumns(operation);
const isReloadRecordsOp = this.checkReloadRecordsOperation(operation);
if (!isReloadRecordsOp) return;
const rowsIds = this.getOperatedRowsIds(operation);
this.asyncReloadRecords(rowsIds, repoId, relatedColumnKeyMap, callback);
}
asyncReloadRecords(rowsIds, repoId, relatedColumnKeyMap, callback) {
if (!Array.isArray(rowsIds) || rowsIds.length === 0) return;
const restRowsIds = [...rowsIds];
const currentRowsIds = restRowsIds.splice(0, MAX_LOAD_RECORDS);
window.sfMetadataContext.getRowsByIds(repoId, currentRowsIds).then(res => {
if (!res || !res.data || !res.data.results) {
this.asyncReloadRecords(restRowsIds, repoId, relatedColumnKeyMap, callback);
return;
}
const fetchedRecords = res.data.results;
let reloadedRecords = [];
let idRecordLoadedMap = {};
let idRecordNotExistMap = {};
if (fetchedRecords.length > 0) {
fetchedRecords.forEach((record) => {
reloadedRecords.push(record);
idRecordLoadedMap[record._id] = true;
});
}
currentRowsIds.forEach((recordId) => {
if (!idRecordLoadedMap[recordId]) {
idRecordNotExistMap[recordId] = true;
}
});
callback({
reloadedRecords,
idRecordNotExistMap,
relatedColumnKeyMap,
});
this.asyncReloadRecords(restRowsIds, repoId, relatedColumnKeyMap, callback);
}).catch (error => {
// for debug
// eslint-disable-next-line no-console
console.log(error);
this.asyncReloadRecords(restRowsIds, repoId, relatedColumnKeyMap, callback);
});
}
getOperationRelatedColumns(table, operation) {
const { op_type } = operation;
let relatedColumnKeys;
switch (op_type) {
case OPERATION_TYPE.MODIFY_RECORDS: {
const { id_original_row_updates } = operation;
relatedColumnKeys = this.getRelatedColumnKeysFromRecordUpdates(id_original_row_updates);
break;
}
case OPERATION_TYPE.RELOAD_RECORDS: {
const { available_columns } = table.view;
let relatedColumnKeyMap = {};
available_columns.forEach(column => {
const { key } = column;
relatedColumnKeyMap[key] = true;
});
return {
relatedColumnKeyMap,
relatedColumns: available_columns,
};
}
case OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON: {
const { row_id, original_updates } = operation;
relatedColumnKeys = this.getRelatedColumnKeysFromRecordUpdates({ [row_id]: original_updates });
break;
}
default: {
relatedColumnKeys = [];
break;
}
}
return this.getRelatedColumns(relatedColumnKeys, table);
}
getOperatedRowsIds(operation) {
const { op_type } = operation;
switch (op_type) {
case OPERATION_TYPE.MODIFY_RECORDS:
case OPERATION_TYPE.RELOAD_RECORDS: {
const { row_ids } = operation;
return Array.isArray(row_ids) ? [...row_ids] : [];
}
case OPERATION_TYPE.MODIFY_RECORD_VIA_BUTTON: {
const { row_id } = operation;
return row_id ? [row_id] : [];
}
default: {
return [];
}
}
}
/**
* @param {array} relatedColumnKeys
* @param {object} pageData
* @param {object} table
* @returns relatedColumnKeyMap, relatedFormulaColumnKeyMap, relatedColumns, relatedFormulaColumns
*/
getRelatedColumns(relatedColumnKeys, table) {
if (!relatedColumnKeys || relatedColumnKeys.length === 0) {
return {
relatedColumnKeyMap: {},
relatedColumns: [],
};
}
let relatedColumnKeyMap = {};
let relatedColumns = [];
const { available_columns } = table.view;
relatedColumnKeys.forEach(columnKey => {
if (!relatedColumnKeyMap[columnKey]) {
const column = getColumnByKey(available_columns, columnKey);
if (column) {
relatedColumnKeyMap[columnKey] = true;
relatedColumns.push(column);
}
}
});
return {
relatedColumnKeyMap,
relatedColumns,
};
}
/**
* @param {object} recordUpdates: { [record._id]: { [column.key]: '', ... }, ... }
* @returns related column keys: [ column.key, ... ]
*/
getRelatedColumnKeysFromRecordUpdates(recordUpdates) {
if (!recordUpdates) return [];
const rowIds = Object.keys(recordUpdates);
return rowIds.reduce((keys, rowId) => {
const rowData = recordUpdates[rowId];
if (rowData) {
keys.push(...Object.keys(rowData));
}
return keys;
}, []);
}
}
export default ServerOperator;

View File

@@ -1,10 +1,15 @@
import {
CellType,
DEFAULT_DATE_FORMAT,
PRIVATE_COLUMN_KEY,
NOT_DISPLAY_COLUMN_KEYS,
} from '../_basic';
import {
SEQUENCE_COLUMN_WIDTH
} from '../constants';
import {
gettext
} from '../../../utils/constants';
export function getSelectColumnOptions(column) {
if (!column || !column.data || !Array.isArray(column.data.options)) {
@@ -24,13 +29,6 @@ export function isCheckboxColumn(column) {
return type === CellType.CHECKBOX;
}
export function getColumnByKey(columnKey, columns) {
if (!columnKey || !Array.isArray(columns)) {
return null;
}
return columns.find(column => column.key === columnKey);
}
export function getColumnByName(columnName, columns) {
if (!columnName || !Array.isArray(columns)) {
return null;
@@ -174,3 +172,83 @@ export const recalculate = (columns, allColumns, tableId) => {
allColumns: displayAllColumns,
};
};
export const getColumnName = (key, name) => {
switch (key) {
case PRIVATE_COLUMN_KEY.CTIME:
return gettext('Created time');
case PRIVATE_COLUMN_KEY.MTIME:
return gettext('Last modified time');
case PRIVATE_COLUMN_KEY.CREATOR:
return gettext('Creator');
case PRIVATE_COLUMN_KEY.LAST_MODIFIER:
return gettext('Last modifier');
case PRIVATE_COLUMN_KEY.FILE_CREATOR:
return gettext('File creator');
case PRIVATE_COLUMN_KEY.FILE_MODIFIER:
return gettext('File modifier');
case PRIVATE_COLUMN_KEY.FILE_CTIME:
return gettext('File created time');
case PRIVATE_COLUMN_KEY.FILE_MTIME:
return gettext('File last modified time');
case PRIVATE_COLUMN_KEY.IS_DIR:
return gettext('Is dir');
case PRIVATE_COLUMN_KEY.PARENT_DIR:
return gettext('Parent dir');
case PRIVATE_COLUMN_KEY.FILE_NAME:
return gettext('File name');
default:
return name;
}
};
const getColumnType = (key, type) => {
switch (key) {
case PRIVATE_COLUMN_KEY.CTIME:
case PRIVATE_COLUMN_KEY.FILE_CTIME:
return CellType.CTIME;
case PRIVATE_COLUMN_KEY.MTIME:
case PRIVATE_COLUMN_KEY.FILE_MTIME:
return CellType.MTIME;
case PRIVATE_COLUMN_KEY.CREATOR:
case PRIVATE_COLUMN_KEY.FILE_CREATOR:
return CellType.CREATOR;
case PRIVATE_COLUMN_KEY.LAST_MODIFIER:
case PRIVATE_COLUMN_KEY.FILE_MODIFIER:
return CellType.LAST_MODIFIER;
case PRIVATE_COLUMN_KEY.FILE_NAME:
return CellType.FILE_NAME;
default:
return type;
}
};
export const getColumns = (columns) => {
if (!Array.isArray(columns) || columns.length === 0) return [];
const validColumns = columns.map((column) => {
const { type, key, name, ...params } = column;
return {
key,
type: getColumnType(key, type),
name: getColumnName(key, name),
...params,
width: 200,
};
}).filter(column => !NOT_DISPLAY_COLUMN_KEYS.includes(column.key));
let displayColumns = [];
validColumns.forEach(column => {
if (column.key === '_name') {
displayColumns.unshift(column);
} else if (column.key === PRIVATE_COLUMN_KEY.PARENT_DIR) {
const nameColumnIndex = displayColumns.findIndex(column => column.key === PRIVATE_COLUMN_KEY.PARENT_DIR);
if (nameColumnIndex === -1) {
displayColumns.unshift(column);
} else {
displayColumns.splice(nameColumnIndex, 0, column);
}
} else {
displayColumns.push(column);
}
});
return displayColumns;
};

View File

@@ -1,70 +0,0 @@
import { gettext, lang } from '../../../utils/constants';
const zhCN = require('@seafile/seafile-calendar/lib/locale/zh_CN');
const zhTW = require('@seafile/seafile-calendar/lib/locale/zh_TW');
const enUS = require('@seafile/seafile-calendar/lib/locale/en_US');
const frFR = require('@seafile/seafile-calendar/lib/locale/fr_FR');
const deDE = require('@seafile/seafile-calendar/lib/locale/de_DE');
const esES = require('@seafile/seafile-calendar/lib/locale/es_ES');
const plPL = require('@seafile/seafile-calendar/lib/locale/pl_PL');
const csCZ = require('@seafile/seafile-calendar/lib/locale/cs_CZ');
const ruRU = require('@seafile/seafile-calendar/lib/locale/ru_RU');
function translateCalendar() {
const locale = lang ? lang : 'en';
let language;
switch (locale) {
case 'zh-cn':
language = zhCN;
break;
case 'zh-tw':
language = zhTW;
break;
case 'en':
language = enUS;
break;
case 'fr':
language = frFR;
break;
case 'de':
language = deDE;
break;
case 'es':
language = esES;
break;
case 'es-ar':
language = esES;
break;
case 'es-mx':
language = esES;
break;
case 'pl':
language = plPL;
break;
case 'cs':
language = csCZ;
break;
case 'ru':
language = ruRU;
break;
default:
language = enUS;
}
return language;
}
function getMobileDatePickerLocale() {
return {
DatePickerLocale: {
year: gettext('Year'),
month: gettext('Month'),
day: gettext('Day'),
hour: gettext('Hour'),
minute: gettext('Minute'),
},
okText: gettext('Done'),
dismissText: gettext('Cancel')
};
}
export { translateCalendar, getMobileDatePickerLocale };

View File

@@ -1,11 +1,11 @@
import { getColumnByKey } from './column-utils';
import { getColumnByKey } from '../_basic';
import { GROUP_HEADER_HEIGHT, GROUP_ROW_TYPE, GROUP_VIEW_OFFSET, INSERT_ROW_HEIGHT } from '../constants';
export const createGroupMetrics = (groups, groupbys, pathFoldedGroupMap, columns, rowHeight, includeInsertRow) => {
let groupbyColumnsMap = {};
groupbys.forEach(groupby => {
const columnKey = groupby.column_key;
const column = getColumnByKey(columnKey, columns);
const column = getColumnByKey(columns, columnKey);
groupbyColumnsMap[columnKey] = column;
});
const maxLevel = groupbys.length;