diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b42107cf42..44ce921614 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,7 +19,7 @@ "@seafile/sdoc-editor": "1.0.50", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "1.0.109", - "@seafile/sf-metadata-ui-component": "0.0.18", + "@seafile/sf-metadata-ui-component": "0.0.20", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/react-codemirror": "^4.19.4", "axios": "^1.7.3", @@ -5094,9 +5094,9 @@ } }, "node_modules/@seafile/sf-metadata-ui-component": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.18.tgz", - "integrity": "sha512-jC4JL06dAntWIr/ZPA/zJ4I9NhDfw0ismvSQuGZsBMvdrdbBBGuFVxirohB9zzwzc0k/K2epyQ/1iARKv5wjow==", + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.20.tgz", + "integrity": "sha512-nCeqHgwAkd05GKd3YROf3k7XEQuh6FicOeYVXcbCXjzH1pki8XxLcv1NW4D85LvWh2eS6m+utegcAlXfS1EsJQ==", "dependencies": { "@seafile/seafile-calendar": "0.0.24", "@seafile/seafile-editor": "~1.0.102", diff --git a/frontend/package.json b/frontend/package.json index f1faed46e6..557ab15121 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,7 @@ "@seafile/sdoc-editor": "1.0.50", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "1.0.109", - "@seafile/sf-metadata-ui-component": "0.0.18", + "@seafile/sf-metadata-ui-component": "0.0.20", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/react-codemirror": "^4.19.4", "axios": "^1.7.3", diff --git a/frontend/src/components/dirent-detail/detail-item/index.css b/frontend/src/components/dirent-detail/detail-item/index.css index aa5913e65b..21323101be 100644 --- a/frontend/src/components/dirent-detail/detail-item/index.css +++ b/frontend/src/components/dirent-detail/detail-item/index.css @@ -12,12 +12,12 @@ .dirent-detail-item .dirent-detail-item-name { width: 160px; - padding: 7px 6px; + padding: 6.5px 6px; min-height: 34px; height: fit-content; color: #666; font-size: 14px; - line-height: 1.4; + line-height: 1.5; } .dirent-detail-item .dirent-detail-item-name .sf-metadata-icon { @@ -29,13 +29,8 @@ .dirent-detail-item .dirent-detail-item-value { width: 200px; display: flex; - padding: 7px 6px; - min-height: 34px; height: fit-content; -} - -.dirent-detail-item .dirent-detail-item-value.editable:hover { - cursor: pointer; + min-height: 34px; } .dirent-detail-item .dirent-detail-item-name:hover, @@ -45,10 +40,48 @@ cursor: default; } +.dirent-detail-item .dirent-detail-item-value.editable:hover { + cursor: pointer; +} + +/* media */ +.cur-view-detail-small .dirent-detail-item .dirent-detail-item-name { + width: calc((100% - 8px) * 0.44); +} + +.cur-view-detail-small .dirent-detail-item .dirent-detail-item-value { + width: calc((100% - 8px) * 0.56); +} + +.cur-view-detail-large .dirent-detail-item .dirent-detail-item-name { + width: 160px; + margin-right: 8px; +} + +.cur-view-detail-large .dirent-detail-item .dirent-detail-item-value { + flex: 1; +} + +.dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty { + display: inline-block; + height: 34px; + width: 100%; + line-height: 1.5; + padding: 6.5px 6px; +} + +.dirent-detail-item .dirent-detail-item-value:not(.editable) .sf-metadata-record-cell-empty:empty::before { + content: attr(placeholder); + color: #666; + font-size: 14px; +} + +/* formatter */ .dirent-detail-item .dirent-detail-item-value .text-formatter, .dirent-detail-item .dirent-detail-item-value .ctime-formatter, .dirent-detail-item .dirent-detail-item-value .mtime-formatter, .dirent-detail-item .dirent-detail-item-value .date-formatter { + padding: 6.5px 6px; line-height: 1.5; } @@ -57,28 +90,5 @@ } .dirent-detail-item-value .creator-formatter { - height: 20px; -} - -.dirent-detail-item-value .sf-metadata-record-cell-empty::before { - content: attr(placeholder); - color: #666; - font-size: 14px; -} - -/* */ -.cur-view-detail-small .dirent-detail-item .dirent-detail-item-name { - width: 44%; -} - -.cur-view-detail-small .dirent-detail-item .dirent-detail-item-value { - width: 56%; -} - -.cur-view-detail-large .dirent-detail-item .dirent-detail-item-name { - width: 160px; -} - -.cur-view-detail-large .dirent-detail-item .dirent-detail-item-value { - flex: 1; + padding: 7px 6px; } diff --git a/frontend/src/components/dirent-detail/detail-item/index.js b/frontend/src/components/dirent-detail/detail-item/index.js index dcde4e76c7..332b4ca88e 100644 --- a/frontend/src/components/dirent-detail/detail-item/index.js +++ b/frontend/src/components/dirent-detail/detail-item/index.js @@ -1,40 +1,39 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; -import { Formatter, Icon } from '@seafile/sf-metadata-ui-component'; import classnames from 'classnames'; +import { Icon } from '@seafile/sf-metadata-ui-component'; import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic'; -import { gettext } from '../../../utils/constants'; import './index.css'; -const DetailItem = ({ field, value, valueId, valueClick, children, ...params }) => { +const DetailItem = ({ readonly, field, className, children }) => { const icon = useMemo(() => { if (field.type === 'size') return COLUMNS_ICON_CONFIG[CellType.NUMBER]; return COLUMNS_ICON_CONFIG[field.type]; }, [field]); return ( -
+
{field.name}
-
- {children ? children : ()} +
+ {children}
); }; DetailItem.defaultProps = { - emptyTip: gettext('Empty') + readonly: true, }; DetailItem.propTypes = { + readonly: PropTypes.bool, field: PropTypes.object.isRequired, - value: PropTypes.any, + className: PropTypes.string, children: PropTypes.any, - valueId: PropTypes.string, }; export default DetailItem; diff --git a/frontend/src/components/dirent-detail/dirent-details/dir-details.js b/frontend/src/components/dirent-detail/dirent-details/dir-details.js index b29554b34a..af390f7cad 100644 --- a/frontend/src/components/dirent-detail/dirent-details/dir-details.js +++ b/frontend/src/components/dirent-detail/dirent-details/dir-details.js @@ -1,20 +1,26 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; +import { Formatter } from '@seafile/sf-metadata-ui-component'; import { getDirentPath } from './utils'; import DetailItem from '../detail-item'; import { CellType } from '../../../metadata/metadata-view/_basic'; import { gettext } from '../../../utils/constants'; import { MetadataDetails, useMetadata } from '../../../metadata'; -const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail, ...params }) => { +const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail }) => { const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); const { enableMetadata } = useMetadata(); + const lastModifiedTimeField = useMemo(() => { + return { type: CellType.MTIME, name: gettext('Last modified time') }; + }, []); return ( <> - + + + {window.app.pageOptions.enableMetadataManagement && enableMetadata && ( - + )} ); diff --git a/frontend/src/components/dirent-detail/dirent-details/file-details.js b/frontend/src/components/dirent-detail/dirent-details/file-details.js index a075a2679f..b00e54072d 100644 --- a/frontend/src/components/dirent-detail/dirent-details/file-details.js +++ b/frontend/src/components/dirent-detail/dirent-details/file-details.js @@ -1,6 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { v4 as uuidV4 } from 'uuid'; +import { Formatter } from '@seafile/sf-metadata-ui-component'; import { getDirentPath } from './utils'; import DetailItem from '../detail-item'; import { CellType } from '../../../metadata/metadata-view/_basic'; @@ -11,12 +12,16 @@ import { Utils } from '../../../utils/utils'; import { MetadataDetails, useMetadata } from '../../../metadata'; import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils'; -const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList, ...params }) => { +const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => { const [isEditFileTagShow, setEditFileTagShow] = useState(false); const { enableMetadata } = useMetadata(); const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]); const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []); + const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []); + const lastModifierField = useMemo(() => ({ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }), []); + const lastModifiedTimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []); + const tagsField = useMemo(() => ({ type: CellType.SINGLE_SELECT, name: gettext('Tags') }), []); const onEditFileTagToggle = useCallback(() => { setEditFileTagShow(!isEditFileTagShow); @@ -28,25 +33,37 @@ const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, return ( <> - - - + + + + + + + + + {!window.app.pageOptions.enableMetadataManagement && enableMetadata && ( - - {Array.isArray(fileTagList) && fileTagList.length > 0 ? ( - - ) : ( - {gettext('Empty')} - )} + +
+ {Array.isArray(fileTagList) && fileTagList.length > 0 ? ( + + ) : ( + {gettext('Empty')} + )} +
)} {window.app.pageOptions.enableMetadataManagement && ( - + )} {isEditFileTagShow && { - const newCollaboratorsCache = { ...this.state.collaboratorsCache, [user.email]: user }; - this.setState({ collaboratorsCache: newCollaboratorsCache }); - }; - - loadCollaborators = () => { - metadataAPI.getCollaborators(this.props.repoID).then(res => { - const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : []; - this.setState({ collaborators }); - }).catch(error => { - this.setState({ collaborators: [] }); - }); - }; - updateDetail = (repoID, dirent, direntPath) => { const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo'; seafileAPI[apiName](repoID, direntPath).then(res => { @@ -73,7 +53,6 @@ class DirentDetails extends React.Component { }; componentDidMount() { - this.loadCollaborators(); this.loadDetail(this.props.repoID, this.props.dirent, this.props.path); } @@ -108,7 +87,7 @@ class DirentDetails extends React.Component { }; render() { - const { dirent, direntDetail, collaborators, collaboratorsCache } = this.state; + const { dirent, direntDetail } = this.state; const { repoID, path, fileTags } = this.props; const direntName = dirent?.name || ''; const smallIconUrl = Utils.getDirentIcon(dirent); @@ -127,10 +106,6 @@ class DirentDetails extends React.Component { dirent={dirent} direntDetail={direntDetail} path={this.props.dirent ? path + '/' + dirent.name : path} - collaborators={collaborators} - collaboratorsCache={collaboratorsCache} - updateCollaboratorsCache={this.updateCollaboratorsCache} - queryUserAPI={this.userService?.queryUser} /> ) : ( )}
diff --git a/frontend/src/components/dirent-detail/index.js b/frontend/src/components/dirent-detail/index.js index 5703fd0dbc..30a7ab442b 100644 --- a/frontend/src/components/dirent-detail/index.js +++ b/frontend/src/components/dirent-detail/index.js @@ -1,11 +1,24 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import LibDetail from './lib-details'; import DirentDetail from './dirent-details'; import ObjectUtils from '../../metadata/metadata-view/utils/object-utils'; +import { MetadataContext } from '../../metadata'; +import { mediaUrl } from '../../utils/constants'; const Index = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => { + useEffect(() => { + // init context + const context = new MetadataContext(); + window.sfMetadataContext = context; + window.sfMetadataContext.init({ repoID, mediaUrl, repoInfo: currentRepoInfo }); + return () => { + window.sfMetadataContext.destroy(); + delete window['sfMetadataContext']; + }; + }, [repoID, currentRepoInfo]); + if (path === '/' && !dirent) { return ( diff --git a/frontend/src/components/dirent-detail/lib-details/index.js b/frontend/src/components/dirent-detail/lib-details/index.js index 794b724759..1539197d93 100644 --- a/frontend/src/components/dirent-detail/lib-details/index.js +++ b/frontend/src/components/dirent-detail/lib-details/index.js @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; +import { Formatter } from '@seafile/sf-metadata-ui-component'; import { Utils } from '../../../utils/utils'; import { gettext } from '../../../utils/constants'; import { seafileAPI } from '../../../utils/seafile-api'; @@ -14,6 +15,10 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => { const [isLoading, setLoading] = useState(true); const [repo, setRepo] = useState({}); const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepoInfo), [currentRepoInfo]); + const filesField = useMemo(() => ({ type: CellType.NUMBER, name: gettext('Files') }), []); + const sizeField = useMemo(() => ({ type: 'size', name: gettext('Size') }), []); + const creatorField = useMemo(() => ({ type: CellType.CREATOR, name: gettext('Creator') }), []); + const mtimeField = useMemo(() => ({ type: CellType.MTIME, name: gettext('Last modified time') }), []); useEffect(() => { setLoading(true); @@ -35,15 +40,27 @@ const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
) : (
- - - - + + + + + + + + + + + +
)} diff --git a/frontend/src/metadata/metadata-view/hooks/collaborators.js b/frontend/src/metadata/hooks/collaborators.js similarity index 56% rename from frontend/src/metadata/metadata-view/hooks/collaborators.js rename to frontend/src/metadata/hooks/collaborators.js index 1aae3ca0b4..10126262ad 100644 --- a/frontend/src/metadata/metadata-view/hooks/collaborators.js +++ b/frontend/src/metadata/hooks/collaborators.js @@ -1,25 +1,39 @@ /* eslint-disable react/prop-types */ -import React, { useContext, useState, useCallback, useEffect } from 'react'; -import { useMetadata } from './metadata'; -import { mediaUrl } from '../../../utils/constants'; -import { isValidEmail } from '../_basic'; +import React, { useContext, useState, useCallback, useEffect, useMemo } from 'react'; +import { mediaUrl } from '../../utils/constants'; +import { isValidEmail, UserService } from '../metadata-view/_basic'; +import User from '../metadata-view/model/user'; +import metadataAPI from '../api'; const CollaboratorsContext = React.createContext(null); -export const CollaboratorsProvider = ({ - children, -}) => { +export const CollaboratorsProvider = ({ repoID, children }) => { const [collaboratorsCache, setCollaboratorsCache] = useState({}); const [collaborators, setCollaborators] = useState([]); - - const { store } = useMetadata(); + const queryUser = useMemo(() => { + const userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo }); + const queryUserAPI = userService.queryUser; + window.queryUser = queryUserAPI; + return queryUserAPI; + }, []); useEffect(() => { - setCollaborators(store?.collaborators || []); - }, [store?.collaborators]); + metadataAPI.getCollaborators(repoID).then(res => { + const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : []; + setCollaborators(collaborators); + }); + }, [repoID]); useEffect(() => { - if (!window.sfMetadata) return; + if (!window.sfMetadata) { + window.sfMetadata = {}; + window.sfMetadata.getCollaboratorsFromCache = () => { + return Object.values(window.sfMetadata.collaboratorsCache || {}) || []; + }; + window.sfMetadata.getCollaborators = () => { + return [...window.sfMetadata.collaborators, ...(Object.values(window.sfMetadata.collaboratorsCache || {}) || [])]; + }; + } window.sfMetadata.collaborators = collaborators; window.sfMetadata.collaboratorsCache = collaboratorsCache; }, [collaborators, collaboratorsCache]); @@ -54,7 +68,7 @@ export const CollaboratorsProvider = ({ }, [collaborators, collaboratorsCache]); return ( - + {children} ); @@ -65,6 +79,6 @@ export const useCollaborators = () => { if (!context) { throw new Error('\'CollaboratorsContext\' is null'); } - const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator } = context; - return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator }; + const { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser } = context; + return { collaborators, collaboratorsCache, updateCollaboratorsCache, getCollaborator, queryUser }; }; diff --git a/frontend/src/metadata/hooks/index.js b/frontend/src/metadata/hooks/index.js index ac4c71cb99..a00eced1d9 100644 --- a/frontend/src/metadata/hooks/index.js +++ b/frontend/src/metadata/hooks/index.js @@ -1 +1,2 @@ export { MetadataProvider, useMetadata } from './metadata'; +export { CollaboratorsProvider, useCollaborators } from './collaborators'; diff --git a/frontend/src/metadata/hooks/metadata.js b/frontend/src/metadata/hooks/metadata.js index 56ded1200e..2bdc0bcb4e 100644 --- a/frontend/src/metadata/hooks/metadata.js +++ b/frontend/src/metadata/hooks/metadata.js @@ -19,29 +19,46 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, const [navigation, setNavigation] = useState([]); const viewsMap = useRef({}); + const cancelURLView = useCallback(() => { + // If attribute extension is turned off, unmark the URL + const { origin, pathname, search } = window.location; + const urlParams = new URLSearchParams(search); + const viewID = urlParams.get('view'); + if (viewID) { + const url = `${origin}${pathname}`; + window.history.pushState({ url: url, path: '' }, '', url); + } + }, []); + useEffect(() => { if (!enableMetadataManagement) { - setEnableExtendedProperties(false); + cancelURLView(); return; } metadataAPI.getMetadataStatus(repoID).then(res => { - setEnableExtendedProperties(res.data.enabled); + const enableMetadata = res.data.enabled; + if (!enableMetadata) { + cancelURLView(); + } + setEnableExtendedProperties(enableMetadata); }).catch(error => { const errorMsg = Utils.getErrorMsg(error, true); toaster.danger(errorMsg); setEnableExtendedProperties(false); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [repoID, enableMetadataManagement]); const updateEnableMetadata = useCallback((newValue) => { if (newValue === enableMetadata) return; if (!newValue) { hideMetadataView && hideMetadataView(); + cancelURLView(); } else { setShowFirstView(true); } setEnableExtendedProperties(newValue); - }, [enableMetadata, hideMetadataView]); + }, [enableMetadata, hideMetadataView, cancelURLView]); // views useEffect(() => { @@ -61,14 +78,6 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, return; } - // If attribute extension is turned off, unmark the URL - const { origin, pathname, search } = window.location; - const urlParams = new URLSearchParams(search); - const viewID = urlParams.get('view'); - if (viewID) { - const url = `${origin}${pathname}`; - window.history.pushState({ url: url, path: '' }, '', url); - } viewsMap.current = {}; setNavigation([]); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/metadata/index.js b/frontend/src/metadata/index.js index 4ad4264f27..b81a66da34 100644 --- a/frontend/src/metadata/index.js +++ b/frontend/src/metadata/index.js @@ -1,4 +1,4 @@ -import SeafileMetadata from './metadata-view'; +import SeafileMetadata, { Context as MetadataContext } from './metadata-view'; import MetadataStatusManagementDialog from './metadata-status-manage-dialog'; import MetadataTreeView from './metadata-tree-view'; import MetadataDetails from './metadata-details'; @@ -8,6 +8,7 @@ export * from './hooks'; export { metadataAPI, + MetadataContext, SeafileMetadata, MetadataStatusManagementDialog, MetadataTreeView, diff --git a/frontend/src/metadata/metadata-details/index.js b/frontend/src/metadata/metadata-details/index.js index 857269bbc3..25ea7b4072 100644 --- a/frontend/src/metadata/metadata-details/index.js +++ b/frontend/src/metadata/metadata-details/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { Utils } from '../../utils/utils'; import metadataAPI from '../api'; @@ -6,12 +6,17 @@ import Column from '../metadata-view/model/metadata/column'; import { normalizeFields, getCellValueByColumn } from './utils'; import DetailItem from '../../components/dirent-detail/detail-item'; import toaster from '../../components/toast'; +import { gettext } from '../../utils/constants'; +import { DetailEditor, CellFormatter } from '../metadata-view'; +import { getColumnOriginName } from '../metadata-view/utils/column-utils'; +import { CellType, getColumnOptions, getOptionName, PREDEFINED_COLUMN_KEYS } from '../metadata-view/_basic'; import './index.css'; -const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params }) => { +const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, emptyTip }) => { const [isLoading, setLoading] = useState(true); const [metadata, setMetadata] = useState({ record: {}, fields: [] }); + const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]); useEffect(() => { setLoading(true); @@ -34,18 +39,77 @@ const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params }) }); }, [repoID, filePath, direntType]); + const onChange = useCallback((fieldKey, newValue) => { + const { record, fields } = metadata; + const field = fields.find(f => f.key === fieldKey); + const fileName = getColumnOriginName(field); + let update = { [fileName]: newValue }; + if (!PREDEFINED_COLUMN_KEYS.includes(field.key) && field.type === CellType.SINGLE_SELECT) { + const options = getColumnOptions(field); + update = { [fileName]: getOptionName(options, newValue) }; + } + metadataAPI.modifyRecord(repoID, record._id, update).then(res => { + const newMetadata = { ...metadata, record: { ...record, ...update } }; + setMetadata(newMetadata); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, [repoID, metadata]); + + const modifyColumnData = useCallback((fieldKey, newData) => { + const { fields, record } = metadata; + let newFields = fields.slice(0); + let update; + metadataAPI.modifyColumnData(repoID, fieldKey, newData).then(res => { + const newField = new Column(res.data.column); + const fieldIndex = fields.findIndex(f => f.key === fieldKey); + newFields[fieldIndex] = newField; + return newField; + }).then((newField) => { + const fileName = getColumnOriginName(newField); + const options = getColumnOptions(newField); + const newOption = options[options.length - 1]; + update = { [fileName]: newOption.id }; + if (!PREDEFINED_COLUMN_KEYS.includes(fieldKey) && newField.type === CellType.SINGLE_SELECT) { + update = { [fileName]: getOptionName(options, newOption.id) }; + } + return metadataAPI.modifyRecord(repoID, record._id, update); + }).then(res => { + const newMetadata = { ...metadata, record: { ...record, ...update }, fields: newFields }; + setMetadata(newMetadata); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, [repoID, metadata]); + if (isLoading) return null; const { fields, record } = metadata; if (!record._id) return null; - return fields.map(field => { - const value = getCellValueByColumn(record, field); - return (); - }); + return ( + <> + {fields.map(field => { + const canEdit = permission === 'rw' && field.editable; + const value = getCellValueByColumn(record, field); + return ( + + {canEdit ? ( + + ) : ( + + )} + + ); + })} + + ); }; MetadataDetails.propTypes = { repoID: PropTypes.string, filePath: PropTypes.string, + repoInfo: PropTypes.object, direntType: PropTypes.string, direntDetail: PropTypes.object, }; diff --git a/frontend/src/metadata/metadata-view/_basic/constants/column/private.js b/frontend/src/metadata/metadata-view/_basic/constants/column/private.js index be882e5fe0..e1bbaa8e97 100644 --- a/frontend/src/metadata/metadata-view/_basic/constants/column/private.js +++ b/frontend/src/metadata/metadata-view/_basic/constants/column/private.js @@ -51,7 +51,7 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.FILE_COLLABORATORS, PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME, PRIVATE_COLUMN_KEY.FILE_KEYWORDS, - PRIVATE_COLUMN_KEY.FILE_SUMMARY, + // PRIVATE_COLUMN_KEY.FILE_SUMMARY, PRIVATE_COLUMN_KEY.FILE_EXPIRED, PRIVATE_COLUMN_KEY.FILE_STATUS, ]; diff --git a/frontend/src/metadata/metadata-view/_basic/index.js b/frontend/src/metadata/metadata-view/_basic/index.js index f0fb590992..0615bb5a61 100644 --- a/frontend/src/metadata/metadata-view/_basic/index.js +++ b/frontend/src/metadata/metadata-view/_basic/index.js @@ -150,4 +150,6 @@ export { isRegExpression, getGeolocationDisplayString, getGeolocationByGranularity, + getFloatNumber, + isNumber, } from './utils'; diff --git a/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js b/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js index 56cd156f9f..a246a4784f 100644 --- a/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js +++ b/frontend/src/metadata/metadata-view/_basic/utils/cell/column/index.js @@ -5,6 +5,8 @@ export { replaceNumberNotAllowInput, formatStringToNumber, formatTextToNumber, + getFloatNumber, + isNumber, } from './number'; export { getOption, diff --git a/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js b/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js index 05e5177a70..944a358046 100644 --- a/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js +++ b/frontend/src/metadata/metadata-view/_basic/utils/cell/index.js @@ -21,4 +21,6 @@ export { getLongtextDisplayString, getGeolocationDisplayString, getGeolocationByGranularity, + getFloatNumber, + isNumber, } from './column'; diff --git a/frontend/src/metadata/metadata-view/_basic/utils/index.js b/frontend/src/metadata/metadata-view/_basic/utils/index.js index 01e91659ae..0ec1ae15bc 100644 --- a/frontend/src/metadata/metadata-view/_basic/utils/index.js +++ b/frontend/src/metadata/metadata-view/_basic/utils/index.js @@ -21,6 +21,8 @@ export { getLongtextDisplayString, getGeolocationDisplayString, getGeolocationByGranularity, + getFloatNumber, + isNumber, } from './cell'; export { getColumnType, diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css index 26ca8f6877..ec9e9304dd 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css +++ b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.css @@ -45,3 +45,9 @@ fill: #666; font-size: 12px; } + +.sf-metadata-delete-collaborator .collaborator .collaborator-remove { + height: 14px; + width: 14px; + margin-left: 2px; +} diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.js index 9928b8651d..80def5c3fa 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.js +++ b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/delete-collaborator/index.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { IconBtn } from '@seafile/sf-metadata-ui-component'; -import { useCollaborators } from '../../../../hooks'; +import { useCollaborators } from '../../../../../hooks'; import './index.css'; @@ -20,7 +20,7 @@ const DeleteCollaborator = ({ value, onDelete }) => { {name} {name} - onDelete(email)} iconName="x-01" /> + onDelete(email, event)} iconName="x-01" />
); })} diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/index.js index 8fa8fe8bf5..1c368df847 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/index.js +++ b/frontend/src/metadata/metadata-view/components/cell-editor/collaborator-editor/index.js @@ -5,12 +5,13 @@ import { SearchInput, Icon } from '@seafile/sf-metadata-ui-component'; import { isFunction } from '../../../_basic'; import { KeyCodes } from '../../../../../constants'; import { gettext } from '../../../../../utils/constants'; -import { useCollaborators } from '../../../hooks'; +import { useCollaborators } from '../../../../hooks'; import DeleteCollaborator from './delete-collaborator'; import './index.css'; const CollaboratorEditor = forwardRef(({ + saveImmediately = false, column, value: oldValue, onCommit, @@ -22,7 +23,6 @@ const CollaboratorEditor = forwardRef(({ const [highlightIndex, setHighlightIndex] = useState(-1); const [maxItemNum, setMaxItemNum] = useState(0); const [itemHeight, setItemHeight] = useState(0); - const timerRef = useRef(null); const editorContainerRef = useRef(null); const editorRef = useRef(null); const collaboratorItemRef = useRef(null); @@ -70,7 +70,10 @@ const CollaboratorEditor = forwardRef(({ newValue.push(email); } setValue(newValue); - }, [value]); + if (saveImmediately) { + onCommit && onCommit(newValue); + } + }, [saveImmediately, value, onCommit]); const onDeleteCollaborator = useCallback((email) => { const newValue = value.slice(0); @@ -79,7 +82,10 @@ const CollaboratorEditor = forwardRef(({ newValue.splice(collaboratorIndex, 1); } setValue(newValue); - }, [value]); + if (saveImmediately) { + onCommit && onCommit(newValue); + } + }, [saveImmediately, value, onCommit]); const onMenuMouseEnter = useCallback((highlightIndex) => { setHighlightIndex(highlightIndex); @@ -182,8 +188,6 @@ const CollaboratorEditor = forwardRef(({ document.addEventListener('keydown', onHotKey, true); return () => { document.removeEventListener('keydown', onHotKey, true); - timerRef.current && clearTimeout(timerRef.current); - timerRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [onHotKey]); @@ -214,7 +218,7 @@ const CollaboratorEditor = forwardRef(({
onSelectCollaborator(isSelected ? null : collaborator.email)} + onMouseDown={() => onSelectCollaborator(collaborator.email)} onMouseEnter={() => onMenuMouseEnter(i)} onMouseLeave={() => onMenuMouseLeave(i)} > @@ -238,7 +242,7 @@ const CollaboratorEditor = forwardRef(({
- +
{renderCollaborators()} @@ -248,6 +252,7 @@ const CollaboratorEditor = forwardRef(({ }); CollaboratorEditor.propTypes = { + saveImmediately: PropTypes.bool, column: PropTypes.object, value: PropTypes.array, onCommit: PropTypes.func, diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/normal-editor-container.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/normal-editor-container.js index 12dded65bb..2cf284e144 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/normal-editor-container.js +++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/normal-editor-container.js @@ -8,7 +8,7 @@ import { isCellValueChanged } from '../../../utils/cell-comparer'; import { EVENT_BUS_TYPE } from '../../../constants'; import { getEventClassName } from '../../../utils'; import Editor from '../editor'; -import { canEdit } from '../../../utils/column-utils'; +import { canEditCell } from '../../../utils/column-utils'; class NormalEditorContainer extends React.Component { @@ -83,7 +83,7 @@ class NormalEditorContainer extends React.Component { const { column, record, openEditorMode, columns, modifyColumnData } = this.props; const editorProps = { ref: this.setEditorRef, - readOnly: !canEdit(column, record, true), + readOnly: !canEditCell(column, record, true), columns, column: this.props.column, value: this.getInitialValue(), diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js index 137d5169e8..9a5ef665ed 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js +++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container/popup-editor-container.js @@ -6,7 +6,7 @@ import { CellType, isFunction, Z_INDEX, getCellValueByColumn, getColumnOptionNam import { isCellValueChanged } from '../../../utils/cell-comparer'; import { EVENT_BUS_TYPE } from '../../../constants'; import Editor from '../editor'; -import { canEdit } from '../../../utils/column-utils'; +import { canEditCell } from '../../../utils/column-utils'; const NOT_SUPPORT_EDITOR_COLUMN_TYPES = [ CellType.CTIME, CellType.MTIME, CellType.CREATOR, CellType.LAST_MODIFIER, @@ -58,7 +58,7 @@ class PopupEditorContainer extends React.Component { createEditor = () => { const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData } = this.props; - const readOnly = canEdit(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type); + const readOnly = canEditCell(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type); const value = this.getInitialValue(readOnly); let editorProps = { diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.css b/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.css index 32ddbfad7f..ae54085dc5 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.css +++ b/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.css @@ -63,6 +63,7 @@ flex: 1; overflow: hidden; text-overflow: ellipsis; + margin-right: 10px; } .single-select-avatar { @@ -82,12 +83,13 @@ padding: 0px 10px; height: 20px; line-height: 20px; - text-align: center; margin-left: 0; border-radius: 10px; overflow: hidden; text-overflow: ellipsis; margin-top: 5px; + white-space: nowrap; + max-width: 100%; } .sf-metadata-single-select-editor .single-select-check-icon { diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.js index c043919027..c4e025bf43 100644 --- a/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.js +++ b/frontend/src/metadata/metadata-view/components/cell-editor/single-select-editor/index.js @@ -2,8 +2,8 @@ import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState, import PropTypes from 'prop-types'; import classnames from 'classnames'; import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component'; -import { getCellValueByColumn, getColumnByKey, isFunction } from '../../../_basic'; -import { generateNewOption, getSelectColumnOptions } from '../../../utils/select-utils'; +import { getCellValueByColumn, getColumnByKey, isFunction, getColumnOptions } from '../../../_basic'; +import { generateNewOption } from '../../../utils/select-utils'; import { KeyCodes } from '../../../../../constants'; import { gettext } from '../../../../../utils/constants'; @@ -23,15 +23,14 @@ const SingleSelectEditor = forwardRef(({ const [searchValue, setSearchValue] = useState(''); const [highlightIndex, setHighlightIndex] = useState(-1); const [maxItemNum, setMaxItemNum] = useState(0); - const [itemHeight, setItemHeight] = useState(0); - const timerRef = useRef(null); + const itemHeight = 30; const editorContainerRef = useRef(null); const editorRef = useRef(null); const selectItemRef = useRef(null); const canEditData = window.sfMetadataContext.canModifyColumnData(column); const options = useMemo(() => { - const options = getSelectColumnOptions(column); + const options = getColumnOptions(column); const { data } = column; const { cascade_column_key, cascade_settings } = data || {}; if (cascade_column_key) { @@ -64,8 +63,8 @@ const SingleSelectEditor = forwardRef(({ }, [column, height]); const blur = useCallback(() => { - onCommit && onCommit(); - }, [onCommit]); + onCommit && onCommit(value); + }, [value, onCommit]); const onChangeSearch = useCallback((newSearchValue) => { if (searchValue === newSearchValue) return; @@ -76,7 +75,7 @@ const SingleSelectEditor = forwardRef(({ if (optionId === value) return; setValue(optionId); setTimeout(() => { - onCommit && onCommit(); + onCommit && onCommit(optionId); }, 1); }, [value, onCommit]); @@ -89,6 +88,7 @@ const SingleSelectEditor = forwardRef(({ }, []); const createOption = useCallback((event) => { + event && event.stopPropagation(); event && event.nativeEvent.stopImmediatePropagation(); const newOption = generateNewOption(options, searchValue?.trim() || ''); let newOptions = options.slice(0); @@ -180,13 +180,10 @@ const SingleSelectEditor = forwardRef(({ } if (editorContainerRef.current && selectItemRef.current) { setMaxItemNum(getMaxItemNum()); - setItemHeight(parseInt(getComputedStyle(selectItemRef.current, null).height)); } document.addEventListener('keydown', onHotKey, true); return () => { document.removeEventListener('keydown', onHotKey, true); - timerRef.current && clearTimeout(timerRef.current); - timerRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [onHotKey]); @@ -211,12 +208,8 @@ const SingleSelectEditor = forwardRef(({ return ({noOptionsTip}); } - // maxWidth = single-selects-container's width - single-selects-container's padding-left and padding-right - single-select-container's padding-left - single-select-check-icon's width - The gap between the single-select-check-icon and single-select-name or scroll's width - // maxWidth = column.width > 200 ? column.width - 20 - 12 - 20 - 10 : 200 - 20 - 12 - 20 - 10 - // maxWidth = column.width > 200 ? column.width - 62 : 200 - 62 - const maxWidth = column.width > 200 ? column.width - 62 : 200 - 62; return displayOptions.map((option, i) => { - const isSelected = value === option.name; + const isSelected = value === option.id || value === option.name; return (
@@ -243,7 +236,7 @@ const SingleSelectEditor = forwardRef(({ ); }); - }, [displayOptions, searchValue, column, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]); + }, [displayOptions, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectOption]); return (
@@ -253,6 +246,7 @@ const SingleSelectEditor = forwardRef(({ onKeyDown={onKeyDown} onChange={onChangeSearch} autoFocus={true} + className="sf-metadata-search-options" />
@@ -280,4 +274,3 @@ SingleSelectEditor.propTypes = { }; export default SingleSelectEditor; - diff --git a/frontend/src/metadata/metadata-view/components/cell-formatter/index.js b/frontend/src/metadata/metadata-view/components/cell-formatter/index.js index 2f8f205726..706d0c676d 100644 --- a/frontend/src/metadata/metadata-view/components/cell-formatter/index.js +++ b/frontend/src/metadata/metadata-view/components/cell-formatter/index.js @@ -1,11 +1,11 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { Formatter } from '@seafile/sf-metadata-ui-component'; -import { useCollaborators } from '../../hooks'; +import { useCollaborators } from '../../../hooks'; import { Utils } from '../../../../utils/utils'; const CellFormatter = ({ readonly, value, field, ...params }) => { - const { collaborators, collaboratorsCache, updateCollaboratorsCache } = useCollaborators(); + const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators(); const props = useMemo(() => { return { collaborators, @@ -14,11 +14,11 @@ const CellFormatter = ({ readonly, value, field, ...params }) => { readonly, value, field, - queryUserAPI: window.sfMetadataContext.userService.queryUser, + queryUserAPI: queryUser, getFileIconUrl: Utils.getFileIconUrl, getFolderIconUrl: Utils.getFolderIconUrl, }; - }, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache]); + }, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser]); return ( diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/checkbox-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/checkbox-editor/index.css new file mode 100644 index 0000000000..72fca47172 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/checkbox-editor/index.css @@ -0,0 +1,22 @@ +.sf-metadata-checkbox-property-detail-editor { + height: 100%; + width: 100%; + padding: 7px 6px; + cursor: pointer; +} + +.sf-metadata-checkbox-property-detail-editor .sf-metadata-checkbox-property-detail-editor-content { + height: 20px; + width: 20px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #e0e0e0; + border-radius: 3px; + cursor: pointer; +} + +.sf-metadata-checkbox-property-detail-editor .sf-metadata-checkbox-property-detail-editor-content .sf-metadata-icon-check-mark { + fill: #20c933; +} diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/checkbox-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/checkbox-editor/index.js new file mode 100644 index 0000000000..ba3d4cb157 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/checkbox-editor/index.js @@ -0,0 +1,28 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { Icon } from '@seafile/sf-metadata-ui-component'; + +import './index.css'; + +const CheckboxEditor = ({ value, onChange: onChangeAPI }) => { + + const onChange = useCallback((event) => { + event && event.stopPropagation(); + onChangeAPI(!value); + }, [value, onChangeAPI]); + + return ( +
+
+ {value && ()} +
+
+ ); +}; + +CheckboxEditor.propTypes = { + value: PropTypes.bool, + onChange: PropTypes.func, +}; + +export default CheckboxEditor; diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/collaborator-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/collaborator-editor/index.css new file mode 100644 index 0000000000..b69786a6a7 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/collaborator-editor/index.css @@ -0,0 +1,36 @@ +.sf-metadata-collaborator-property-detail-editor { + min-height: 34px; + width: 100%; +} + +.sf-metadata-collaborator-property-detail-editor .sf-metadata-delete-collaborator { + border-bottom: none; + background-color: inherit; + border-radius: unset; + padding: 2px 6px; +} + +.sf-metadata-collaborator-property-detail-editor .sf-metadata-delete-collaborator .collaborator { + margin: 5px 10px 5px 0; +} + +.sf-metadata-collaborator-property-editor-popover .popover { + max-width: unset; +} + +.sf-metadata-collaborator-property-editor-popover .sf-metadata-collaborator-editor { + width: 100%; + position: unset; + min-width: 200px; + padding: 0; + overflow: hidden; + opacity: 1; + background-color: #ffffff; + border: none; + border-radius: unset; + box-shadow: none; +} + +.sf-metadata-collaborator-property-editor-popover .sf-metadata-delete-collaborator { + display: none; +} diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/collaborator-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/collaborator-editor/index.js new file mode 100644 index 0000000000..09d31d55c9 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/collaborator-editor/index.js @@ -0,0 +1,102 @@ +import React, { useCallback, useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Popover } from 'reactstrap'; +import { KeyCodes } from '../../../_basic'; +import { getEventClassName, gettext } from '../../../utils'; +import Editor from '../../cell-editor/collaborator-editor'; +import DeleteCollaborator from '../../cell-editor/collaborator-editor/delete-collaborator'; + +import './index.css'; + +const CollaboratorEditor = ({ field, value, onChange }) => { + const ref = useRef(null); + const [showEditor, setShowEditor] = useState(false); + + const onClick = useCallback((event) => { + if (!showEditor) return; + if (!event.target) return; + const className = getEventClassName(event); + if (className.indexOf('sf-metadata-search-collaborators') > -1) return; + const editor = document.querySelector('.sf-metadata-collaborator-editor'); + if ((editor && editor.contains(event.target)) || ref.current.contains(event.target)) return; + setShowEditor(false); + }, [showEditor]); + + const onHotKey = useCallback((event) => { + if (event.keyCode === KeyCodes.Esc) { + if (showEditor) { + setShowEditor(false); + } + } + }, [showEditor]); + + useEffect(() => { + document.addEventListener('mousedown', onClick); + document.addEventListener('keydown', onHotKey, true); + return () => { + document.removeEventListener('mousedown', onClick); + document.removeEventListener('keydown', onHotKey, true); + }; + }, [onClick, onHotKey]); + + const openEditor = useCallback(() => { + setShowEditor(true); + }, []); + + const onCommit = useCallback((newValue) => { + onChange(newValue); + }, [onChange]); + + const deleteCollaborator = useCallback((email, event) => { + event && event.stopPropagation(); + event && event.nativeEvent && event.nativeEvent.stopImmediatePropagation(); + const newValue = value.filter(c => c !== email); + onChange(newValue); + }, [value, onChange]); + + const renderEditor = useCallback(() => { + if (!showEditor) return null; + const { width } = ref.current.getBoundingClientRect(); + return ( + + + + ); + }, [showEditor, onCommit, value, field]); + + + return ( +
+ {} + {renderEditor()} +
+ ); +}; + +CollaboratorEditor.propTypes = { + field: PropTypes.object.isRequired, + value: PropTypes.array, + onChange: PropTypes.func.isRequired, +}; + +export default CollaboratorEditor; diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/date-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/date-editor/index.css new file mode 100644 index 0000000000..4e9e84a247 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/date-editor/index.css @@ -0,0 +1,9 @@ +.sf-metadata-date-property-detail-editor { + width: 100%; + height: 34px; + padding: 6.5px 6px; +} + +.dirent-detail-item-value.editable .ant-calendar-picker-input { + display: none; +} diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/date-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/date-editor/index.js new file mode 100644 index 0000000000..5f2892f89c --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/date-editor/index.js @@ -0,0 +1,54 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { SfCalendar } from '@seafile/sf-metadata-ui-component'; +import PropTypes from 'prop-types'; +import { DEFAULT_DATE_FORMAT, getDateDisplayString } from '../../../_basic'; +import { gettext } from '../../../utils'; + +import './index.css'; + +const DateEditor = ({ value, field, onChange: onChangeAPI, lang }) => { + const [showEditor, setShowEditor] = useState(false); + const format = useMemo(() => field?.data?.format || DEFAULT_DATE_FORMAT, [field]); + + const openEditor = useCallback(() => { + setShowEditor(true); + }, []); + + const onChange = useCallback((newValue) => { + onChangeAPI(newValue); + }, [onChangeAPI]); + + const onClear = useCallback(() => { + onChangeAPI(null); + setShowEditor(false); + }, [onChangeAPI]); + + const closeEditor = useCallback(() => { + setShowEditor(false); + }, []); + + return ( + <> +
+ {getDateDisplayString(value, format)} +
+ {showEditor && ( + + )} + + + ); +}; + +DateEditor.propTypes = { + value: PropTypes.string, + field: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + lang: PropTypes.string, +}; + +export default DateEditor; diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/index.css new file mode 100644 index 0000000000..4b73dbf12e --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/index.css @@ -0,0 +1,43 @@ +.sf-metadata-property-editor-popover .popover[x-placement^="bottom"] { + margin-top: 0; +} + +.sf-metadata-property-editor-popover .popover[x-placement^="top"] { + margin-bottom: 0; +} + +.sf-metadata-property-editor-popover .sf-metadata-property-editor-popover-container { + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; +} + +.sf-metadata-property-editor-popover .add-search-result { + height: 30px; + padding: 0 10px; + overflow: hidden; + color: #666; +} + +.sf-metadata-property-editor-popover .add-search-result:hover{ + cursor: pointer; + background-color: #f5f5f5; +} + +.sf-metadata-property-editor-popover .add-search-result .add-new-option { + display: inline-block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sf-metadata-property-detail-editor { + border-radius: inherit; +} + +.sf-metadata-property-detail-editor:empty::before { + content: attr(placeholder); + color: #666; + font-size: 14px; +} diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/index.js new file mode 100644 index 0000000000..b24c4f6b60 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/index.js @@ -0,0 +1,51 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { CellType } from '../../_basic'; +import CheckboxEditor from './checkbox-editor'; +import TextEditor from './text-editor'; +import NumberEditor from './number-editor'; +import SingleSelectEditor from './single-select-editor'; +import CollaboratorEditor from './collaborator-editor'; +import DateEditor from './date-editor'; +import { lang } from '../../../../utils/constants'; + +import './index.css'; + +const DetailEditor = ({ field, onChange: onChangeAPI, ...props }) => { + const onChange = useCallback((newValue) => { + onChangeAPI(field.key, newValue); + }, [field, onChangeAPI]); + + + switch (field.type) { + case CellType.CHECKBOX: { + return (); + } + case CellType.TEXT: { + return (); + } + case CellType.NUMBER: { + return (); + } + case CellType.DATE: { + return (); + } + case CellType.SINGLE_SELECT: { + return (); + } + case CellType.COLLABORATOR: { + return (); + } + default: { + return null; + } + } + + +}; + +DetailEditor.propTypes = { + field: PropTypes.object.isRequired, +}; + +export default DetailEditor; diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/number-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/number-editor/index.css new file mode 100644 index 0000000000..85a42f5a41 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/number-editor/index.css @@ -0,0 +1,24 @@ +.sf-metadata-number-property-detail-editor { + min-height: 34px; + height: 34px; + padding: 6.5px 6px; + line-height: 1.5; + border: none !important; + background: inherit; +} + +.sf-metadata-number-property-detail-editor::placeholder { + color: #666; + font-size: 14px; +} + +.sf-metadata-number-property-detail-editor:hover { + cursor: pointer; +} + +.sf-metadata-number-property-detail-editor:focus { + background-color: #fff; + box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px; + border-radius: 3px; + cursor: auto; +} \ No newline at end of file diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/number-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/number-editor/index.js new file mode 100644 index 0000000000..58708eb759 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/number-editor/index.js @@ -0,0 +1,100 @@ +import React, { useCallback, useRef, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { KeyCodes } from '../../../../../constants'; +import { isCellValueChanged } from '../../../utils/cell-comparer'; +import { gettext } from '../../../utils'; +import ObjectUtils from '../../../utils/object-utils'; +import { getNumberDisplayString, DEFAULT_NUMBER_FORMAT, formatStringToNumber, + replaceNumberNotAllowInput, isMac, +} from '../../../_basic'; + +import './index.css'; + +const NumberEditor = React.memo(({ value: oldValue, field, onChange: onChangeAPI }) => { + const [value, setValue] = useState(''); + const ref = useRef(null); + + useEffect(() => { + const validValue = oldValue || oldValue === 0 ? oldValue : ''; + const data = field?.data || {}; + const value = getNumberDisplayString(validValue, data) || ''; + setValue(value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [oldValue, field]); + + const valueChange = useCallback((event) => { + const format = field?.data?.format || DEFAULT_NUMBER_FORMAT; + const currency_symbol = format === 'custom_currency' ? field.data['currency_symbol'] : null; + const initValue = event.target.value.trim(); + // Prevent the repetition of periods bug in the Chinese input method of the Windows system + if (!isMac() && initValue.indexOf('.。') > -1) return; + const newValue = replaceNumberNotAllowInput(initValue, format, currency_symbol); + if (newValue === value) return; + setValue(newValue); + }, [field, value]); + + const onBlur = useCallback(() => { + const newValue = formatStringToNumber(value, field.data); + if (newValue === oldValue) return; + onChangeAPI(newValue); + }, [oldValue, value, field, onChangeAPI]); + + const onPaste = useCallback((event) => { + event.stopPropagation(); + }, []); + + const onCut = useCallback((event) => { + event.stopPropagation(); + }, []); + + const onKeyDown = useCallback((event) => { + const { selectionStart, selectionEnd, value } = event.currentTarget; + if (event.keyCode === KeyCodes.Enter) { + event.preventDefault(); + ref.current && ref.current.blur(); + } else if ( + (event.keyCode === KeyCodes.ChineseInputMethod) || + (event.keyCode === KeyCodes.LeftArrow && selectionStart === 0) || + (event.keyCode === KeyCodes.RightArrow && selectionEnd === value.length) + ) { + event.stopPropagation(); + } + }, []); + + const onCompositionEnd = useCallback((event) => { + valueChange(event); + }, [valueChange]); + + return ( + + ); + + +}, (props, nextProps) => { + const isChanged = isCellValueChanged(props.value, nextProps.value, nextProps.field.type) || + !ObjectUtils.isSameObject(props.field, nextProps.field); + return !isChanged; +}); + +NumberEditor.propTypes = { + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + field: PropTypes.object.isRequired, + onChange: PropTypes.func, +}; + +export default NumberEditor; diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/single-select-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/single-select-editor/index.css new file mode 100644 index 0000000000..4d0551d43a --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/single-select-editor/index.css @@ -0,0 +1,40 @@ +.sf-metadata-single-select-property-detail-editor { + height: 34px; + width: 100%; + overflow: hidden; + display: flex; + align-items: center; + padding: 0 6px; + position: relative; +} + +.sf-metadata-single-select-property-detail-editor .sf-metadata-single-select-property-value { + border-radius: 10px; + font-size: 13px; + line-height: 20px; + margin: 6px 0; + max-width: 250px; + overflow: hidden; + padding: 0 10px; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + width: -webkit-min-content; + width: min-content; +} + +.sf-metadata-single-select-property-editor-popover .popover { + max-width: unset; +} + +.sf-metadata-single-select-property-editor-popover .sf-metadata-single-select-editor { + position: unset; + min-width: 200px; + padding: 0; + overflow: hidden; + opacity: 1; + background-color: #ffffff; + border: none; + border-radius: none; + box-shadow: none; +} diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/single-select-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/single-select-editor/index.js new file mode 100644 index 0000000000..3ef036e173 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/single-select-editor/index.js @@ -0,0 +1,111 @@ +import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Popover } from 'reactstrap'; +import { getColumnOptions, getOption, KeyCodes } from '../../../_basic'; +import { getEventClassName, gettext } from '../../../utils'; +import Editor from '../../cell-editor/single-select-editor'; + +import './index.css'; + +const SingleSelectEditor = ({ field, value, record, fields, onChange, modifyColumnData }) => { + const ref = useRef(null); + const [showEditor, setShowEditor] = useState(false); + const options = useMemo(() => getColumnOptions(field), [field]); + + const onClick = useCallback((event) => { + if (!event.target) return; + const className = getEventClassName(event); + if (className.indexOf('sf-metadata-search-options') > -1) return; + const dom = document.querySelector('.sf-metadata-single-select-editor'); + if (!dom) return; + if (dom.contains(event.target)) return; + if (ref.current && !ref.current.contains(event.target) && showEditor) { + setShowEditor(false); + } + }, [showEditor]); + + const onHotKey = useCallback((event) => { + if (event.keyCode === KeyCodes.Esc) { + if (showEditor) { + setShowEditor(false); + } + } + }, [showEditor]); + + useEffect(() => { + document.addEventListener('mousedown', onClick); + document.addEventListener('keydown', onHotKey, true); + return () => { + document.removeEventListener('mousedown', onClick); + document.removeEventListener('keydown', onHotKey, true); + }; + }, [onClick, onHotKey]); + + const openEditor = useCallback(() => { + setShowEditor(true); + }, []); + + const onCommit = useCallback((newValue) => { + if (newValue && !getOption(options, newValue)) { + setShowEditor(false); + return; + } + onChange(newValue); + setShowEditor(false); + }, [options, onChange]); + + const option = value ? getOption(options, value) : null; + + const renderEditor = useCallback(() => { + if (!showEditor) return null; + const { width } = ref.current.getBoundingClientRect(); + return ( + + + + ); + }, [showEditor, onCommit, record, value, modifyColumnData, fields, field]); + + return ( +
+ {option && ( +
+ {option.name} +
+ )} + {renderEditor()} +
+ ); +}; + +SingleSelectEditor.propTypes = { + field: PropTypes.object.isRequired, + value: PropTypes.string, + onChange: PropTypes.func.isRequired, +}; + +export default SingleSelectEditor; diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/text-editor/index.css b/frontend/src/metadata/metadata-view/components/detail-editor/text-editor/index.css new file mode 100644 index 0000000000..153a983c12 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/text-editor/index.css @@ -0,0 +1,17 @@ +.sf-metadata-text-property-detail-editor { + padding: 6.5px 6px; + min-height: 34px; + width: 100%; + border: none !important; + outline: none !important; +} + +.sf-metadata-text-property-detail-editor.formatter { + background-color: inherit; +} + +.sf-metadata-text-property-detail-editor:not(.formatter) { + background-color: #fff; + box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px; + border-radius: 3px; +} diff --git a/frontend/src/metadata/metadata-view/components/detail-editor/text-editor/index.js b/frontend/src/metadata/metadata-view/components/detail-editor/text-editor/index.js new file mode 100644 index 0000000000..1bf3d53259 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/detail-editor/text-editor/index.js @@ -0,0 +1,90 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { ClickOutside } from '@seafile/sf-metadata-ui-component'; +import { KeyCodes } from '../../../../../constants'; +import { isCellValueChanged } from '../../../utils/cell-comparer'; +import { gettext, getTrimmedString } from '../../../utils'; +import ObjectUtils from '../../../utils/object-utils'; + +import './index.css'; + +const TextEditor = React.memo(({ value: oldValue, onChange: onChangeAPI }) => { + const [showEditor, setShowEditor] = useState(false); + const ref = useRef(null); + + useEffect(() => { + ref.current.innerText = oldValue || ''; + }, [oldValue]); + + const closeEditor = useCallback(() => { + const value = ref.current.innerText; + if (value !== oldValue) { + onChangeAPI(getTrimmedString(value) || null); + } + setShowEditor(false); + }, [oldValue, onChangeAPI]); + + const onPaste = useCallback((event) => { + event.stopPropagation(); + }, []); + + const onCut = useCallback((event) => { + event.stopPropagation(); + }, []); + + const onKeyDown = useCallback((event) => { + const { selectionStart, selectionEnd, value } = event.currentTarget; + if (event.keyCode === KeyCodes.Enter) { + event.preventDefault(); + closeEditor(); + } else if ( + (event.keyCode === KeyCodes.ChineseInputMethod) || + (event.keyCode === KeyCodes.LeftArrow && selectionStart === 0) || + (event.keyCode === KeyCodes.RightArrow && selectionEnd === value.length) + ) { + event.stopPropagation(); + } + }, [closeEditor]); + + const displayEditor = useCallback(() => { + if (showEditor) return; + setShowEditor(true); + setTimeout(() => { + ref.current.focus(); + const range = document.createRange(); + range.selectNodeContents(ref.current); + range.collapse(false); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }, 1); + }, [showEditor]); + + return ( + +
+ + ); +}, (props, nextProps) => { + const isChanged = isCellValueChanged(props.value, nextProps.value, nextProps.field.type) || + !ObjectUtils.isSameObject(props.field, nextProps.field); + return !isChanged; +}); + +TextEditor.propTypes = { + value: PropTypes.string, + field: PropTypes.object.isRequired, + onChange: PropTypes.func, +}; + +export default TextEditor; diff --git a/frontend/src/metadata/metadata-view/components/index.js b/frontend/src/metadata/metadata-view/components/index.js index b2e6502991..546822c0b0 100644 --- a/frontend/src/metadata/metadata-view/components/index.js +++ b/frontend/src/metadata/metadata-view/components/index.js @@ -1,7 +1,11 @@ import DeleteConfirmDialog from './delete-confirm-dialog'; import Table from './table'; +import DetailEditor from './detail-editor'; +import CellFormatter from './cell-formatter'; export { DeleteConfirmDialog, Table, + DetailEditor, + CellFormatter, }; diff --git a/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js b/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js index 929a5351fa..4b9f667a26 100644 --- a/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js +++ b/frontend/src/metadata/metadata-view/components/popover/filter-popover/widgets/filter-item/index.js @@ -9,8 +9,8 @@ import { filterTermModifierIsWithin, isDateColumn, FILTER_ERR_MSG, + getColumnOptions as getSelectColumnOptions, } from '../../../../../_basic'; -import { getSelectColumnOptions } from '../../../../../utils/select-utils'; import CollaboratorFilter from './collaborator-filter'; import FilterCalendar from '../filter-calendar'; import FilterItemUtils from '../filter-item-utils'; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.css b/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.css index cd89dd5c69..937140523a 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.css +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.css @@ -5,7 +5,7 @@ text-overflow: ellipsis; white-space: nowrap; border-right: 1px solid #eee; - padding: 4px 8px; + padding: 6px 8px; display: flex; justify-content: flex-start; } @@ -71,7 +71,7 @@ /* cell formatter */ .sf-metadata-result-table-cell .sf-metadata-ui.cell-formatter-container { height: 100%; - line-height: 24px; + line-height: 20px; font-size: 14px; } @@ -303,3 +303,17 @@ padding-top: 5.5px; padding-bottom: 5.5px; } + +/* file name */ +.sf-metadata-result-table-cell.sf-metadata-result-table-file-name-cell { + padding-top: 4px; + padding-bottom: 4px; +} + +.sf-metadata-result-table-cell.sf-metadata-result-table-file-name-cell .sf-metadata-ui.cell-formatter-container { + line-height: 24px; +} + +.sf-metadata-result-table-file-name-cell .sf-metadata-ui.file-name-formatter .sf-metadata-file-icon { + transform: translateY(-1px); +} diff --git a/frontend/src/metadata/metadata-view/context.js b/frontend/src/metadata/metadata-view/context.js index da44c8dfee..561e6b3493 100644 --- a/frontend/src/metadata/metadata-view/context.js +++ b/frontend/src/metadata/metadata-view/context.js @@ -1,9 +1,8 @@ import metadataAPI from '../api'; -import { UserService, LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS, +import { LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS, EDITABLE_PRIVATE_COLUMN_KEYS, PREDEFINED_COLUMN_KEYS } from './_basic'; import EventBus from '../../components/common/event-bus'; import { username } from '../../utils/constants'; -import User from './model/user'; class Context { @@ -11,29 +10,26 @@ class Context { this.settings = {}; this.metadataAPI = null; this.localStorage = null; - this.userService = null; this.eventBus = null; this.hasInit = false; this.permission = 'r'; this.collaboratorsCache = {}; } - async init({ otherSettings }) { + async init(settings) { if (this.hasInit) return; // init settings - this.settings = otherSettings || {}; + this.settings = settings || {}; // init metadataAPI - const { mediaUrl, repoInfo } = this.settings; + const { repoInfo } = this.settings; this.metadataAPI = metadataAPI; // init localStorage const { repoID, viewID } = this.settings; - this.localStorage = new LocalStorage(`sf-metadata-${repoID}-${viewID}`); - - // init userService - this.userService = new UserService({ mediaUrl, api: this.metadataAPI.listUserInfo }); + const localStorageName = viewID ? `sf-metadata-${repoID}-${viewID}` : `sf-metadata-${repoID}`; + this.localStorage = new LocalStorage(localStorageName); const eventBus = new EventBus(); this.eventBus = eventBus; @@ -47,7 +43,6 @@ class Context { this.settings = {}; this.metadataAPI = null; this.localStorage = null; - this.userService = null; this.eventBus = null; this.hasInit = false; this.permission = 'r'; @@ -135,23 +130,6 @@ class Context { return true; }; - getCollaboratorFromCache(email) { - return this.collaboratorsCache[email]; - } - - getCollaboratorsFromCache() { - const collaboratorsCache = this.collaboratorsCache; - return Object.values(collaboratorsCache).filter(item => item.email !== 'anonymous'); - } - - updateCollaboratorsCache(email, collaborator) { - if (collaborator instanceof User) { - this.collaboratorsCache[email] = collaborator; - return; - } - this.collaboratorsCache[email] = new User(collaborator); - } - restoreRows = () => { // todo }; diff --git a/frontend/src/metadata/metadata-view/hooks/index.js b/frontend/src/metadata/metadata-view/hooks/index.js index aaa50f5ec2..b9c8c3d37e 100644 --- a/frontend/src/metadata/metadata-view/hooks/index.js +++ b/frontend/src/metadata/metadata-view/hooks/index.js @@ -1,7 +1,5 @@ -import { CollaboratorsProvider, useCollaborators } from './collaborators'; import { MetadataProvider, useMetadata } from './metadata'; export { - CollaboratorsProvider, useCollaborators, MetadataProvider, useMetadata, }; diff --git a/frontend/src/metadata/metadata-view/hooks/metadata.js b/frontend/src/metadata/metadata-view/hooks/metadata.js index 18e05a37ea..3f757c4f9e 100644 --- a/frontend/src/metadata/metadata-view/hooks/metadata.js +++ b/frontend/src/metadata/metadata-view/hooks/metadata.js @@ -47,9 +47,8 @@ export const MetadataProvider = ({ setLoading(true); // init context const context = new Context(); - window.sfMetadata = {}; window.sfMetadataContext = context; - window.sfMetadataContext.init({ otherSettings: { ...params, repoID, viewID } }); + window.sfMetadataContext.init({ ...params, repoID, viewID }); storeRef.current = new Store({ context: window.sfMetadataContext, repoId: repoID, viewId: viewID }); window.sfMetadataStore = storeRef.current; storeRef.current.initStartIndex(); @@ -68,7 +67,6 @@ export const MetadataProvider = ({ const unsubscribeReloadData = eventBus.subscribe(EVENT_BUS_TYPE.RELOAD_DATA, reloadMetadata); return () => { - window.sfMetadata = {}; window.sfMetadataContext.destroy(); window.sfMetadataStore.destroy(); unsubscribeServerTableChanged(); diff --git a/frontend/src/metadata/metadata-view/index.js b/frontend/src/metadata/metadata-view/index.js index 8fd08bfc5d..4c74b932c1 100644 --- a/frontend/src/metadata/metadata-view/index.js +++ b/frontend/src/metadata/metadata-view/index.js @@ -1,15 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { MetadataProvider, CollaboratorsProvider } from './hooks/index'; -import { Table } from './components/index'; +import { MetadataProvider } from './hooks'; +import { Table, DetailEditor, CellFormatter } from './components'; +import Context from './context'; const SeafileMetadata = ({ ...params }) => { - return ( - - - +
); }; @@ -21,3 +19,8 @@ SeafileMetadata.propTypes = { }; export default SeafileMetadata; +export { + Context, + DetailEditor, + CellFormatter, +}; diff --git a/frontend/src/metadata/metadata-view/utils/column-utils.js b/frontend/src/metadata/metadata-view/utils/column-utils.js index 8617c6e558..a765c44d98 100644 --- a/frontend/src/metadata/metadata-view/utils/column-utils.js +++ b/frontend/src/metadata/metadata-view/utils/column-utils.js @@ -327,7 +327,7 @@ export const normalizeColumns = (columns) => { return displayColumns; }; -export function canEdit(col, record, enableCellSelect) { +export function canEditCell(col, record, enableCellSelect) { if (!col) return false; if (window.sfMetadataContext.canModifyColumn(col) === false) return false; if (col.editable != null && typeof (col.editable) === 'function') { diff --git a/frontend/src/metadata/metadata-view/utils/convert-utils.js b/frontend/src/metadata/metadata-view/utils/convert-utils.js index ff9084c6bc..d887c85aa9 100644 --- a/frontend/src/metadata/metadata-view/utils/convert-utils.js +++ b/frontend/src/metadata/metadata-view/utils/convert-utils.js @@ -1,6 +1,7 @@ -import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString, PREDEFINED_COLUMN_KEYS } from '../_basic'; +import { CellType, DEFAULT_DATE_FORMAT, generatorCellOption, getCollaboratorsName, getOptionName, getDateDisplayString, + PREDEFINED_COLUMN_KEYS, getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber, getColumnOptions, +} from '../_basic'; import { formatTextToDate } from './date'; -import { getFloatNumber, getNumberDisplayString, formatStringToNumber, isNumber } from '../_basic/utils/cell/column/number'; const SUPPORT_PASTE_FROM_COLUMN = { [CellType.NUMBER]: [CellType.TEXT, CellType.NUMBER], @@ -8,13 +9,6 @@ const SUPPORT_PASTE_FROM_COLUMN = { const reg_chinese_date_format = /(\d{4})年(\d{1,2})月(\d{1,2})日$/; -export function getSelectColumnOptions(column) { - if (!column || !column.data || !Array.isArray(column.data.options)) { - return []; - } - return column.data.options; -} - function convertCellValue(cellValue, oldCellValue, targetColumn, fromColumn) { const { type: fromColumnType, data: fromColumnData } = fromColumn; const { type: targetColumnType, data: targetColumnData } = targetColumn; @@ -140,7 +134,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn) let fromOptionName; switch (fromColumnType) { case CellType.SINGLE_SELECT: { - const fromOptions = getSelectColumnOptions(fromColumn); + const fromOptions = getColumnOptions(fromColumn); fromOptionName = getOptionName(fromOptions, cellValue) || ''; break; } @@ -156,7 +150,7 @@ function convert2SingleSelect(cellValue, oldCellValue, fromColumn, targetColumn) return oldCellValue; } - const currentOptions = getSelectColumnOptions(targetColumn); + const currentOptions = getColumnOptions(targetColumn); const newOption = generatorCellOption(currentOptions, fromOptionName); return PREDEFINED_COLUMN_KEYS.includes(targetColumn.key) ? newOption.id : newOption.name; } @@ -230,11 +224,11 @@ function convert2Text(cellValue, oldCellValue, fromColumn) { return getDateDisplayString(cellValue, fromColumnData.format || DEFAULT_DATE_FORMAT); } case CellType.SINGLE_SELECT: { - const options = getSelectColumnOptions(fromColumn); + const options = getColumnOptions(fromColumn); return getOptionName(options, cellValue) || null; } case CellType.COLLABORATOR: { - const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache(); + const collaborators = window.sfMetadata.getCollaborators(); return getCollaboratorsName(collaborators, cellValue); } case CellType.CREATOR: @@ -242,7 +236,7 @@ function convert2Text(cellValue, oldCellValue, fromColumn) { if (!cellValue) { return null; } - const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache(); + const collaborators = window.sfMetadata.getCollaborators(); return getCollaboratorsName(collaborators, [cellValue]); } default: { @@ -257,7 +251,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) { if (!Array.isArray(cellValue) || cellValue.length === 0) { return null; } - const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache(); + const collaborators = window.sfMetadata.getCollaborators(); let validEmailMap = {}; collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true); return cellValue.filter(email => !!validEmailMap[email]); @@ -270,7 +264,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) { if (userNames.length === 0) { return oldCellValue; } - const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache(); + const collaborators = window.sfMetadata.getCollaborators(); let nameCollaboratorMap = {}; collaborators.forEach(collaborator => nameCollaboratorMap[collaborator.name] = collaborator); const emails = userNames.map(name => { @@ -284,7 +278,7 @@ function convert2Collaborator(cellValue, oldCellValue, fromColumnType) { } case CellType.CREATOR: case CellType.LAST_MODIFIER: { - const collaborators = window.sfMetadata.collaborators || window.sfMetadataContext.getCollaboratorsFromCache(); + const collaborators = window.sfMetadata.getCollaborators(); let validEmailMap = {}; collaborators.forEach(collaborator => validEmailMap[collaborator.email] = true); if (!cellValue || !validEmailMap[cellValue]) { diff --git a/frontend/src/metadata/metadata-view/utils/select-utils.js b/frontend/src/metadata/metadata-view/utils/select-utils.js index 513a366b73..4a002d4068 100644 --- a/frontend/src/metadata/metadata-view/utils/select-utils.js +++ b/frontend/src/metadata/metadata-view/utils/select-utils.js @@ -1,13 +1,6 @@ import { SELECT_OPTION_COLORS } from '../_basic/constants/select-option'; import { generateOptionID } from '../_basic/utils/column/option'; -export function getSelectColumnOptions(column) { - if (!column || !column.data || !Array.isArray(column.data.options)) { - return []; - } - return column.data.options; -} - const getNotDuplicateOption = (options) => { const defaultOptions = SELECT_OPTION_COLORS.slice(12, 24); let defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)]; diff --git a/frontend/src/metadata/metadata-view/utils/selected-cell-utils.js b/frontend/src/metadata/metadata-view/utils/selected-cell-utils.js index 640c481628..a6211c21d3 100644 --- a/frontend/src/metadata/metadata-view/utils/selected-cell-utils.js +++ b/frontend/src/metadata/metadata-view/utils/selected-cell-utils.js @@ -1,5 +1,5 @@ import { Z_INDEX, getGroupByPath, isFunction, getCellValueByColumn } from '../_basic'; -import { getColumnByIndex, canEdit } from './column-utils'; +import { getColumnByIndex, canEditCell } from './column-utils'; import { SUPPORT_PREVIEW_COLUMN_TYPES } from '../constants'; import { getGroupRecordByIndex } from './group-metrics'; @@ -44,7 +44,7 @@ export const isSelectedCellEditable = ({ enableCellSelect, selectedPosition, col const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex }); if (!window.sfMetadataContext.canModifyRow(row)) return false; const isCellEditable = isFunction(onCheckCellIsEditable) ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true; - return isCellEditable && canEdit(column, row, enableCellSelect); + return isCellEditable && canEditCell(column, row, enableCellSelect); }; export function selectedRangeIsSingleCell(selectedRange) { diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index d4f63ae3af..655bdddf02 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -24,7 +24,7 @@ import CopyMoveDirentProgressDialog from '../../components/dialog/copy-move-dire import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog'; import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type'; import { PRIVATE_FILE_TYPE } from '../../constants'; -import { MetadataProvider } from '../../metadata/hooks'; +import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks'; const propTypes = { eventBus: PropTypes.object, @@ -2174,123 +2174,125 @@ class LibContentView extends React.Component { renameMetadataView={this.renameMetadataView} hideMetadataView={this.hideFileMetadata} > -
- - {canUpload && this.state.pathExist && !this.state.isViewFile && ( - this.uploader = uploader} - dragAndDrop={true} - path={this.state.path} - repoID={this.props.repoID} - direntList={this.state.direntList} - onFileUploadSuccess={this.onFileUploadSuccess} + +
+ + {canUpload && this.state.pathExist && !this.state.isViewFile && ( + this.uploader = uploader} + dragAndDrop={true} + path={this.state.path} + repoID={this.props.repoID} + direntList={this.state.direntList} + onFileUploadSuccess={this.onFileUploadSuccess} + isCustomPermission={isCustomPermission} + /> + )} +
+ {isCopyMoveProgressDialogShow && ( + )} -
- {isCopyMoveProgressDialogShow && ( - - )} - {isDeleteFolderDialogOpen && ( - - )} - - - + {isDeleteFolderDialogOpen && ( + + )} + + + + ); }