1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-20 19:08:21 +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:
Michael An
2025-04-08 16:47:35 +08:00
committed by GitHub
parent e67fc4a3d9
commit 1be01e5186
6 changed files with 178 additions and 81 deletions

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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,13 +114,12 @@ 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];
exitTagIds.forEach(id => { selectedExitTags.forEach(id => {
if (!newValue.includes(id)) { if (!newValue.includes(id)) {
newValue.push(id); newValue.push(id);
} }
@@ -116,41 +128,74 @@ const FileTagsDialog = ({ record, onToggle, onSubmit }) => {
onSubmit([{ record_id: recordId, tags: newValue, old_tags: value }]); 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 ( return (
<Modal isOpen={true} toggle={() => onToggle()} className="sf-metadata-auto-image-tags"> <Modal
<SeahubModalHeader toggle={() => onToggle()}>{fileName + gettext('\'s tags')}</SeahubModalHeader> 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> <ModalBody>
{isLoading ? ( {isLoading ?
<CenteredLoading /> <CenteredLoading />
) : ( :
<div className="auto-image-tags-container"> <div>
{fileTags.length > 0 ? ( <div className="mb-6">
<div className='mb-1'>{gettext('Matching tags')}</div>
{exitTags.length > 0 && (
<> <>
{fileTags.map((tagName, index) => { {exitTags.map((tag, index) => {
const { _tag_color: tagColor, _tag_name: tagName } = tag;
const isSelected = selectedTags.includes(tagName); const isSelected = selectedTags.includes(tagName);
return ( return (
<div <div
key={index} key={index}
className={classNames('auto-image-tag', { 'selected': isSelected })} className={classNames('sf-file-exit-tag', { 'selected': isSelected })}
onClick={() => onSelectImageTag(tagName)} 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} {tagName}
</div> </div>
); );
})} })}
</> </>
) : ( )}
<EmptyTip className="w-100 h-100" text={gettext('No tags')} /> {newTags.length === 0 && (
<span className='tip'>{gettext('No recommended new tags')}</span>
)} )}
</div> </div>
)} </div>
}
</ModalBody> </ModalBody>
<ModalFooter> </div>
<Button color="secondary" onClick={() => onToggle()}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={isLoading || isSubmitting || fileTags.length === 0} onClick={handelSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal> </Modal>
); );
}; };

View File

@@ -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 && (

View File

@@ -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