mirror of
https://github.com/haiwen/seahub.git
synced 2025-04-27 19:05:16 +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:
parent
e67fc4a3d9
commit
1be01e5186
@ -25,7 +25,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detail-header .detail-control .detail-control-close {
|
||||
.detail-header .detail-control .detail-control-icon {
|
||||
font-size: 16px;
|
||||
fill: #666;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ const Header = ({ title, icon, iconSize = 32, onClose, children, component = {}
|
||||
{children}
|
||||
{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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
.sf-metadata-auto-image-tags .auto-image-tags-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sf-metadata-auto-image-tags .auto-image-tag {
|
||||
height: 28px;
|
||||
.sf-file-tags .sf-file-new-tag {
|
||||
display: inline-flex;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 4px;
|
||||
border-radius: 14px;
|
||||
margin-right: 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap';
|
||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||
import CenteredLoading from '../../../../components/centered-loading';
|
||||
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 { Utils } from '../../../../utils/utils';
|
||||
import { getFileNameFromRecord, getParentDirFromRecord, getTagsFromRecord, getRecordIdFromRecord } from '../../../utils/cell';
|
||||
@ -20,12 +18,13 @@ import './index.css';
|
||||
const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
||||
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [isSubmitting, setSubmitting] = useState(false);
|
||||
const [fileTags, setFileTags] = useState([]);
|
||||
const [newTags, setNewTags] = useState([]);
|
||||
const [exitTags, setExitTags] = useState([]);
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
|
||||
const fileName = useMemo(() => getFileNameFromRecord(record), [record]);
|
||||
|
||||
const lastSettingsValue = parseInt(localStorage.getItem('sf_cur_view_detail_width'));
|
||||
const { tagsData, addTags } = useTags();
|
||||
|
||||
useEffect(() => {
|
||||
@ -40,7 +39,18 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
||||
}
|
||||
window.sfMetadataContext.generateFileTags(path).then(res => {
|
||||
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);
|
||||
}).catch(error => {
|
||||
const errorMessage = gettext('Failed to generate file tags');
|
||||
@ -48,9 +58,9 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
||||
setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [tagsData]);
|
||||
|
||||
const onSelectImageTag = useCallback((tagName) => {
|
||||
const onClickTag = useCallback((tagName) => {
|
||||
let newSelectedTags = selectedTags.slice(0);
|
||||
const tagNameIndex = selectedTags.findIndex(i => i === tagName);
|
||||
if (tagNameIndex === -1) {
|
||||
@ -62,37 +72,40 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
||||
}, [selectedTags]);
|
||||
|
||||
const handelSubmit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
if (selectedTags.length === 0) {
|
||||
if (isLoading || selectedTags.length === 0) {
|
||||
onToggle();
|
||||
return;
|
||||
}
|
||||
|
||||
let { newTags, exitTagIds } = selectedTags.reduce((cur, pre) => {
|
||||
const tag = getTagByName(tagsData, pre);
|
||||
let selectedNewTags = [];
|
||||
let selectedExitTags = [];
|
||||
selectedTags.forEach(tagName => {
|
||||
const tag = getTagByName(tagsData, tagName);
|
||||
if (tag) {
|
||||
cur.exitTagIds.push(getTagId(tag));
|
||||
selectedExitTags.push(tag);
|
||||
} 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 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);
|
||||
let value = getTagsFromRecord(record);
|
||||
value = value ? value.map(item => item.row_id) : [];
|
||||
|
||||
if (newTags.length > 0) {
|
||||
addTags(newTags, {
|
||||
if (selectedNewTags.length > 0) {
|
||||
addTags(selectedNewTags, {
|
||||
success_callback: (operation) => {
|
||||
const newTagIds = operation.tags?.map(tag => getTagId(tag));
|
||||
let newValue = [...value, ...newTagIds];
|
||||
exitTagIds.forEach(id => {
|
||||
selectedExitTags.forEach(id => {
|
||||
if (!newValue.includes(id)) {
|
||||
newValue.push(id);
|
||||
}
|
||||
@ -101,56 +114,88 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
|
||||
onToggle();
|
||||
},
|
||||
fail_callback: (error) => {
|
||||
setSubmitting(false);
|
||||
toaster.danger(Utils.getErrorMsg(error));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
let newValue = [...value];
|
||||
exitTagIds.forEach(id => {
|
||||
if (!newValue.includes(id)) {
|
||||
newValue.push(id);
|
||||
} else {
|
||||
let newValue = [...value];
|
||||
selectedExitTags.forEach(id => {
|
||||
if (!newValue.includes(id)) {
|
||||
newValue.push(id);
|
||||
}
|
||||
});
|
||||
if (newValue.length !== value.length) {
|
||||
onSubmit([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||
}
|
||||
});
|
||||
if (newValue.length !== value.length) {
|
||||
onSubmit([{ record_id: recordId, tags: newValue, old_tags: value }]);
|
||||
onToggle();
|
||||
}
|
||||
onToggle();
|
||||
}, [selectedTags, onSubmit, onToggle, record, addTags, tagsData]);
|
||||
}, [selectedTags, onSubmit, onToggle, record, addTags, tagsData, isLoading]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} toggle={() => onToggle()} className="sf-metadata-auto-image-tags">
|
||||
<SeahubModalHeader toggle={() => onToggle()}>{fileName + gettext('\'s tags')}</SeahubModalHeader>
|
||||
<ModalBody>
|
||||
{isLoading ? (
|
||||
<CenteredLoading />
|
||||
) : (
|
||||
<div className="auto-image-tags-container">
|
||||
{fileTags.length > 0 ? (
|
||||
<>
|
||||
{fileTags.map((tagName, index) => {
|
||||
const isSelected = selectedTags.includes(tagName);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames('auto-image-tag', { 'selected': isSelected })}
|
||||
onClick={() => onSelectImageTag(tagName)}
|
||||
>
|
||||
{tagName}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<EmptyTip className="w-100 h-100" text={gettext('No tags')} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={() => onToggle()}>{gettext('Cancel')}</Button>
|
||||
<Button color="primary" disabled={isLoading || isSubmitting || fileTags.length === 0} onClick={handelSubmit}>{gettext('Submit')}</Button>
|
||||
</ModalFooter>
|
||||
<Modal
|
||||
isOpen={true}
|
||||
toggle={() => { handelSubmit(); }}
|
||||
className="sf-file-tags"
|
||||
backdropClassName="sf-file-tags-backdrop"
|
||||
style={{ marginRight: lastSettingsValue }}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} className="modal-content">
|
||||
<ModalHeader>{fileName + ' ' + gettext('tags')}</ModalHeader>
|
||||
<ModalBody>
|
||||
{isLoading ?
|
||||
<CenteredLoading />
|
||||
:
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<div className='mb-1'>{gettext('Matching tags')}</div>
|
||||
{exitTags.length > 0 && (
|
||||
<>
|
||||
{exitTags.map((tag, index) => {
|
||||
const { _tag_color: tagColor, _tag_name: tagName } = tag;
|
||||
const isSelected = selectedTags.includes(tagName);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames('sf-file-exit-tag', { 'selected': isSelected })}
|
||||
onClick={() => onClickTag(tagName)}
|
||||
>
|
||||
<div className="sf-file-exit-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||
<div className="sf-file-exit-tag-name">{tagName}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
@ -147,7 +147,7 @@ const AIIcon = () => {
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="detail-control mr-2">
|
||||
<Icon symbol="ai" className="detail-control-close" />
|
||||
<Icon symbol="ai" className="detail-control-icon" />
|
||||
</div>
|
||||
</DropdownToggle>
|
||||
{isMenuShow && (
|
||||
|
@ -21,7 +21,7 @@ const SettingsIcon = () => {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
{isShowSetter && (
|
||||
<HideColumnPopover
|
||||
|
Loading…
Reference in New Issue
Block a user