feat: metadata view part (#6289)
* feat: metadata view part * feat: optimize code --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local>
14
frontend/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
1
frontend/src/assets/icons/check-mark.svg
Normal 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 |
@@ -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 |
1
frontend/src/assets/icons/description.svg
Normal 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 |
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 483 B |
@@ -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 |
@@ -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) {
|
||||
|
@@ -41,6 +41,9 @@ const FILTER_COLUMN_OPTIONS = {
|
||||
[CellType.TEXT]: {
|
||||
filterPredicateList: textPredicates,
|
||||
},
|
||||
[CellType.FILE_NAME]: {
|
||||
filterPredicateList: textPredicates,
|
||||
},
|
||||
[CellType.CTIME]: {
|
||||
filterPredicateList: datePredicates,
|
||||
filterTermModifierList: dateTermModifiers,
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -6,6 +6,7 @@ const SORT_TYPE = {
|
||||
};
|
||||
|
||||
const SORT_COLUMN_OPTIONS = [
|
||||
CellType.FILE_NAME,
|
||||
CellType.CTIME,
|
||||
CellType.MTIME,
|
||||
CellType.TEXT,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
getColumnType,
|
||||
getColumnsByType,
|
||||
getColumnByKey,
|
||||
} from './core';
|
||||
export {
|
||||
isDateColumn,
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -17,4 +17,5 @@ export {
|
||||
export {
|
||||
filterRow,
|
||||
filterRows,
|
||||
getFilteredRows,
|
||||
} from './filter-row';
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -6,5 +6,5 @@ export {
|
||||
|
||||
export {
|
||||
groupTableRows,
|
||||
groupViewRows,
|
||||
getGroupRows,
|
||||
} from './group-row';
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -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">
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
})
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
@@ -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,
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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 && (
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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 }}>
|
||||
|
@@ -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 };
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
14
frontend/src/metadata/metadata-view/model/metadata/view.js
Normal 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;
|
247
frontend/src/metadata/metadata-view/store/data-processor.js
Normal 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;
|
354
frontend/src/metadata/metadata-view/store/index.js
Normal 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;
|
125
frontend/src/metadata/metadata-view/store/operations/apply.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
];
|
@@ -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,
|
||||
};
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
241
frontend/src/metadata/metadata-view/store/server-operator.js
Normal 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;
|
@@ -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;
|
||||
};
|
||||
|
@@ -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 };
|
@@ -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;
|
||||
|