mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-19 18:29:23 +00:00
Change generate tags UI (#7712)
* 01 change header icon class * 02 change tags UI * 03 change modal header title
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header .detail-control .detail-control-close {
|
.detail-header .detail-control .detail-control-icon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
fill: #666;
|
fill: #666;
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ const Header = ({ title, icon, iconSize = 32, onClose, children, component = {}
|
|||||||
{children}
|
{children}
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<div className="detail-control" onClick={onClose}>
|
<div className="detail-control" onClick={onClose}>
|
||||||
{closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-close" />}
|
{closeIcon ? closeIcon : <Icon symbol="close" className="detail-control-icon" />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,24 +1,76 @@
|
|||||||
.sf-metadata-auto-image-tags .modal-body {
|
.sf-file-tags-backdrop.show {
|
||||||
|
opacity: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-file-tags {
|
||||||
|
margin-top: 370px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-file-tags .modal-content {
|
||||||
|
border: 1px solid #efefef;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-file-tags .modal-header {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-file-tags .modal-body {
|
||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-auto-image-tags .auto-image-tags-container {
|
.sf-file-tags .sf-file-new-tag {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
height: 20px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-auto-image-tags .auto-image-tag {
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border: 1px solid #dedede;
|
border: 1px solid #dedede;
|
||||||
border-radius: 4px;
|
border-radius: 14px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
line-height: 26px;
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sf-file-exit-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 0 8px 0 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #dbdbdb;
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-auto-image-tags .auto-image-tag.selected {
|
.sf-file-exit-tag .sf-file-exit-tag-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-file-exit-tag .sf-file-exit-tag-name {
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #212529;
|
||||||
|
margin-left: 8px;
|
||||||
|
max-width: 200px;
|
||||||
|
flex: 1 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-file-new-tag.selected,
|
||||||
|
.sf-file-exit-tag.selected {
|
||||||
border-color: #FF9800;
|
border-color: #FF9800;
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,9 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import CenteredLoading from '../../../../components/centered-loading';
|
import CenteredLoading from '../../../../components/centered-loading';
|
||||||
import toaster from '../../../../components/toast';
|
import toaster from '../../../../components/toast';
|
||||||
import EmptyTip from '../../../../components/empty-tip';
|
|
||||||
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
|
||||||
import { gettext } from '../../../../utils/constants';
|
import { gettext } from '../../../../utils/constants';
|
||||||
import { Utils } from '../../../../utils/utils';
|
import { Utils } from '../../../../utils/utils';
|
||||||
import { getFileNameFromRecord, getParentDirFromRecord, getTagsFromRecord, getRecordIdFromRecord } from '../../../utils/cell';
|
import { getFileNameFromRecord, getParentDirFromRecord, getTagsFromRecord, getRecordIdFromRecord } from '../../../utils/cell';
|
||||||
@@ -20,12 +18,13 @@ import './index.css';
|
|||||||
const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
const [isSubmitting, setSubmitting] = useState(false);
|
const [newTags, setNewTags] = useState([]);
|
||||||
const [fileTags, setFileTags] = useState([]);
|
const [exitTags, setExitTags] = useState([]);
|
||||||
const [selectedTags, setSelectedTags] = useState([]);
|
const [selectedTags, setSelectedTags] = useState([]);
|
||||||
|
|
||||||
const fileName = useMemo(() => getFileNameFromRecord(record), [record]);
|
const fileName = useMemo(() => getFileNameFromRecord(record), [record]);
|
||||||
|
|
||||||
|
const lastSettingsValue = parseInt(localStorage.getItem('sf_cur_view_detail_width'));
|
||||||
const { tagsData, addTags } = useTags();
|
const { tagsData, addTags } = useTags();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,7 +39,18 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
|||||||
}
|
}
|
||||||
window.sfMetadataContext.generateFileTags(path).then(res => {
|
window.sfMetadataContext.generateFileTags(path).then(res => {
|
||||||
const tags = res.data.tags || [];
|
const tags = res.data.tags || [];
|
||||||
setFileTags(tags);
|
let newTags = [];
|
||||||
|
let exitTags = [];
|
||||||
|
tags.forEach(tag => {
|
||||||
|
const tagObj = getTagByName(tagsData, tag);
|
||||||
|
if (tagObj) {
|
||||||
|
exitTags.push(tagObj);
|
||||||
|
} else {
|
||||||
|
newTags.push(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setNewTags(newTags);
|
||||||
|
setExitTags(exitTags);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
const errorMessage = gettext('Failed to generate file tags');
|
const errorMessage = gettext('Failed to generate file tags');
|
||||||
@@ -48,9 +58,9 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [tagsData]);
|
||||||
|
|
||||||
const onSelectImageTag = useCallback((tagName) => {
|
const onClickTag = useCallback((tagName) => {
|
||||||
let newSelectedTags = selectedTags.slice(0);
|
let newSelectedTags = selectedTags.slice(0);
|
||||||
const tagNameIndex = selectedTags.findIndex(i => i === tagName);
|
const tagNameIndex = selectedTags.findIndex(i => i === tagName);
|
||||||
if (tagNameIndex === -1) {
|
if (tagNameIndex === -1) {
|
||||||
@@ -62,37 +72,40 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
|||||||
}, [selectedTags]);
|
}, [selectedTags]);
|
||||||
|
|
||||||
const handelSubmit = useCallback(() => {
|
const handelSubmit = useCallback(() => {
|
||||||
setSubmitting(true);
|
if (isLoading || selectedTags.length === 0) {
|
||||||
if (selectedTags.length === 0) {
|
|
||||||
onToggle();
|
onToggle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { newTags, exitTagIds } = selectedTags.reduce((cur, pre) => {
|
let selectedNewTags = [];
|
||||||
const tag = getTagByName(tagsData, pre);
|
let selectedExitTags = [];
|
||||||
|
selectedTags.forEach(tagName => {
|
||||||
|
const tag = getTagByName(tagsData, tagName);
|
||||||
if (tag) {
|
if (tag) {
|
||||||
cur.exitTagIds.push(getTagId(tag));
|
selectedExitTags.push(tag);
|
||||||
} else {
|
} else {
|
||||||
cur.newTags.push(pre);
|
selectedNewTags.push(tagName);
|
||||||
}
|
}
|
||||||
return cur;
|
});
|
||||||
}, { newTags: [], exitTagIds: [] });
|
|
||||||
|
|
||||||
newTags = newTags.map(tagName => {
|
selectedNewTags = selectedNewTags.map(tagName => {
|
||||||
const defaultOptions = SELECT_OPTION_COLORS.slice(0, 24);
|
const defaultOptions = SELECT_OPTION_COLORS.slice(0, 24);
|
||||||
const defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];
|
const defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];
|
||||||
return { [TAGS_PRIVATE_COLUMN_KEY.TAG_NAME]: tagName, [TAGS_PRIVATE_COLUMN_KEY.TAG_COLOR]: defaultOption.COLOR };
|
return {
|
||||||
|
[TAGS_PRIVATE_COLUMN_KEY.TAG_NAME]: tagName,
|
||||||
|
[TAGS_PRIVATE_COLUMN_KEY.TAG_COLOR]: defaultOption.COLOR,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const recordId = getRecordIdFromRecord(record);
|
const recordId = getRecordIdFromRecord(record);
|
||||||
let value = getTagsFromRecord(record);
|
let value = getTagsFromRecord(record);
|
||||||
value = value ? value.map(item => item.row_id) : [];
|
value = value ? value.map(item => item.row_id) : [];
|
||||||
|
|
||||||
if (newTags.length > 0) {
|
if (selectedNewTags.length > 0) {
|
||||||
addTags(newTags, {
|
addTags(selectedNewTags, {
|
||||||
success_callback: (operation) => {
|
success_callback: (operation) => {
|
||||||
const newTagIds = operation.tags?.map(tag => getTagId(tag));
|
const newTagIds = operation.tags?.map(tag => getTagId(tag));
|
||||||
let newValue = [...value, ...newTagIds];
|
let newValue = [...value, ...newTagIds];
|
||||||
exitTagIds.forEach(id => {
|
selectedExitTags.forEach(id => {
|
||||||
if (!newValue.includes(id)) {
|
if (!newValue.includes(id)) {
|
||||||
newValue.push(id);
|
newValue.push(id);
|
||||||
}
|
}
|
||||||
@@ -101,56 +114,88 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
|||||||
onToggle();
|
onToggle();
|
||||||
},
|
},
|
||||||
fail_callback: (error) => {
|
fail_callback: (error) => {
|
||||||
setSubmitting(false);
|
toaster.danger(Utils.getErrorMsg(error));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
} else {
|
||||||
}
|
let newValue = [...value];
|
||||||
let newValue = [...value];
|
selectedExitTags.forEach(id => {
|
||||||
exitTagIds.forEach(id => {
|
if (!newValue.includes(id)) {
|
||||||
if (!newValue.includes(id)) {
|
newValue.push(id);
|
||||||
newValue.push(id);
|
}
|
||||||
|
});
|
||||||
|
if (newValue.length !== value.length) {
|
||||||
|
onSubmit([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||||
}
|
}
|
||||||
});
|
onToggle();
|
||||||
if (newValue.length !== value.length) {
|
|
||||||
onSubmit([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
|
||||||
}
|
}
|
||||||
onToggle();
|
}, [selectedTags, onSubmit, onToggle, record, addTags, tagsData, isLoading]);
|
||||||
}, [selectedTags, onSubmit, onToggle, record, addTags, tagsData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={true} toggle={() => onToggle()} className="sf-metadata-auto-image-tags">
|
<Modal
|
||||||
<SeahubModalHeader toggle={() => onToggle()}>{fileName + gettext('\'s tags')}</SeahubModalHeader>
|
isOpen={true}
|
||||||
<ModalBody>
|
toggle={() => { handelSubmit(); }}
|
||||||
{isLoading ? (
|
className="sf-file-tags"
|
||||||
<CenteredLoading />
|
backdropClassName="sf-file-tags-backdrop"
|
||||||
) : (
|
style={{ marginRight: lastSettingsValue }}
|
||||||
<div className="auto-image-tags-container">
|
>
|
||||||
{fileTags.length > 0 ? (
|
<div onClick={(e) => e.stopPropagation()} className="modal-content">
|
||||||
<>
|
<ModalHeader>{fileName + ' ' + gettext('tags')}</ModalHeader>
|
||||||
{fileTags.map((tagName, index) => {
|
<ModalBody>
|
||||||
const isSelected = selectedTags.includes(tagName);
|
{isLoading ?
|
||||||
return (
|
<CenteredLoading />
|
||||||
<div
|
:
|
||||||
key={index}
|
<div>
|
||||||
className={classNames('auto-image-tag', { 'selected': isSelected })}
|
<div className="mb-6">
|
||||||
onClick={() => onSelectImageTag(tagName)}
|
<div className='mb-1'>{gettext('Matching tags')}</div>
|
||||||
>
|
{exitTags.length > 0 && (
|
||||||
{tagName}
|
<>
|
||||||
</div>
|
{exitTags.map((tag, index) => {
|
||||||
);
|
const { _tag_color: tagColor, _tag_name: tagName } = tag;
|
||||||
})}
|
const isSelected = selectedTags.includes(tagName);
|
||||||
</>
|
return (
|
||||||
) : (
|
<div
|
||||||
<EmptyTip className="w-100 h-100" text={gettext('No tags')} />
|
key={index}
|
||||||
)}
|
className={classNames('sf-file-exit-tag', { 'selected': isSelected })}
|
||||||
</div>
|
onClick={() => onClickTag(tagName)}
|
||||||
)}
|
>
|
||||||
</ModalBody>
|
<div className="sf-file-exit-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||||
<ModalFooter>
|
<div className="sf-file-exit-tag-name">{tagName}</div>
|
||||||
<Button color="secondary" onClick={() => onToggle()}>{gettext('Cancel')}</Button>
|
</div>
|
||||||
<Button color="primary" disabled={isLoading || isSubmitting || fileTags.length === 0} onClick={handelSubmit}>{gettext('Submit')}</Button>
|
);
|
||||||
</ModalFooter>
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{exitTags.length === 0 && (
|
||||||
|
<span className='tip'>{gettext('No matching tags')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className='mb-1'>{gettext('Recommended new tags')}</div>
|
||||||
|
{newTags.length > 0 && (
|
||||||
|
<>
|
||||||
|
{newTags.map((tagName, index) => {
|
||||||
|
const isSelected = selectedTags.includes(tagName);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={classNames('sf-file-new-tag', { 'selected': isSelected })}
|
||||||
|
onClick={() => onClickTag(tagName)}
|
||||||
|
>
|
||||||
|
{tagName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{newTags.length === 0 && (
|
||||||
|
<span className='tip'>{gettext('No recommended new tags')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -147,7 +147,7 @@ const AIIcon = () => {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div className="detail-control mr-2">
|
<div className="detail-control mr-2">
|
||||||
<Icon symbol="ai" className="detail-control-close" />
|
<Icon symbol="ai" className="detail-control-icon" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
{isMenuShow && (
|
{isMenuShow && (
|
||||||
|
@@ -21,7 +21,7 @@ const SettingsIcon = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="detail-control mr-2" id={target} onClick={onSetterToggle}>
|
<div className="detail-control mr-2" id={target} onClick={onSetterToggle}>
|
||||||
<Icon symbol="set-up" className="detail-control-close" />
|
<Icon symbol="set-up" className="detail-control-icon" />
|
||||||
</div>
|
</div>
|
||||||
{isShowSetter && (
|
{isShowSetter && (
|
||||||
<HideColumnPopover
|
<HideColumnPopover
|
||||||
|
Reference in New Issue
Block a user