From f50fe8a2bd0d916ae5332261218d5525f2e0711f Mon Sep 17 00:00:00 2001 From: WangJianhui666 <40563566+WangJianhui666@users.noreply.github.com> Date: Tue, 13 Nov 2018 16:39:13 +0800 Subject: [PATCH] Show file tags (#2509) --- frontend/package.json | 2 +- .../components/dialog/create-tag-dialog.js | 10 +- .../components/dialog/edit-filetag-dialog.js | 110 ++++++++++++++++++ .../src/components/dialog/list-tag-dialog.js | 5 +- .../components/dialog/update-tag-dialog.js | 8 +- .../dirent-detail/detail-list-view.js | 42 ++++++- .../dirent-detail/dirent-details.js | 13 +++ .../dirent-list-view/dirent-list-item.js | 28 +++++ .../dirent-list-view/dirent-list-view.js | 3 +- .../src/components/toolbar/path-toolbar.js | 2 +- frontend/src/css/dirent-detail.css | 21 +++- frontend/src/css/repo-tag.css | 8 +- frontend/src/models/dirent.js | 1 + frontend/src/models/file-tag.js | 10 ++ .../src/pages/repo-wiki-mode/main-panel.js | 5 + media/css/seahub_react.css | 37 +++++- media/css/sf_font2/seafile-font2.eot | Bin 9484 -> 9724 bytes media/css/sf_font2/seafile-font2.svg | 3 +- media/css/sf_font2/seafile-font2.ttf | Bin 9300 -> 9540 bytes media/css/sf_font2/seafile-font2.woff | Bin 6268 -> 6464 bytes seahub/api2/endpoints/file_tag.py | 6 +- seahub/api2/views.py | 8 ++ seahub/file_tags/models.py | 29 +++-- seahub/utils/file_tags.py | 26 +++++ 24 files changed, 346 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/dialog/edit-filetag-dialog.js create mode 100644 frontend/src/models/file-tag.js create mode 100644 seahub/utils/file_tags.py diff --git a/frontend/package.json b/frontend/package.json index ecfdc8631d..9ec076b593 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,7 @@ "react-moment": "^0.7.9", "react-select": "^2.1.1", "reactstrap": "^6.4.0", - "seafile-js": "^0.2.34", + "seafile-js": "^0.2.36", "seafile-ui": "^0.1.10", "sw-precache-webpack-plugin": "0.11.4", "unified": "^7.0.0", diff --git a/frontend/src/components/dialog/create-tag-dialog.js b/frontend/src/components/dialog/create-tag-dialog.js index 5a72737599..5bfed448fa 100644 --- a/frontend/src/components/dialog/create-tag-dialog.js +++ b/frontend/src/components/dialog/create-tag-dialog.js @@ -64,10 +64,12 @@ class CreateTagDialog extends React.Component { {gettext('New Tag')} -
-

{gettext('Name')}

- {this.newInput = input;}} placeholder={gettext('name')} value={this.state.tagName} onChange={this.inputNewName}/> -
+
+
+ + {this.newInput = input;}} placeholder={gettext('name')} value={this.state.tagName} onChange={this.inputNewName}/> +
+
{colorList.map((item, index)=>{ diff --git a/frontend/src/components/dialog/edit-filetag-dialog.js b/frontend/src/components/dialog/edit-filetag-dialog.js new file mode 100644 index 0000000000..38f9f7d918 --- /dev/null +++ b/frontend/src/components/dialog/edit-filetag-dialog.js @@ -0,0 +1,110 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { repoID, gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import RepoTag from '../../models/repo-tag'; + +const propTypes = { + filePath: PropTypes.string.isRequired, + fileTagList: PropTypes.array.isRequired, + onFileTagChanged: PropTypes.func.isRequired, + toggleCancel: PropTypes.func.isRequired, +}; + +class EditFileTagDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + repotagList: [], + }; + } + + componentDidMount() { + this.getRepoTagList(); + } + + getRepoTagList = () => { + seafileAPI.listRepoTags(repoID).then(res => { + let repotagList = []; + res.data.repo_tags.forEach(item => { + let repoTag = new RepoTag(item); + repotagList.push(repoTag); + }); + this.setState({ + repotagList: repotagList, + }); + }); + } + + getRepoTagIdList = () => { + let repoTagIdList = []; + let fileTagList = this.props.fileTagList; + fileTagList.map((fileTag) => { + repoTagIdList.push(fileTag.repo_tag_id); + }); + return repoTagIdList; + } + + editFileTag = (repoTag) => { + let repoTagIdList = this.getRepoTagIdList(); + if (repoTagIdList.indexOf(repoTag.id) === -1) { + let id = repoTag.id; + let filePath = this.props.filePath; + seafileAPI.addFileTag(repoID, filePath, id).then(() => { + repoTagIdList = this.getRepoTagIdList(); + }); + } else { + let fileTag = null; + let fileTagList = this.props.fileTagList; + for(let i = 0; i < fileTagList.length; i++) { + if (fileTagList[i].repo_tag_id === repoTag.id) { + fileTag = fileTagList[i]; + break; + } + } + seafileAPI.deleteFileTag(repoID, fileTag.id).then(() => { + repoTagIdList = this.getRepoTagIdList(); + }); + } + this.props.onFileTagChanged(); + } + + toggle = () => { + this.props.toggleCancel(); + } + + render() { + let repoTagIdList = this.getRepoTagIdList(); + return ( + + {gettext('File Tags')} + + { +
    + {this.state.repotagList.map((repoTag) => { + return ( +
  • +
    + {repoTag.name} + {repoTagIdList.indexOf(repoTag.id) > -1 && + + } +
    +
  • + ); + })} +
+ } +
+ + + +
+ ); + } +} + +EditFileTagDialog.propTypes = propTypes; + +export default EditFileTagDialog; diff --git a/frontend/src/components/dialog/list-tag-dialog.js b/frontend/src/components/dialog/list-tag-dialog.js index 00816b52e5..2df5431114 100644 --- a/frontend/src/components/dialog/list-tag-dialog.js +++ b/frontend/src/components/dialog/list-tag-dialog.js @@ -18,9 +18,10 @@ class TagListItem extends React.Component { } render() { + let color = this.props.item.color; return(
  • - {this.props.item.name} + {this.props.item.name}
  • ); @@ -64,7 +65,7 @@ class ListTagDialog extends React.Component { return ( - {gettext('Tag List')} + {gettext('Tags')} { this.state.repotagList.length === 0 && diff --git a/frontend/src/components/dialog/update-tag-dialog.js b/frontend/src/components/dialog/update-tag-dialog.js index 55bf9e9392..72deac5c38 100644 --- a/frontend/src/components/dialog/update-tag-dialog.js +++ b/frontend/src/components/dialog/update-tag-dialog.js @@ -78,9 +78,11 @@ class UpdateTagDialog extends React.Component { {gettext('Edit Tag')}
    -

    {gettext('Name:')}

    - {this.newInput = input;}} placeholder="newName" value={this.state.newName} onChange={this.inputNewName}/> -
    +
    + + {this.newInput = input;}} placeholder="newName" value={this.state.newName} onChange={this.inputNewName}/> +
    +
    {colorList.map((item, index)=>{ diff --git a/frontend/src/components/dirent-detail/detail-list-view.js b/frontend/src/components/dirent-detail/detail-list-view.js index 39273cd72e..1eef1461ba 100644 --- a/frontend/src/components/dirent-detail/detail-list-view.js +++ b/frontend/src/components/dirent-detail/detail-list-view.js @@ -3,16 +3,26 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import { gettext } from '../../utils/constants'; import { Utils } from '../../utils/utils'; +import EditFileTagDialog from '../dialog/edit-filetag-dialog'; const propTypes = { repo: PropTypes.object.isRequired, direntType: PropTypes.string.isRequired, direntDetail: PropTypes.object.isRequired, direntPath: PropTypes.string.isRequired, + fileTagList: PropTypes.array.isRequired, + onFileTagChanged: PropTypes.func.isRequired, }; class DetailListView extends React.Component { + constructor(props) { + super(props); + this.state = { + isEditFileTagShow: false, + }; + } + getDirentPostion = () => { let { repo, direntPath } = this.props; let position = repo.repo_name + '/'; @@ -24,8 +34,14 @@ class DetailListView extends React.Component { return position; } + onEditFileTagToggle = () => { + this.setState({ + isEditFileTagShow: !this.state.isEditFileTagShow + }); + } + render() { - let { direntType, direntDetail } = this.props; + let { direntType, direntDetail, fileTagList } = this.props; let position = this.getDirentPostion(); if (direntType === 'dir') { return ( @@ -51,8 +67,32 @@ class DetailListView extends React.Component { {gettext('Size')}{direntDetail.size} {gettext('Position')}{position} {gettext('Last Update')}{moment(direntDetail.mtime).format('YYYY-MM-DD')} + {gettext('Tags')} + +
      + {fileTagList.map((fileTag) => { + return ( +
    • + + {fileTag.name} +
    • + ); + })} +
    + + + + { + this.state.isEditFileTagShow && + + }
    ); } diff --git a/frontend/src/components/dirent-detail/dirent-details.js b/frontend/src/components/dirent-detail/dirent-details.js index 977b9e4008..eb9d9952c1 100644 --- a/frontend/src/components/dirent-detail/dirent-details.js +++ b/frontend/src/components/dirent-detail/dirent-details.js @@ -4,12 +4,14 @@ import { seafileAPI } from '../../utils/seafile-api'; import { serviceUrl, repoID } from '../../utils/constants'; import DetailListView from './detail-list-view'; import Repo from '../../models/repo'; +import FileTag from '../../models/file-tag'; import '../../css/dirent-detail.css'; const propTypes = { dirent: PropTypes.object.isRequired, direntPath: PropTypes.string.isRequired, onItemDetailsClose: PropTypes.func.isRequired, + onFileTagChanged: PropTypes.func.isRequired, }; class DirentDetail extends React.Component { @@ -20,6 +22,7 @@ class DirentDetail extends React.Component { direntType: '', direntDetail: '', repo: null, + fileTagList: [], }; } @@ -44,6 +47,14 @@ class DirentDetail extends React.Component { direntDetail: res.data, }); }); + seafileAPI.listFileTags(repoID, direntPath).then(res => { + let fileTagList = []; + res.data.file_tags.forEach(item => { + let file_tag = new FileTag(item); + fileTagList.push(file_tag); + }); + this.setState({fileTagList: fileTagList}); + }); } else { seafileAPI.getDirInfo(repoID, direntPath).then(res => { this.setState({ @@ -75,6 +86,8 @@ class DirentDetail extends React.Component { direntPath={this.props.direntPath} direntType={this.state.direntType} direntDetail={this.state.direntDetail} + fileTagList={this.state.fileTagList} + onFileTagChanged={this.props.onFileTagChanged} /> }
    diff --git a/frontend/src/components/dirent-list-view/dirent-list-item.js b/frontend/src/components/dirent-list-view/dirent-list-item.js index 1fc1d226c5..0597a64b1a 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-item.js +++ b/frontend/src/components/dirent-list-view/dirent-list-item.js @@ -6,6 +6,7 @@ import URLDecorator from '../../utils/url-decorator'; import Toast from '../toast'; import DirentMenu from './dirent-menu'; import DirentRename from './dirent-rename'; +import FileTag from '../../models/file-tag'; const propTypes = { filePath: PropTypes.string.isRequired, @@ -35,11 +36,13 @@ class DirentListItem extends React.Component { highlight: false, isItemMenuShow: false, menuPosition: {top: 0, left: 0 }, + fileTagList: [], }; } componentDidMount() { document.addEventListener('click', this.onItemMenuHide); + this.getFileTag(); } componentWillUnmount() { @@ -304,6 +307,22 @@ class DirentListItem extends React.Component { return path === '/' ? path + dirent.name : path + '/' + dirent.name; } + getFileTag = () => { + if (this.props.dirent.type === 'file' && this.props.dirent.file_tags!== undefined) { + let FileTgas = this.props.dirent.file_tags; + let fileTagList = []; + FileTgas.forEach(item => { + let fileTag = new FileTag(item) + fileTagList.push(fileTag) + }); + this.setState({fileTagList: fileTagList}); + } + } + + componentWillReceiveProps() { + this.getFileTag(); + } + render() { let { dirent } = this.props; return ( @@ -327,6 +346,15 @@ class DirentListItem extends React.Component { {dirent.name} } + +
    + { dirent.type !== 'dir' && this.state.fileTagList.map((fileTag) => { + return ( + + ); + })} +
    + { this.state.isOperationShow && diff --git a/frontend/src/components/dirent-list-view/dirent-list-view.js b/frontend/src/components/dirent-list-view/dirent-list-view.js index 82b62f0146..79dbd2dec5 100644 --- a/frontend/src/components/dirent-list-view/dirent-list-view.js +++ b/frontend/src/components/dirent-list-view/dirent-list-view.js @@ -147,7 +147,8 @@ class DirentListView extends React.Component { - {gettext('Name')} + {gettext('Name')} + {gettext('Size')} {gettext('Last Update')} diff --git a/frontend/src/components/toolbar/path-toolbar.js b/frontend/src/components/toolbar/path-toolbar.js index 6fa1849e62..62f0ed9d9c 100644 --- a/frontend/src/components/toolbar/path-toolbar.js +++ b/frontend/src/components/toolbar/path-toolbar.js @@ -57,7 +57,7 @@ class PathToolbar extends React.Component { return (
      -
    • +
    diff --git a/frontend/src/css/dirent-detail.css b/frontend/src/css/dirent-detail.css index 8d4718ad4e..0c601f65be 100644 --- a/frontend/src/css/dirent-detail.css +++ b/frontend/src/css/dirent-detail.css @@ -85,4 +85,23 @@ font-size: 14px; color: #333; word-break: break-all; -} \ No newline at end of file +} + +.dirent-table-container .file-tag-container th { + vertical-align: top; + list-style: none; +} + +.dirent-table-container .file-tag-container .tag-list { + list-style: none; +} + +.file-tag-list li { + display: flex; + align-items: center; +} + +.file-tag-list .tag-name { + display: inline-block; + margin-left: 0.5rem; +} diff --git a/frontend/src/css/repo-tag.css b/frontend/src/css/repo-tag.css index 05411d7a95..70db87a8e0 100644 --- a/frontend/src/css/repo-tag.css +++ b/frontend/src/css/repo-tag.css @@ -7,9 +7,11 @@ } .tag-list-item { + position: relative; display: flex; justify-content: space-around; margin-bottom: 0.5rem; + height: 2.25rem; } .tag-list-item .tag-demo { @@ -42,7 +44,7 @@ background-color: rgba(9,45,66,.13); } -.tag-create .color-chooser, -.tag-edit .color-chooser { - margin-top: 0.5rem; +.tag-list-item .tag-operation { + position: absolute; + right: 0.5rem; } diff --git a/frontend/src/models/dirent.js b/frontend/src/models/dirent.js index 126b67dcb6..b983e7edd0 100644 --- a/frontend/src/models/dirent.js +++ b/frontend/src/models/dirent.js @@ -18,6 +18,7 @@ class Dirent { this.modifier_name = json.modifier_name; this.modifier_email = json.modifier_email; this.modifier_contact_email = json.modifier_contact_email; + this.file_tags = json.file_tags; } } diff --git a/frontend/src/models/file-tag.js b/frontend/src/models/file-tag.js new file mode 100644 index 0000000000..08cd10abc3 --- /dev/null +++ b/frontend/src/models/file-tag.js @@ -0,0 +1,10 @@ +class FileTag { + constructor(object) { + this.id = object.file_tag_id; + this.repo_tag_id = object.repo_tag_id; + this.name = object.tag_name; + this.color = object.tag_color; + } +} + +export default FileTag; diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js index 55496eedd3..7ec3fb93a7 100644 --- a/frontend/src/pages/repo-wiki-mode/main-panel.js +++ b/frontend/src/pages/repo-wiki-mode/main-panel.js @@ -204,6 +204,10 @@ class MainPanel extends Component { this.setState({isDirentDetailShow: false}); } + onFileTagChanged = () => { + this.updateViewList(this.props.filePath); + } + render() { let filePathList = this.props.filePath.split('/'); let nodePath = ''; @@ -322,6 +326,7 @@ class MainPanel extends Component { dirent={this.state.currentDirent} direntPath={this.state.currentFilePath} onItemDetailsClose={this.onItemDetailsClose} + onFileTagChanged={this.onFileTagChanged} />
    } diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css index 7b94cedcc4..e2fe85ed11 100644 --- a/media/css/seahub_react.css +++ b/media/css/seahub_react.css @@ -76,7 +76,7 @@ .sf2-icon-two-columns:before { content:"\e036"; } .sf2-icon-confirm:before {content:"\e01e"} .sf2-icon-cancel:before {content:"\e01f"} -.sf2-icon-tag-manager:before {content:"\e015"} +.sf2-icon-tag:before {content:"\e037"} /* common class and element style*/ a { color:#eb8205; } @@ -975,6 +975,36 @@ a.op-icon:focus { border-bottom-color:#eb8205; } +/* begin file-tag */ +.tag-list { + position: relative; +} + +.tag-list .file-tag { + margin-left: 0.25rem; + cursor: pointer; +} + +.tag-list .file-tag:first-child { + margin-left: 0; +} + +.tag-list-stacked .file-tag{ + margin-left: -0.5rem; + box-shadow: 0 0 0 2px #fff; + cursor: pointer; +} + +.file-tag { + position: relative; + display: inline-block; + width: 1rem; + height: 1rem; + border-radius: 50%; +} + +/* end file-tag */ + /* begin activity page */ .activity-table-container { flex: 1; @@ -982,3 +1012,8 @@ a.op-icon:focus { overflow: auto; } /* end activity page */ + +.dirent-item.tag-list { + display: flex; + align-items: center; +} diff --git a/media/css/sf_font2/seafile-font2.eot b/media/css/sf_font2/seafile-font2.eot index 9657d0f92ed32f41520cf0729cfdfc97fb69e970..97d03619ad25fca79cce5fb952d53da722404edd 100644 GIT binary patch delta 804 zcmYjPT}YEr7=F(7?R@*TZ=dsTYEJi~Zf4H8sr!}|lxTtdT_h4w#&GKLci;lMC@&&V zkjxEK>_)1KE`=G0P(cM*3Q1Seg?JGm5=0jT!N}fk-gw}gm*;(-^SMo_8=~^f32xxq*gY33v zGXpDxv>J8@SRl*EGA+xlO8wH>#i7lz1ajz!VIZDJ)g+i;h71c7l3(=&r;KqHzEf{HMj)7APG z)?QGe0tKnfGOz zTqC#2H{>Nt(z2O;Axuf>LhEWyI-q=G69QGMXB+GjwUSFObdkKOpH;ZIgLyq6wZXwe zugcwCPoo+N7TF4T$ita!Zav8ioN+tLQ}l~FFLx4Bsbn(IAvqMqdU^gM7lq`krGSYP zJe!SZht8@fG(Z`1hr>^AJ0vCssr3K?sXeDp2>JqnREcYegyXFN%$Ikfv`Ri=_lnsRF-?2DY>T8;YZQ5#pda!pEY~* G*Zc=i2%o0_ delta 531 zcmez4-Q&f^qsqV#qB4=qjOB>x68(t|oyme;QVa|#77PpwGVU&}ZVc%RY(TyQkgwn$ ztZ$^>@6*S?z!(AKhb8AG794O_-p9beqyrT5N>3~<`2U}Q8K_oztSwlKCTwk>SG*rV8+ z*pIN^;$Yx#<2b~{#1+8R!nKI&5Z4W^58PbbTHG<*%ede1MDaB69OAjd%fTzdYr^Zt zI|*o%3~MLn=2*EWjGLG8?cm?MRY8Q2-IPI>!H~gV@&!d*7E@hAgUO6a3xNJuq?EKG~?v$s&eX - @@ -62,4 +61,6 @@ + + diff --git a/media/css/sf_font2/seafile-font2.ttf b/media/css/sf_font2/seafile-font2.ttf index c1e543a211fc7405608d47c76421d12acc0295e4..06592a248d4674c6027d43b9202c3ccce3031803 100644 GIT binary patch delta 776 zcmYLH-%C?r7=FI}_|DGR+1b=JO_$qrZf5Czs5?t5NYX;Gf%R0|?7 zh^!4%w2LnK1B4k!5J7oi*@Z+G5+Mc=v4ZNNi$Jp1dC>>o_wqc?`@ZLU-tW9Wz0#Qm z17N`|=yATIqr2<+r4LVlnp$F=r%twaAO=p@MA&`qLLeNNJ~Iuli-ddn<67d?ZIcbq z1&Mn$tW6|#cBm6-NQP;6Y;tI7cH;$L7yv5fVuRX1QETc4X&-4vVzI%rW@ytHsn|}0 zHx^H(%*)0S!i$7&j*a(g&qr2z0R4BuskoL(;0}I~9wqF)s>KIK^?gHtYn!;W#P~$= z&-`cv@T^izbHN-LjCS5ru2_%$hFM2tz=pDr-;271e1;ukt=Z$Om#NuDtSkGCwWXa1 z6%xtHt{pfFoq#1my90B=fH{hsA_FzuQpq^2E$qcYmVpR8Wf&<>fC(Zbm?6`+6Tn2$QI7@$5Tt8{(TFBA zBLWpswB!=HR@Po;957B9Uvd+V@dx}J|0*a#x3DhS#6B@2E{f~o7xAxDCPk$YX-V2P z$ISQ5>*l=dkn7|&`Lg`jlCiwa{nXzTbG_DOYi`o=nWYW%wIEw#Z^%j!-OxkttOi(h z6?ZYeFQPWOnBZ4=mEYH-M#7~IJCFD{bIhz}n2|H?WW|bpe)H*OQmhaqi4M`FD7Le+ zueo5zJha%EK;nslvU})kib4aFF|#-PoZBTbAxzdo2xWurdPBHA6v}Gu$Mk$U&xj0# sHL+$EVJeH}u6b^-9IqBx_ES{}*C(}MAZztjvX*Rvw_N`J2Q1|O0sG>ZNdN!< delta 518 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