mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 23:20:51 +00:00
feat: detail support tag (#7119)
* feat: detail support tag * feat: optimize code * feat: optimize code * feat: optimize code --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@@ -19,7 +19,7 @@
|
|||||||
"@seafile/sdoc-editor": "1.0.155",
|
"@seafile/sdoc-editor": "1.0.155",
|
||||||
"@seafile/seafile-calendar": "0.0.28",
|
"@seafile/seafile-calendar": "0.0.28",
|
||||||
"@seafile/seafile-editor": "1.0.126",
|
"@seafile/seafile-editor": "1.0.126",
|
||||||
"@seafile/sf-metadata-ui-component": "^0.0.53",
|
"@seafile/sf-metadata-ui-component": "^0.0.56",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
||||||
"@uiw/codemirror-themes": "^4.23.5",
|
"@uiw/codemirror-themes": "^4.23.5",
|
||||||
"@uiw/react-codemirror": "^4.19.4",
|
"@uiw/react-codemirror": "^4.19.4",
|
||||||
@@ -5024,9 +5024,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@seafile/sf-metadata-ui-component": {
|
"node_modules/@seafile/sf-metadata-ui-component": {
|
||||||
"version": "0.0.53",
|
"version": "0.0.56",
|
||||||
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.53.tgz",
|
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.56.tgz",
|
||||||
"integrity": "sha512-RzogKUvy0RkziPMkdjizPOfrMFFtkaZUaqvZT3RYggrZsPcJ68rbak2tI+4kIH7MUOQB4s43y5ygku0oxK7OEg==",
|
"integrity": "sha512-sw3mbgUtXjEeqmi03azqTWzAaoW12CkT5FvB+bYa2wC1Re0o3t1kTFzqHVd/2NckOzHR2Qe9HFnQ8A0+5FOqGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@seafile/seafile-calendar": "0.0.28",
|
"@seafile/seafile-calendar": "0.0.28",
|
||||||
"@seafile/seafile-editor": "^1.0.122",
|
"@seafile/seafile-editor": "^1.0.122",
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
"@seafile/sdoc-editor": "1.0.155",
|
"@seafile/sdoc-editor": "1.0.155",
|
||||||
"@seafile/seafile-calendar": "0.0.28",
|
"@seafile/seafile-calendar": "0.0.28",
|
||||||
"@seafile/seafile-editor": "1.0.126",
|
"@seafile/seafile-editor": "1.0.126",
|
||||||
"@seafile/sf-metadata-ui-component": "^0.0.53",
|
"@seafile/sf-metadata-ui-component": "^0.0.56",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
"@uiw/codemirror-extensions-langs": "^4.19.4",
|
||||||
"@uiw/codemirror-themes": "^4.23.5",
|
"@uiw/codemirror-themes": "^4.23.5",
|
||||||
"@uiw/react-codemirror": "^4.19.4",
|
"@uiw/react-codemirror": "^4.19.4",
|
||||||
|
@@ -27,6 +27,11 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon-tag {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.dirent-detail-item .dirent-detail-item-value {
|
.dirent-detail-item .dirent-detail-item-value {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -35,7 +35,7 @@ const FileDetails = ({ repoID, repoInfo, path, direntDetail }) => {
|
|||||||
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
|
<DetailItem field={lastModifiedTimeField} className="sf-metadata-property-detail-formatter">
|
||||||
<Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/>
|
<Formatter field={lastModifiedTimeField} value={direntDetail.last_modified}/>
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
{window.app.pageOptions.enableMetadataManagement && enableMetadata && (
|
{enableMetadata && (
|
||||||
<MetadataDetails repoID={repoID} filePath={path} repoInfo={repoInfo} direntType="file" />
|
<MetadataDetails repoID={repoID} filePath={path} repoInfo={repoInfo} direntType="file" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@@ -14,6 +14,7 @@ import OnlyofficeFileToolbar from './onlyoffice-file-toolbar';
|
|||||||
import EmbeddedFileDetails from '../dirent-detail/embedded-file-details';
|
import EmbeddedFileDetails from '../dirent-detail/embedded-file-details';
|
||||||
import { MetadataStatusProvider } from '../../hooks';
|
import { MetadataStatusProvider } from '../../hooks';
|
||||||
import { CollaboratorsProvider } from '../../metadata';
|
import { CollaboratorsProvider } from '../../metadata';
|
||||||
|
import { TagsProvider } from '../../tag/hooks';
|
||||||
import Loading from '../loading';
|
import Loading from '../loading';
|
||||||
|
|
||||||
import '../../css/file-view.css';
|
import '../../css/file-view.css';
|
||||||
@@ -153,13 +154,15 @@ class FileView extends React.Component {
|
|||||||
{isDetailsPanelOpen && (
|
{isDetailsPanelOpen && (
|
||||||
<MetadataStatusProvider repoID={repoID} >
|
<MetadataStatusProvider repoID={repoID} >
|
||||||
<CollaboratorsProvider repoID={repoID}>
|
<CollaboratorsProvider repoID={repoID}>
|
||||||
<EmbeddedFileDetails
|
<TagsProvider repoID={repoID} repoInfo={{ permission: filePerm }}>
|
||||||
repoID={repoID}
|
<EmbeddedFileDetails
|
||||||
path={filePath}
|
repoID={repoID}
|
||||||
dirent={{ 'name': fileName, type: 'file' }}
|
path={filePath}
|
||||||
repoInfo={{ permission: filePerm }}
|
dirent={{ 'name': fileName, type: 'file' }}
|
||||||
onClose={this.toggleDetailsPanel}
|
repoInfo={{ permission: filePerm }}
|
||||||
/>
|
onClose={this.toggleDetailsPanel}
|
||||||
|
/>
|
||||||
|
</TagsProvider>
|
||||||
</CollaboratorsProvider>
|
</CollaboratorsProvider>
|
||||||
</MetadataStatusProvider>
|
</MetadataStatusProvider>
|
||||||
)}
|
)}
|
||||||
|
@@ -5,6 +5,8 @@
|
|||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-delete-select-tags .sf-metadata-delete-select-tag {
|
.sf-metadata-delete-select-tags .sf-metadata-delete-select-tag {
|
||||||
|
@@ -28,6 +28,12 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .none-search-result {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@@ -24,7 +24,9 @@ const TagsEditor = forwardRef(({
|
|||||||
onPressTab,
|
onPressTab,
|
||||||
updateFileTags,
|
updateFileTags,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { tagsData, addTag } = useTags();
|
const { tagsData, addTag, context } = useTags();
|
||||||
|
|
||||||
|
const canAddTag = context.canAddTag();
|
||||||
|
|
||||||
const [value, setValue] = useState((oldValue || []).map(item => item.row_id).filter(item => getRowById(tagsData, item)));
|
const [value, setValue] = useState((oldValue || []).map(item => item.row_id).filter(item => getRowById(tagsData, item)));
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
@@ -44,9 +46,10 @@ const TagsEditor = forwardRef(({
|
|||||||
const displayTags = useMemo(() => getTagsByNameOrColor(tags, searchValue), [searchValue, tags]);
|
const displayTags = useMemo(() => getTagsByNameOrColor(tags, searchValue), [searchValue, tags]);
|
||||||
|
|
||||||
const isShowCreateBtn = useMemo(() => {
|
const isShowCreateBtn = useMemo(() => {
|
||||||
|
if (!canAddTag) return false;
|
||||||
if (!canEditData || !searchValue) return false;
|
if (!canEditData || !searchValue) return false;
|
||||||
return !getTagByNameOrColor(displayTags, searchValue);
|
return !getTagByNameOrColor(displayTags, searchValue);
|
||||||
}, [canEditData, displayTags, searchValue]);
|
}, [canEditData, displayTags, searchValue, canAddTag]);
|
||||||
|
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
return { width: column.width };
|
return { width: column.width };
|
||||||
|
@@ -1,45 +0,0 @@
|
|||||||
.sf-metadata-tags-formatter .sf-metadata-tags-operation-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-formatter .sf-metadata-tags-operation-container .sf-metadata-tags-operation-add-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background: #eceff4;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-formatter .sf-metadata-tags-operation-container .sf-metadata-tags-operation-add-btn:hover {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-formatter .sf-metadata-tags-operation-add-btn .sf-metadata-icon-add-table {
|
|
||||||
font-size: 12px;
|
|
||||||
fill: #212519;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-formatter .sf-metadata-tags-formatter-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: max-content;
|
|
||||||
min-height: 1rem;
|
|
||||||
position: relative;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-formatter .sf-metadata-tags-formatter-container .sf-metadata-tag-formatter {
|
|
||||||
margin-right: -0.5rem;
|
|
||||||
border: 0.125rem solid #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
display: inline-block;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
@@ -1,39 +0,0 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTags } from '../../../../tag/hooks';
|
|
||||||
import { getRowById } from '../../../utils/table';
|
|
||||||
import { getTagColor, getTagName } from '../../../../tag/utils/cell/core';
|
|
||||||
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const FileTagsFormatter = ({ value: oldValue }) => {
|
|
||||||
const { tagsData } = useTags();
|
|
||||||
const value = useMemo(() => {
|
|
||||||
if (!Array.isArray(oldValue)) return [];
|
|
||||||
return oldValue.filter(item => getRowById(tagsData, item.row_id)).map(item => item.row_id);
|
|
||||||
}, [oldValue, tagsData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sf-metadata-ui cell-formatter-container link-formatter sf-metadata-link-formatter sf-metadata-tags-formatter">
|
|
||||||
{value.length > 0 && (
|
|
||||||
<div className="sf-metadata-tags-formatter-container">
|
|
||||||
{value.map((item) => {
|
|
||||||
const tag = getRowById(tagsData, item);
|
|
||||||
const tagColor = getTagColor(tag);
|
|
||||||
const tagName = getTagName(tag);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="sf-metadata-tag-formatter" key={item} style={{ backgroundColor: tagColor }} title={tagName}></span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
FileTagsFormatter.propTypes = {
|
|
||||||
value: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileTagsFormatter;
|
|
@@ -4,6 +4,7 @@ import { Formatter } from '@seafile/sf-metadata-ui-component';
|
|||||||
import { useCollaborators } from '../../hooks';
|
import { useCollaborators } from '../../hooks';
|
||||||
import { CellType } from '../../constants';
|
import { CellType } from '../../constants';
|
||||||
import FileName from './file-name';
|
import FileName from './file-name';
|
||||||
|
import { useTags } from '../../../tag/hooks';
|
||||||
|
|
||||||
const CellFormatter = ({ readonly, value, field, record, ...params }) => {
|
const CellFormatter = ({ readonly, value, field, record, ...params }) => {
|
||||||
const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators();
|
const { collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser } = useCollaborators();
|
||||||
@@ -18,13 +19,14 @@ const CellFormatter = ({ readonly, value, field, record, ...params }) => {
|
|||||||
queryUserAPI: queryUser,
|
queryUserAPI: queryUser,
|
||||||
};
|
};
|
||||||
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser]);
|
}, [readonly, value, field, collaborators, collaboratorsCache, updateCollaboratorsCache, queryUser]);
|
||||||
|
const { tagsData } = useTags();
|
||||||
|
|
||||||
if (field.type === CellType.FILE_NAME) {
|
if (field.type === CellType.FILE_NAME) {
|
||||||
return (<FileName { ...props } { ...params } record={record} />);
|
return (<FileName { ...props } { ...params } record={record} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formatter { ...props } { ...params } />
|
<Formatter { ...props } { ...params } tagsData={tagsData} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@ import CollaboratorEditor from './collaborator-editor';
|
|||||||
import DateEditor from './date-editor';
|
import DateEditor from './date-editor';
|
||||||
import LongTextEditor from './long-text-editor';
|
import LongTextEditor from './long-text-editor';
|
||||||
import RateEditor from './rate-editor';
|
import RateEditor from './rate-editor';
|
||||||
|
import TagsEditor from './tags-editor';
|
||||||
import { lang } from '../../../utils/constants';
|
import { lang } from '../../../utils/constants';
|
||||||
import { CellType } from '../../constants';
|
import { CellType } from '../../constants';
|
||||||
|
|
||||||
@@ -47,6 +48,9 @@ const DetailEditor = ({ field, onChange: onChangeAPI, ...props }) => {
|
|||||||
case CellType.RATE: {
|
case CellType.RATE: {
|
||||||
return (<RateEditor { ...props } field={field} onChange={onChange} />);
|
return (<RateEditor { ...props } field={field} onChange={onChange} />);
|
||||||
}
|
}
|
||||||
|
case CellType.TAGS: {
|
||||||
|
return (<TagsEditor { ...props } field={field} />);
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -3,3 +3,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-rate-property-detail-editor .sf-metadata-rate-editor {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,46 @@
|
|||||||
|
.sf-metadata-tags-property-detail-editor {
|
||||||
|
padding: 0 6px;
|
||||||
|
min-height: 34px;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-property-detail-editor:empty {
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-property-detail-editor .sf-metadata-delete-select-tags {
|
||||||
|
min-height: 34px;
|
||||||
|
border-bottom: none;
|
||||||
|
background-color: inherit;
|
||||||
|
border-radius: 0;
|
||||||
|
border-radius: initial;
|
||||||
|
padding: 2px 0px;
|
||||||
|
max-height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-property-detail-editor .sf-metadata-delete-select-tags .sf-metadata-delete-select-tag {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-property-editor-popover .sf-metadata-delete-select-tags {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-property-editor-popover .popover {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-property-editor-popover .sf-metadata-tags-editor {
|
||||||
|
position: unset;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 1;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useCallback, useMemo, useState, useRef, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Popover } from 'reactstrap';
|
||||||
|
import { getRowById } from '../../../utils/table';
|
||||||
|
import { getRecordIdFromRecord } from '../../../utils/cell';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
import DeleteTag from '../../cell-editors/tags-editor/delete-tags';
|
||||||
|
import { KeyCodes } from '../../../../constants';
|
||||||
|
import { getEventClassName } from '../../../utils/common';
|
||||||
|
import Editor from '../../cell-editors/tags-editor';
|
||||||
|
import { useTags } from '../../../../tag/hooks';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const TagsEditor = ({ record, value, field, updateFileTags }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
|
|
||||||
|
const { tagsData } = useTags();
|
||||||
|
|
||||||
|
const validValue = useMemo(() => {
|
||||||
|
if (!Array.isArray(value) || value.length === 0) return [];
|
||||||
|
return value.filter(item => getRowById(tagsData, item.row_id)).map(item => item.row_id);
|
||||||
|
}, [value, tagsData]);
|
||||||
|
|
||||||
|
const onClick = useCallback((event) => {
|
||||||
|
if (!event.target) return;
|
||||||
|
const className = getEventClassName(event);
|
||||||
|
if (className.indexOf('sf-metadata-search-tags') > -1) return;
|
||||||
|
const dom = document.querySelector('.sf-metadata-tags-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 onDeleteTag = useCallback((tagId, event) => {
|
||||||
|
event && event.stopPropagation();
|
||||||
|
event && event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
|
||||||
|
const newValue = validValue.slice(0);
|
||||||
|
let optionIdx = validValue.indexOf(tagId);
|
||||||
|
if (optionIdx > -1) {
|
||||||
|
newValue.splice(optionIdx, 1);
|
||||||
|
}
|
||||||
|
const recordId = getRecordIdFromRecord(record);
|
||||||
|
updateFileTags([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||||
|
}, [validValue, value, record, updateFileTags]);
|
||||||
|
|
||||||
|
const renderEditor = useCallback(() => {
|
||||||
|
if (!showEditor) return null;
|
||||||
|
const { width } = ref.current.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
target={ref}
|
||||||
|
isOpen={true}
|
||||||
|
placement="bottom-end"
|
||||||
|
hideArrow={true}
|
||||||
|
fade={false}
|
||||||
|
className="sf-metadata-property-editor-popover sf-metadata-tags-property-editor-popover"
|
||||||
|
boundariesElement={document.body}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
saveImmediately={true}
|
||||||
|
value={value}
|
||||||
|
column={{ ...field, width: Math.max(width - 2, 200) }}
|
||||||
|
record={record}
|
||||||
|
updateFileTags={updateFileTags}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}, [showEditor, field, record, value, updateFileTags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="sf-metadata-property-detail-editor sf-metadata-tags-property-detail-editor"
|
||||||
|
placeholder={gettext('Empty')}
|
||||||
|
ref={ref}
|
||||||
|
onClick={openEditor}
|
||||||
|
>
|
||||||
|
{validValue.length > 0 && (<DeleteTag value={validValue} tags={tagsData} onDelete={onDeleteTag} />)}
|
||||||
|
{renderEditor()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
TagsEditor.propTypes = {
|
||||||
|
record: PropTypes.object,
|
||||||
|
value: PropTypes.array,
|
||||||
|
field: PropTypes.object,
|
||||||
|
updateFileTags: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagsEditor;
|
@@ -21,7 +21,6 @@ export const NOT_DISPLAY_COLUMN_KEYS = [
|
|||||||
PRIVATE_COLUMN_KEY.LOCATION,
|
PRIVATE_COLUMN_KEY.LOCATION,
|
||||||
PRIVATE_COLUMN_KEY.FACE_LINKS,
|
PRIVATE_COLUMN_KEY.FACE_LINKS,
|
||||||
PRIVATE_COLUMN_KEY.FACE_VECTORS,
|
PRIVATE_COLUMN_KEY.FACE_VECTORS,
|
||||||
PRIVATE_COLUMN_KEY.TAGS,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SYSTEM_FOLDERS = [
|
export const SYSTEM_FOLDERS = [
|
||||||
|
@@ -74,3 +74,17 @@
|
|||||||
.dirent-detail-item-value:not(.editable) .sf-metadata-rate-formatter .sf-metadata-rate-item {
|
.dirent-detail-item-value:not(.editable) .sf-metadata-rate-formatter .sf-metadata-rate-item {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dirent-detail-item-value:not(.editable) .sf-metadata-tags-formatter {
|
||||||
|
padding: 8px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dirent-detail-item-value:not(.editable) .sf-metadata-tags-formatter .sf-metadata-ui-tags-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dirent-detail-item-value:not(.editable) .sf-metadata-tags-formatter .sf-metadata-ui-tags-container .sf-metadata-ui-tag {
|
||||||
|
top: 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
@@ -11,9 +11,11 @@ import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColu
|
|||||||
import { normalizeFields } from './utils';
|
import { normalizeFields } from './utils';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { CellType, EVENT_BUS_TYPE, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants';
|
import { CellType, EVENT_BUS_TYPE, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants';
|
||||||
import { getColumnOptions, getColumnOriginName } from '../../utils/column';
|
import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../../utils/column';
|
||||||
import { SYSTEM_FOLDERS } from './constants';
|
import { SYSTEM_FOLDERS } from './constants';
|
||||||
import Location from './location';
|
import Location from './location';
|
||||||
|
import { checkIsDir } from '../../utils/row';
|
||||||
|
import tagsAPI from '../../../tag/api';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord
|
|||||||
|
|
||||||
const onChange = useCallback((fieldKey, newValue) => {
|
const onChange = useCallback((fieldKey, newValue) => {
|
||||||
const { record, fields } = metadata;
|
const { record, fields } = metadata;
|
||||||
const field = fields.find(f => f.key === fieldKey);
|
const field = getColumnByKey(fields, fieldKey);
|
||||||
const fileName = getColumnOriginName(field);
|
const fileName = getColumnOriginName(field);
|
||||||
const recordId = getRecordIdFromRecord(record);
|
const recordId = getRecordIdFromRecord(record);
|
||||||
const fileObjId = getFileObjIdFromRecord(record);
|
const fileObjId = getFileObjIdFromRecord(record);
|
||||||
@@ -82,6 +84,24 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord
|
|||||||
setMetadata(newMetadata);
|
setMetadata(newMetadata);
|
||||||
}, [metadata]);
|
}, [metadata]);
|
||||||
|
|
||||||
|
const updateFileTags = useCallback((updateRecords) => {
|
||||||
|
const { record } = metadata;
|
||||||
|
const { record_id, tags } = updateRecords[0];
|
||||||
|
|
||||||
|
tagsAPI.updateFileTags(repoID, [{ record_id, tags }]).then(res => {
|
||||||
|
const newValue = tags ? tags.map(id => ({ row_id: id, display_value: id })) : [];
|
||||||
|
const update = { [PRIVATE_COLUMN_KEY.TAGS]: newValue };
|
||||||
|
const newMetadata = { ...metadata, record: { ...record, ...update } };
|
||||||
|
setMetadata(newMetadata);
|
||||||
|
if (window?.sfMetadataContext?.eventBus) {
|
||||||
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, record_id, update);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
});
|
||||||
|
}, [repoID, metadata]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (SYSTEM_FOLDERS.find(folderPath => filePath.startsWith(folderPath))) {
|
if (SYSTEM_FOLDERS.find(folderPath => filePath.startsWith(folderPath))) {
|
||||||
@@ -98,7 +118,11 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord
|
|||||||
metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => {
|
metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => {
|
||||||
const { results, metadata } = res.data;
|
const { results, metadata } = res.data;
|
||||||
const record = Array.isArray(results) && results.length > 0 ? results[0] : {};
|
const record = Array.isArray(results) && results.length > 0 ? results[0] : {};
|
||||||
const fields = normalizeFields(metadata).map(field => new Column(field));
|
let fields = normalizeFields(metadata).map(field => new Column(field));
|
||||||
|
const isDir = checkIsDir(record);
|
||||||
|
if (isDir) {
|
||||||
|
fields = fields.filter(field => field.type !== CellType.TAGS);
|
||||||
|
}
|
||||||
updateRecord && updateRecord(record);
|
updateRecord && updateRecord(record);
|
||||||
setMetadata({ record, fields });
|
setMetadata({ record, fields });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -134,9 +158,17 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord
|
|||||||
return (
|
return (
|
||||||
<DetailItem key={field.key} field={field} readonly={!canEdit}>
|
<DetailItem key={field.key} field={field} readonly={!canEdit}>
|
||||||
{canEdit ? (
|
{canEdit ? (
|
||||||
<DetailEditor field={field} value={value} onChange={onChange} fields={fields} record={record} modifyColumnData={modifyColumnData} />
|
<DetailEditor
|
||||||
|
field={field}
|
||||||
|
value={value}
|
||||||
|
fields={fields}
|
||||||
|
record={record}
|
||||||
|
modifyColumnData={modifyColumnData}
|
||||||
|
onChange={onChange}
|
||||||
|
updateFileTags={updateFileTags}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CellFormatter field={field} value={value} emptyTip={gettext('Empty')} className="sf-metadata-property-detail-formatter" />
|
<CellFormatter readonly={true} field={field} value={value} emptyTip={gettext('Empty')} className="sf-metadata-property-detail-formatter" />
|
||||||
)}
|
)}
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
);
|
);
|
||||||
|
@@ -5,7 +5,6 @@ import CheckboxEditor from '../../../../../../components/cell-editors/checkbox-e
|
|||||||
import RateEditor from '../../../../../../components/cell-editors/rate-editor';
|
import RateEditor from '../../../../../../components/cell-editors/rate-editor';
|
||||||
import { canEditCell } from '../../../../../../utils/column';
|
import { canEditCell } from '../../../../../../utils/column';
|
||||||
import { CellType } from '../../../../../../constants';
|
import { CellType } from '../../../../../../constants';
|
||||||
import FileTagsFormatter from '../../../../../../components/cell-formatter/file-tags-formatter';
|
|
||||||
|
|
||||||
const Formatter = ({ isCellSelected, field, value, onChange, record }) => {
|
const Formatter = ({ isCellSelected, field, value, onChange, record }) => {
|
||||||
const { type } = field;
|
const { type } = field;
|
||||||
@@ -17,10 +16,6 @@ const Formatter = ({ isCellSelected, field, value, onChange, record }) => {
|
|||||||
return (<RateEditor isCellSelected={isCellSelected} value={value} field={field} onChange={onChange} />);
|
return (<RateEditor isCellSelected={isCellSelected} value={value} field={field} onChange={onChange} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === CellType.TAGS) {
|
|
||||||
return (<FileTagsFormatter isCellSelected={isCellSelected} field={field} readonly={!cellEditAble} value={value} record={record} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<CellFormatter readonly={true} isCellSelected={isCellSelected} value={value} field={field} record={record} />);
|
return (<CellFormatter readonly={true} isCellSelected={isCellSelected} value={value} field={field} record={record} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -183,6 +183,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
}, [tagsData, modifyLocalTags]);
|
}, [tagsData, modifyLocalTags]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!handelSelectTag) return;
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
const { search } = window.location;
|
const { search } = window.location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
@@ -206,6 +207,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
}, [isLoading]);
|
}, [isLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!currentPath) return;
|
||||||
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/')) return;
|
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/')) return;
|
||||||
const currentTagId = currentPath.split('/').pop();
|
const currentTagId = currentPath.split('/').pop();
|
||||||
if (currentTagId === ALL_TAGS_ID) {
|
if (currentTagId === ALL_TAGS_ID) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
||||||
import { useTagView } from '../../hooks';
|
import { useTagView, useTags } from '../../hooks';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import TagFile from './tag-file';
|
import TagFile from './tag-file';
|
||||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||||
@@ -10,6 +10,7 @@ import './index.css';
|
|||||||
|
|
||||||
const TagFiles = () => {
|
const TagFiles = () => {
|
||||||
const { tagFiles, repoID, repoInfo } = useTagView();
|
const { tagFiles, repoID, repoInfo } = useTagView();
|
||||||
|
const { tagsData } = useTags();
|
||||||
const [selectedFiles, setSelectedFiles] = useState(null);
|
const [selectedFiles, setSelectedFiles] = useState(null);
|
||||||
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
|
const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false);
|
||||||
const [containerWidth, setContainerWidth] = useState(0);
|
const [containerWidth, setContainerWidth] = useState(0);
|
||||||
@@ -119,6 +120,7 @@ const TagFiles = () => {
|
|||||||
repoID={repoID}
|
repoID={repoID}
|
||||||
isSelected={selectedFiles && selectedFiles.includes(fileId)}
|
isSelected={selectedFiles && selectedFiles.includes(fileId)}
|
||||||
file={file}
|
file={file}
|
||||||
|
tagsData={tagsData}
|
||||||
onSelectFile={onSelectFile}
|
onSelectFile={onSelectFile}
|
||||||
reSelectFiles={reSelectFiles}
|
reSelectFiles={reSelectFiles}
|
||||||
openImagePreview={openImagePreview}
|
openImagePreview={openImagePreview}
|
||||||
|
@@ -1,10 +1,15 @@
|
|||||||
.tag-list-title .sf-metadata-tags-formatter .sf-metadata-tag-formatter {
|
.sf-metadata-tags-main .tag-list-title .sf-metadata-ui-tags-container {
|
||||||
height: 16px;
|
width: fit-content;
|
||||||
width: 16px;
|
max-width: 100%;
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-list-title .sf-metadata-tags-formatter .sf-metadata-tag-formatter:last-child {
|
.tag-list-title .sf-metadata-ui-tags-container .sf-metadata-ui-tag {
|
||||||
|
height: 16px !important;
|
||||||
|
width: 16px !important;
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list-title .sf-metadata-ui-tags-container .sf-metadata-ui-tag:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,8 +23,3 @@
|
|||||||
.sf-metadata-tags-main .table-container td.name a {
|
.sf-metadata-tags-main .table-container td.name a {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-main .tag-list-title .sf-metadata-tags-formatter {
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
@@ -3,19 +3,19 @@ import PropTypes from 'prop-types';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import { FileTagsFormatter } from '@seafile/sf-metadata-ui-component';
|
||||||
import { gettext, siteRoot, thumbnailDefaultSize } from '../../../../utils/constants';
|
import { gettext, siteRoot, thumbnailDefaultSize } from '../../../../utils/constants';
|
||||||
import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, getFileSizedFromRecord,
|
import { getParentDirFromRecord, getRecordIdFromRecord, getFileNameFromRecord, getFileSizedFromRecord,
|
||||||
getFileMTimeFromRecord, getTagsFromRecord, getFilePathByRecord,
|
getFileMTimeFromRecord, getTagsFromRecord, getFilePathByRecord,
|
||||||
} from '../../../../metadata/utils/cell';
|
} from '../../../../metadata/utils/cell';
|
||||||
import { Utils } from '../../../../utils/utils';
|
import { Utils } from '../../../../utils/utils';
|
||||||
import FileTagsFormatter from '../../../../metadata/components/cell-formatter/file-tags-formatter';
|
|
||||||
import { openFile } from '../../../../metadata/utils/open-file';
|
import { openFile } from '../../../../metadata/utils/open-file';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const TagFile = ({ isSelected, repoID, file, onSelectFile, reSelectFiles, openImagePreview }) => {
|
const TagFile = ({ isSelected, repoID, file, tagsData, onSelectFile, reSelectFiles, openImagePreview }) => {
|
||||||
const [highlight, setHighlight] = useState(false);
|
const [highlight, setHighlight] = useState(false);
|
||||||
const [isIconLoadError, setIconLoadError] = useState(false);
|
const [isIconLoadError, setIconLoadError] = useState(false);
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ const TagFile = ({ isSelected, repoID, file, onSelectFile, reSelectFiles, openIm
|
|||||||
<a href={path} onClick={handelClickFileName}>{name}</a>
|
<a href={path} onClick={handelClickFileName}>{name}</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="tag-list-title">
|
<td className="tag-list-title">
|
||||||
<FileTagsFormatter value={tags} />
|
<FileTagsFormatter value={tags} tagsData={tagsData} className="sf-metadata-tags-formatter" />
|
||||||
</td>
|
</td>
|
||||||
<td className="operation"></td>
|
<td className="operation"></td>
|
||||||
<td className="file-size">{size || ''}</td>
|
<td className="file-size">{size || ''}</td>
|
||||||
|
@@ -7,6 +7,7 @@ import Loading from './components/loading';
|
|||||||
import SdocEditor from './pages/sdoc/sdoc-editor';
|
import SdocEditor from './pages/sdoc/sdoc-editor';
|
||||||
import { MetadataStatusProvider } from './hooks';
|
import { MetadataStatusProvider } from './hooks';
|
||||||
import { CollaboratorsProvider } from './metadata';
|
import { CollaboratorsProvider } from './metadata';
|
||||||
|
import { TagsProvider } from './tag/hooks';
|
||||||
|
|
||||||
const { serviceURL, avatarURL, siteRoot, lang, mediaUrl, isPro } = window.app.config;
|
const { serviceURL, avatarURL, siteRoot, lang, mediaUrl, isPro } = window.app.config;
|
||||||
const { username, name } = window.app.userInfo;
|
const { username, name } = window.app.userInfo;
|
||||||
@@ -55,7 +56,9 @@ ReactDom.render(
|
|||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<MetadataStatusProvider repoID={repoID}>
|
<MetadataStatusProvider repoID={repoID}>
|
||||||
<CollaboratorsProvider repoID={repoID}>
|
<CollaboratorsProvider repoID={repoID}>
|
||||||
<SdocEditor />
|
<TagsProvider repoID={repoID} repoInfo={{ permission: filePerm }}>
|
||||||
|
<SdocEditor />
|
||||||
|
</TagsProvider>
|
||||||
</CollaboratorsProvider>
|
</CollaboratorsProvider>
|
||||||
</MetadataStatusProvider>
|
</MetadataStatusProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@@ -231,12 +231,12 @@ def remove_tags_table(metadata_server_api):
|
|||||||
tables = metadata.get('tables', [])
|
tables = metadata.get('tables', [])
|
||||||
for table in tables:
|
for table in tables:
|
||||||
if table['name'] == TAGS_TABLE.name:
|
if table['name'] == TAGS_TABLE.name:
|
||||||
metadata_server_api.delete_table(table['id'])
|
metadata_server_api.delete_table(table['id'], True)
|
||||||
elif table['name'] == METADATA_TABLE.name:
|
elif table['name'] == METADATA_TABLE.name:
|
||||||
columns = table.get('columns', [])
|
columns = table.get('columns', [])
|
||||||
for column in columns:
|
for column in columns:
|
||||||
if column['key'] in [METADATA_TABLE.columns.tags.key]:
|
if column['key'] in [METADATA_TABLE.columns.tags.key]:
|
||||||
metadata_server_api.delete_column(table['id'], column['key'])
|
metadata_server_api.delete_column(table['id'], column['key'], True)
|
||||||
|
|
||||||
|
|
||||||
def get_file_download_token(repo_id, file_id, username):
|
def get_file_download_token(repo_id, file_id, username):
|
||||||
|
Reference in New Issue
Block a user