1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-05-12 18:05:05 +00:00

refactor(metadata): remove ui-component ()

1. ModalPortal
2. Icon
3. IconBtn
4. Loading
5. CenteredLoading
6. ClickOutside
7. SearchInput
8. Switch
9. CustomizeAddTool
10. SfCalendar
11. SfFilterCalendar
12. CustomizeSelect
13. CustomizePopover
14. FieldDisplaySettings
15. Formatters
16. remove duplicate codes
This commit is contained in:
Jerry Ren 2025-03-01 10:12:48 +08:00 committed by GitHub
parent 67083238c2
commit 890880a5c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
281 changed files with 3523 additions and 1271 deletions
frontend
package-lock.jsonpackage.json
src
components
css
metadata/components

View File

@ -14,12 +14,19 @@
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@gatsbyjs/reach-router": "2.0.1",
<<<<<<< HEAD
"@seafile/react-image-lightbox": "4.0.2",
=======
"@seafile/react-image-lightbox": "4.0.1",
>>>>>>> 1997d1d4b (refactor(metadata): remove ui-component)
"@seafile/resumablejs": "1.1.16",
"@seafile/sdoc-editor": "2.0.11",
"@seafile/seafile-calendar": "0.0.28",
"@seafile/seafile-editor": "2.0.0",
<<<<<<< HEAD
"@seafile/sf-metadata-ui-component": "^1.0.2",
=======
>>>>>>> 1997d1d4b (refactor(metadata): remove ui-component)
"@seafile/stldraw-editor": "1.0.0",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/codemirror-themes": "^4.23.5",
@ -3244,23 +3251,6 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
},
"node_modules/@formatjs/intl-unified-numberformat": {
"version": "3.3.7",
"resolved": "https://registry.npmmirror.com/@formatjs/intl-unified-numberformat/-/intl-unified-numberformat-3.3.7.tgz",
"integrity": "sha512-KnWgLRHzCAgT9eyt3OS34RHoyD7dPDYhRcuKn+/6Kv2knDF8Im43J6vlSW6Hm1w63fNq3ZIT1cFk7RuVO3Psag==",
"deprecated": "We have renamed the package to @formatjs/intl-numberformat",
"license": "MIT",
"dependencies": {
"@formatjs/intl-utils": "^2.3.0"
}
},
"node_modules/@formatjs/intl-utils": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz",
"integrity": "sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==",
"deprecated": "the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package",
"license": "MIT"
},
"node_modules/@gatsbyjs/reach-router": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@gatsbyjs/reach-router/-/reach-router-2.0.1.tgz",
@ -5567,6 +5557,7 @@
"url": "https://opencollective.com/unified"
}
},
<<<<<<< HEAD
"node_modules/@seafile/sf-metadata-ui-component": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-1.0.2.tgz",
@ -5633,6 +5624,8 @@
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
=======
>>>>>>> 1997d1d4b (refactor(metadata): remove ui-component)
"node_modules/@seafile/slate": {
"version": "0.91.8",
"resolved": "https://registry.npmmirror.com/@seafile/slate/-/slate-0.91.8.tgz",
@ -8849,12 +8842,6 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bowser": {
"version": "1.9.4",
"resolved": "https://registry.npmmirror.com/bowser/-/bowser-1.9.4.tgz",
"integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -10235,16 +10222,6 @@
"postcss": "^8.4"
}
},
"node_modules/css-in-js-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
"license": "MIT",
"dependencies": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
}
},
"node_modules/css-loader": {
"version": "6.11.0",
"resolved": "https://registry.npmmirror.com/css-loader/-/css-loader-6.11.0.tgz",
@ -11729,6 +11706,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
@ -13583,19 +13561,6 @@
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/glamor": {
"version": "2.20.40",
"resolved": "https://registry.npmmirror.com/glamor/-/glamor-2.20.40.tgz",
"integrity": "sha512-DNXCd+c14N9QF8aAKrfl4xakPk5FdcFwmH7sD0qnC0Pr7xoZ5W9yovhUrY/dJc3psfGGXC58vqQyRtuskyUJxA==",
"license": "MIT",
"dependencies": {
"fbjs": "^0.8.12",
"inline-style-prefixer": "^3.0.6",
"object-assign": "^4.1.1",
"prop-types": "^15.5.10",
"through": "^2.3.8"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
@ -15161,16 +15126,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/inline-style-prefixer": {
"version": "3.0.8",
"resolved": "https://registry.npmmirror.com/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz",
"integrity": "sha512-ne8XIyyqkRaNJ1JfL1NYzNdCNxq+MCBQhC8NgOQlzNm2vv3XxlP0VSLQUbSRCF6KPEoveCVEpayHoHzcMyZsMQ==",
"license": "MIT",
"dependencies": {
"bowser": "^1.7.3",
"css-in-js-utils": "^2.0.0"
}
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
@ -15185,32 +15140,6 @@
"node": ">= 0.4"
}
},
"node_modules/intl-format-cache": {
"version": "4.3.1",
"resolved": "https://registry.npmmirror.com/intl-format-cache/-/intl-format-cache-4.3.1.tgz",
"integrity": "sha512-OEUYNA7D06agqPOYhbTkl0T8HA3QKSuwWh1HiClEnpd9vw7N+3XsQt5iZ0GUEchp5CW1fQk/tary+NsbF3yQ1Q==",
"license": "BSD-3-Clause"
},
"node_modules/intl-messageformat": {
"version": "7.8.4",
"resolved": "https://registry.npmmirror.com/intl-messageformat/-/intl-messageformat-7.8.4.tgz",
"integrity": "sha512-yS0cLESCKCYjseCOGXuV4pxJm/buTfyCJ1nzQjryHmSehlptbZbn9fnlk1I9peLopZGGbjj46yHHiTAEZ1qOTA==",
"license": "BSD-3-Clause",
"dependencies": {
"intl-format-cache": "^4.2.21",
"intl-messageformat-parser": "^3.6.4"
}
},
"node_modules/intl-messageformat-parser": {
"version": "3.6.4",
"resolved": "https://registry.npmmirror.com/intl-messageformat-parser/-/intl-messageformat-parser-3.6.4.tgz",
"integrity": "sha512-RgPGwue0mJtoX2Ax8EmMzJzttxjnva7gx0Q7mKJ4oALrTZvtmCeAw5Msz2PcjW4dtCh/h7vN/8GJCxZO1uv+OA==",
"deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/intl-unified-numberformat": "^3.2.0"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz",
@ -27547,12 +27476,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/thunky/-/thunky-1.1.0.tgz",

View File

@ -14,7 +14,6 @@
"@seafile/sdoc-editor": "2.0.11",
"@seafile/seafile-calendar": "0.0.28",
"@seafile/seafile-editor": "2.0.0",
"@seafile/sf-metadata-ui-component": "^1.0.2",
"@seafile/stldraw-editor": "1.0.0",
"@uiw/codemirror-extensions-langs": "^4.19.4",
"@uiw/codemirror-themes": "^4.23.5",

View File

@ -0,0 +1,7 @@
.sf-centered-loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Loading from '../loading';
import './index.css';
function CenteredLoading(props) {
return (
<div className={classnames('sf-centered-loading', props.className)}>
<Loading />
</div>
);
}
CenteredLoading.propTypes = {
className: PropTypes.string,
};
export default CenteredLoading;

View File

@ -1,26 +1,30 @@
.add-item-btn {
cursor: pointer;
display: flex;
align-items: center;
height: 40px;
font-size: 14px;
font-weight: 500;
border-top: 1px solid #dedede;
background: #fff;
padding: 0 1rem;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
position: relative;
height: 30px;
padding: 0 10px;
overflow: hidden;
}
.add-item-btn:hover {
background-color: #f5f5f5;
cursor: pointer;
background: #f5f5f5;
}
.add-item-btn .add-new-option {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.add-item-btn .seafile-multicolor-icon-add-table {
margin-right: 10px;
font-size: 12px;
font-weight: 600;
transform: none;
fill: #666;
}
.add-item-btn .description {
flex: 1;
}

View File

@ -1,13 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../../css/common-add-tool.css';
import Icon from '../icon';
function CommonAddTool(props) {
const { callBack, footerName, className, addIconClassName } = props;
import './index.css';
function CommonAddTool({ callBack, footerName, className, addIconClassName, hideIcon, style }) {
return (
<div className={`add-item-btn ${className ? className : ''}`} onClick={(e) => {callBack(e);}}>
<span className={`sf3-font sf3-font-enlarge mr-2 ${addIconClassName || ''}`}></span>
<span className='add-new-option' title={footerName}>{footerName}</span>
<div className={`add-item-btn ${className ? className : ''}`} style={style} onClick={(e) => {callBack(e);}}>
{!hideIcon && <Icon symbol="add-table" className={addIconClassName} />}
<span className="description text-truncate">{footerName}</span>
</div>
);
}

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import SearchInput from '../search-input';
import SearchInput from '../../search-input';
import ClickOutside from '../../click-outside';
import Option from './option';
import KeyCodes from '../../../constants/keyCodes';
import ClickOutside from './click-outside';
import './select-option-group.css';

View File

@ -6,9 +6,8 @@ import DirOperationToolbar from '../../components/toolbar/dir-operation-toolbar'
import MetadataViewName from '../../metadata/components/metadata-view-name';
import TagViewName from '../../tag/components/tag-view-name';
import { siteRoot, gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import { debounce, Utils } from '../../utils/utils';
import { PRIVATE_FILE_TYPE } from '../../constants';
import { debounce } from '../../metadata/utils/common';
import { EVENT_BUS_TYPE } from '../../metadata/constants';
import { ALL_TAGS_ID } from '../../tag/constants';

View File

@ -4,7 +4,7 @@ import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap
import { gettext, enableFileTags } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import TextTranslation from '../../utils/text-translation';
import SeahubPopover from '../common/seahub-popover';
import CustomizePopover from '../customize-popover';
import ListTagPopover from '../popover/list-tag-popover';
import ViewModes from '../../components/view-modes';
import SortMenu from '../../components/sort-menu';
@ -164,12 +164,11 @@ class DirTool extends React.Component {
}
</div>
{this.state.isRepoTagDialogOpen &&
<SeahubPopover
<CustomizePopover
popoverClassName="list-tag-popover"
target="cur-folder-more-op-toggle"
hideSeahubPopover={this.hidePopover}
hideSeahubPopoverWithEsc={this.hidePopover}
canHideSeahubPopover={true}
hidePopover={this.hidePopover}
hidePopoverWithEsc={this.hidePopover}
boundariesElement={document.body}
placement={'bottom-end'}
>
@ -177,7 +176,7 @@ class DirTool extends React.Component {
repoID={repoID}
onListTagCancel={this.toggleCancel}
/>
</SeahubPopover>
</CustomizePopover>
}
</React.Fragment>
);

View File

@ -1,7 +1,8 @@
import React from 'react';
import { Popover } from 'reactstrap';
import PropTypes from 'prop-types';
import { KeyCodes } from '../../constants';
import { KeyCodes } from '../constants';
import { getEventClassName } from '../utils/dom';
const propTypes = {
target: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
@ -9,17 +10,17 @@ const propTypes = {
innerClassName: PropTypes.string,
popoverClassName: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
hideSeahubPopover: PropTypes.func.isRequired,
hideSeahubPopoverWithEsc: PropTypes.func,
hidePopover: PropTypes.func.isRequired,
hidePopoverWithEsc: PropTypes.func,
hideArrow: PropTypes.bool,
canHideSeahubPopover: PropTypes.bool,
canHidePopover: PropTypes.bool,
placement: PropTypes.string,
modifiers: PropTypes.object
};
class SeahubPopover extends React.Component {
class CustomizePopover extends React.Component {
SeahubPopoverRef = null;
popoverRef = null;
isSelectOpen = false;
componentDidMount() {
@ -32,28 +33,22 @@ class SeahubPopover extends React.Component {
document.removeEventListener('keydown', this.onKeyDown);
}
getEventClassName = (e) => {
// svg mouseEvent event.target.className is an object
if (!e || !e.target) return '';
return e.target.getAttribute('class') || '';
};
onKeyDown = (e) => {
const { canHideSeahubPopover, hideSeahubPopoverWithEsc } = this.props;
if (e.keyCode === KeyCodes.Escape && typeof hideSeahubPopoverWithEsc === 'function' && !this.isSelectOpen) {
const { canHidePopover, hidePopoverWithEsc } = this.props;
if (e.keyCode === KeyCodes.Escape && typeof hidePopoverWithEsc === 'function' && !this.isSelectOpen) {
e.preventDefault();
hideSeahubPopoverWithEsc();
hidePopoverWithEsc();
} else if (e.keyCode === KeyCodes.Enter) {
// Resolve the default behavior of the enter key when entering formulas is blocked
if (canHideSeahubPopover) return;
if (canHidePopover) return;
e.stopImmediatePropagation();
}
};
onMouseDown = (e) => {
if (!this.props.canHideSeahubPopover) return;
if (this.SeahubPopoverRef && e && this.getEventClassName(e).indexOf('popover') === -1 && !this.SeahubPopoverRef.contains(e.target)) {
this.props.hideSeahubPopover(e);
if (!this.props.canHidePopover) return;
if (this.popoverRef && e && getEventClassName(e).indexOf('popover') === -1 && !this.popoverRef.contains(e.target)) {
this.props.hidePopover(e);
}
};
@ -82,7 +77,7 @@ class SeahubPopover extends React.Component {
modifiers={modifiers}
{...additionalProps}
>
<div ref={ref => this.SeahubPopoverRef = ref} onClick={this.onPopoverInsideClick}>
<div ref={ref => this.popoverRef = ref} onClick={this.onPopoverInsideClick}>
{this.props.children}
</div>
</Popover>
@ -90,12 +85,12 @@ class SeahubPopover extends React.Component {
}
}
SeahubPopover.defaultProps = {
CustomizePopover.defaultProps = {
placement: 'bottom-start',
hideArrow: true,
canHideSeahubPopover: true
canHidePopover: true
};
SeahubPopover.propTypes = propTypes;
CustomizePopover.propTypes = propTypes;
export default SeahubPopover;
export default CustomizePopover;

View File

@ -0,0 +1,100 @@
.seafile-customize-select {
position: relative;
display: flex;
padding: 0 10px;
border-radius: 3px;
align-items: center;
justify-content: space-between;
max-width: 900px;
user-select: none;
text-align: left;
line-height: 1.5;
background-image: none;
font-size: 14px;
color: #212529;
}
.seafile-customize-select:focus,
.seafile-customize-select.focus {
border-color: #1991eb !important;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
}
.seafile-customize-select.disabled:focus,
.seafile-customize-select.focus.disabled,
.seafile-customize-select.disabled:hover {
border-color: rgba(0, 40, 100, 0.12) !important;
box-shadow: unset;
cursor: default;
}
.seafile-customize-select:hover {
cursor: pointer;
border-color: rgb(179, 179, 179);
}
.seafile-customize-select .sf3-font-down {
color: #999;
}
.seafile-customize-select .selected-option {
display: flex;
flex: 1;
overflow: hidden;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
background: #fff;
}
.seafile-customize-select .selected-option .custom-select-dropdown-icon {
height: 12px;
width: 12px;
color: #999;
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.5rem;
}
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content,
.seafile-customize-select.selector-group .seafile-option-group .seafile-option-group-content {
padding: 10px;
}
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content {
padding: 10px 0;
}
.seafile-customize-select.selector-collaborator .option {
padding: 5px 0 5px 10px !important;
line-height: 20px;
}
.seafile-customize-select.selector-group .option {
height: 30px;
display: flex;
align-items: center;
}
.seafile-customize-select.selector-group .select-group-option {
justify-content: space-between;
}
.seafile-customize-select.selector-group .selected-option .selected-group {
padding: 0 2px;
background: #eceff4;
border-radius: 3px;
}
.seafile-customize-select .selected-option-show {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.seafile-customize-select .select-placeholder {
line-height: 1;
font-size: 14px;
white-space: nowrap;
}

View File

@ -0,0 +1,173 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ModalPortal from '../modal-portal';
import SelectOptionGroup from './select-option-group';
import { getEventClassName } from '../../utils/dom';
import './index.css';
class CustomizeSelect extends Component {
constructor(props) {
super(props);
this.state = {
isShowSelectOptions: false
};
}
onSelectToggle = (event) => {
event.preventDefault();
/*
if select is showing, click events do not need to be monitored by other click events,
so it can be closed when other select is clicked.
*/
if (this.state.isShowSelectOptions) event.stopPropagation();
const eventClassName = getEventClassName(event);
if (this.props.readOnly || eventClassName.indexOf('option-search-control') > -1 || eventClassName === 'seafile-option-group-search') return;
// Prevent closing by pressing the space bar in the search input
if (event.target.value === '') return;
this.setState({
isShowSelectOptions: !this.state.isShowSelectOptions
});
};
onClick = (event) => {
if (this.props.isShowSelected && event.target.className.includes('icon-fork-number')) {
return;
}
if (!this.selector.contains(event.target)) {
this.closeSelect();
}
};
closeSelect = () => {
this.setState({ isShowSelectOptions: false });
};
getSelectedOptionTop = () => {
if (!this.selector) return 38;
const { height } = this.selector.getBoundingClientRect();
return height;
};
getFilterOptions = (searchValue) => {
const { options, searchable } = this.props;
if (!searchable) return options || [];
const validSearchVal = searchValue.trim().toLowerCase();
if (!validSearchVal) return options || [];
return options.filter(option => {
const { value, name } = option;
if (typeof name === 'string') {
return name.toLowerCase().indexOf(validSearchVal) > -1;
}
if (typeof value === 'object') {
if (value.column) {
return value.column.name.toLowerCase().indexOf(validSearchVal) > -1;
}
if (value.name) {
return value.name.toLowerCase().indexOf(validSearchVal) > -1;
}
return value.columnOption && value.columnOption.name.toLowerCase().indexOf(validSearchVal) > -1;
}
return false;
});
};
renderDropDownIcon = () => {
const { readOnly, component } = this.props;
if (readOnly) return;
const { DropDownIcon } = component || {};
if (DropDownIcon) {
return (
<div className="custom-select-dropdown-icon">{DropDownIcon}</div>
);
}
return (<i className="sf3-font sf3-font-down" aria-hidden="true"></i>);
};
render() {
const { className, value, options, placeholder, searchable, searchPlaceholder, noOptionsPlaceholder,
readOnly, isInModal, addOptionAble, component } = this.props;
return (
<div
ref={(node) => this.selector = node}
className={classnames('seafile-customize-select custom-select',
{ 'focus': this.state.isShowSelectOptions },
{ 'disabled': readOnly },
className
)}
onClick={this.onSelectToggle}>
<div className="selected-option">
{value && value.label ?
<span className="selected-option-show">{value.label}</span>
:
<span className="select-placeholder">{placeholder}</span>
}
{this.renderDropDownIcon()}
</div>
{this.state.isShowSelectOptions && !isInModal && (
<SelectOptionGroup
value={value}
addOptionAble={addOptionAble}
component={component}
isShowSelected={this.props.isShowSelected}
top={this.getSelectedOptionTop()}
options={options}
onSelectOption={this.props.onSelectOption}
searchable={searchable}
searchPlaceholder={searchPlaceholder}
noOptionsPlaceholder={noOptionsPlaceholder}
onClickOutside={this.onClick}
closeSelect={this.closeSelect}
getFilterOptions={this.getFilterOptions}
supportMultipleSelect={this.props.supportMultipleSelect}
/>
)}
{this.state.isShowSelectOptions && isInModal && (
<ModalPortal>
<SelectOptionGroup
className={className}
value={value}
addOptionAble={addOptionAble}
component={component}
isShowSelected={this.props.isShowSelected}
position={this.selector.getBoundingClientRect()}
isInModal={isInModal}
top={this.getSelectedOptionTop()}
options={options}
onSelectOption={this.props.onSelectOption}
searchable={searchable}
searchPlaceholder={searchPlaceholder}
noOptionsPlaceholder={noOptionsPlaceholder}
onClickOutside={this.onClick}
closeSelect={this.closeSelect}
getFilterOptions={this.getFilterOptions}
supportMultipleSelect={this.props.supportMultipleSelect}
/>
</ModalPortal>
)}
</div>
);
}
}
CustomizeSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.object,
options: PropTypes.array,
placeholder: PropTypes.string,
onSelectOption: PropTypes.func,
readOnly: PropTypes.bool,
searchable: PropTypes.bool,
addOptionAble: PropTypes.bool,
searchPlaceholder: PropTypes.string,
noOptionsPlaceholder: PropTypes.string,
component: PropTypes.object,
supportMultipleSelect: PropTypes.bool,
isShowSelected: PropTypes.bool,
isInModal: PropTypes.bool, // if select component in a modal (option group need ModalPortal to show)
};
export default CustomizeSelect;

View File

@ -0,0 +1,103 @@
.seafile-option-group {
position: absolute;
left: 0;
min-height: 60px;
max-height: 300px;
min-width: 100%;
max-width: 15rem;
padding: 0.5rem 0;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
background: #fff;
border: 1px solid rgba(0, 40, 100, 0.12);
border-radius: 3px;
z-index: 10001;
}
.seafile-option-group .seafile-option-group-search {
width: 100%;
padding: 0 10px 6px 10px;
min-width: 170px;
}
.seafile-option-group-search .form-control {
height: 31px;
}
.seafile-option-group .none-search-result {
height: 100px;
width: 100%;
padding: 10px;
color: #666666;
}
.seafile-option-group .seafile-option-group-content {
max-height: 252px;
overflow-y: auto;
}
.seafile-select-option {
display: block;
width: 100%;
line-height: 24px;
padding: 0.25rem 10px;
clear: both;
font-weight: 400;
text-align: inherit;
background-color: transparent;
border: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.seafile-select-option.seafile-select-option-active {
background-color: #20a0ff;
color: #fff;
cursor: pointer;
}
.seafile-select-option.seafile-select-option-active .select-option-name {
color: #fff;
}
.seafile-select-option:hover .header-icon .seafile-multicolor-icon,
.seafile-select-option.seafile-select-option-active .header-icon .seafile-multicolor-icon {
fill: #fff;
}
.seafile-select-option:not(.seafile-select-option-active):hover .header-icon .seafile-multicolor-icon {
fill: #aaa;
}
.seafile-select-option .select-option-name .single-select-option {
margin: 0 0 0 12px;
}
.seafile-select-option .select-option-name .multiple-select-option {
margin: 0;
}
.seafile-option-group-selector-single-select .select-option-name,
.seafile-option-group-selector-multiple-select .multiple-option-name {
display: flex;
align-items: center;
justify-content: space-between;
}
.seafile-option-group-selector-multiple-select .multiple-check-icon {
display: inline-flex;
width: 20px;
text-align: center;
}
.seafile-option-group-selector-multiple-select .multiple-check-icon .seafile-multicolor-icon-check-mark {
font-size: 12px;
color: #798d99;
}
.seafile-option-group-selector-single-select .seafile-select-option:hover,
.seafile-option-group-selector-single-select .seafile-select-option.seafile-select-option-active,
.seafile-option-group-selector-multiple-select .seafile-select-option:hover,
.seafile-option-group-selector-multiple-select .seafile-select-option.seafile-select-option-active {
background-color: #f5f5f5;
}

View File

@ -0,0 +1,233 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ClickOutside from '../../click-outside';
import SearchInput from '../../search-input';
import Option from './option';
import { KeyCodes } from '../../../constants';
import './index.css';
const OPTION_HEIGHT = 32;
class SelectOptionGroup extends Component {
constructor(props) {
super(props);
this.state = {
searchVal: '',
activeIndex: -1,
disableHover: false,
};
this.filterOptions = null;
this.timer = null;
}
componentDidMount() {
window.addEventListener('keydown', this.onHotKey);
setTimeout(() => {
this.resetMenuStyle();
}, 1);
}
componentWillUnmount() {
this.filterOptions = null;
this.timer && clearTimeout(this.timer);
window.removeEventListener('keydown', this.onHotKey);
}
resetMenuStyle = () => {
const { isInModal, position } = this.props;
const { top, height } = this.optionGroupRef.getBoundingClientRect();
if (isInModal) {
if (position.y + position.height + height > window.innerHeight) {
this.optionGroupRef.style.top = (position.y - height) + 'px';
}
this.optionGroupRef.style.opacity = 1;
}
else {
if (height + top > window.innerHeight) {
const borderWidth = 2;
this.optionGroupRef.style.top = -1 * (height + borderWidth) + 'px';
}
}
};
onHotKey = (event) => {
const keyCode = event.keyCode;
if (keyCode === KeyCodes.UpArrow) {
this.onPressUp();
} else if (keyCode === KeyCodes.DownArrow) {
this.onPressDown();
} else if (keyCode === KeyCodes.Enter) {
let option = this.filterOptions && this.filterOptions[this.state.activeIndex];
if (option) {
this.props.onSelectOption(option.value);
if (!this.props.supportMultipleSelect) {
this.props.closeSelect();
}
}
} else if (keyCode === KeyCodes.Tab || keyCode === KeyCodes.Escape) {
this.props.closeSelect();
}
};
onPressUp = () => {
if (this.state.activeIndex > 0) {
this.setState({ activeIndex: this.state.activeIndex - 1 }, () => {
this.scrollContent();
});
}
};
onPressDown = () => {
if (this.filterOptions && this.state.activeIndex < this.filterOptions.length - 1) {
this.setState({ activeIndex: this.state.activeIndex + 1 }, () => {
this.scrollContent();
});
}
};
onMouseDown = (e) => {
const { isInModal } = this.props;
// prevent event propagation when click option or search input
if (isInModal) {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}
};
scrollContent = () => {
const { offsetHeight, scrollTop } = this.optionGroupContentRef;
this.setState({ disableHover: true });
this.timer = setTimeout(() => {
this.setState({ disableHover: false });
}, 500);
if (this.state.activeIndex * OPTION_HEIGHT === 0) {
this.optionGroupContentRef.scrollTop = 0;
return;
}
if (this.state.activeIndex * OPTION_HEIGHT < scrollTop) {
this.optionGroupContentRef.scrollTop = scrollTop - OPTION_HEIGHT;
}
else if (this.state.activeIndex * OPTION_HEIGHT > offsetHeight + scrollTop) {
this.optionGroupContentRef.scrollTop = scrollTop + OPTION_HEIGHT;
}
};
changeIndex = (index) => {
this.setState({ activeIndex: index });
};
onChangeSearch = (searchVal) => {
let value = searchVal || '';
if (value !== this.state.searchVal) {
this.setState({ searchVal: value, activeIndex: -1, });
}
};
renderOptGroup = (searchVal) => {
let { noOptionsPlaceholder, onSelectOption } = this.props;
this.filterOptions = this.props.getFilterOptions(searchVal);
if (this.filterOptions.length === 0) {
return (
<div className="none-search-result">{noOptionsPlaceholder}</div>
);
}
return this.filterOptions.map((opt, i) => {
let key = opt.value.column ? opt.value.column.key : i;
let isActive = this.state.activeIndex === i;
return (
<Option
key={key}
index={i}
isActive={isActive}
value={opt.value}
onSelectOption={onSelectOption}
changeIndex={this.changeIndex}
supportMultipleSelect={this.props.supportMultipleSelect}
disableHover={this.state.disableHover}
>
{opt.label}
</Option>
);
});
};
render() {
const { searchable, searchPlaceholder, top, left, minWidth, value, isShowSelected, isInModal, position,
className, addOptionAble, component } = this.props;
const { AddOption } = component || {};
let { searchVal } = this.state;
let style = { top: top || 0, left: left || 0 };
if (minWidth) {
style = { top: top || 0, left: left || 0, minWidth };
}
if (isInModal) {
style = {
position: 'fixed',
left: position.x,
top: position.y + position.height,
minWidth: position.width,
opacity: 0,
};
}
return (
<ClickOutside onClickOutside={this.props.onClickOutside}>
<div
className={classnames('seafile-option-group', className ? 'seafile-option-group-' + className : '', {
'pt-0': isShowSelected,
'create-new-seafile-option-group': addOptionAble,
})}
ref={(ref) => this.optionGroupRef = ref}
style={style}
onMouseDown={this.onMouseDown}
>
{isShowSelected &&
<div className="editor-list-delete mb-2" onClick={(e) => e.stopPropagation()}>{value.label || ''}</div>
}
{searchable && (
<div className="seafile-option-group-search">
<SearchInput
className="option-search-control"
placeholder={searchPlaceholder}
onChange={this.onChangeSearch}
autoFocus={true}
/>
</div>
)}
<div className="seafile-option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
{this.renderOptGroup(searchVal)}
</div>
{addOptionAble && AddOption}
</div>
</ClickOutside>
);
}
}
SelectOptionGroup.propTypes = {
top: PropTypes.number,
left: PropTypes.number,
minWidth: PropTypes.number,
options: PropTypes.array,
onSelectOption: PropTypes.func,
searchable: PropTypes.bool,
addOptionAble: PropTypes.bool,
component: PropTypes.object,
searchPlaceholder: PropTypes.string,
noOptionsPlaceholder: PropTypes.string,
onClickOutside: PropTypes.func.isRequired,
closeSelect: PropTypes.func.isRequired,
getFilterOptions: PropTypes.func.isRequired,
supportMultipleSelect: PropTypes.bool,
value: PropTypes.object,
isShowSelected: PropTypes.bool,
stopClickEvent: PropTypes.bool,
isInModal: PropTypes.bool,
position: PropTypes.object,
className: PropTypes.string,
};
export default SelectOptionGroup;

View File

@ -0,0 +1,50 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
class Option extends Component {
onSelectOption = (value, event) => {
if (this.props.supportMultipleSelect) {
event.stopPropagation();
}
this.props.onSelectOption(value, event);
};
onMouseEnter = () => {
if (!this.props.disableHover) {
this.props.changeIndex(this.props.index);
}
};
onMouseLeave = () => {
if (!this.props.disableHover) {
this.props.changeIndex(-1);
}
};
render() {
return (
<div
className={classnames('seafile-select-option', { 'seafile-select-option-active': this.props.isActive })}
onClick={this.onSelectOption.bind(this, this.props.value)}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>{this.props.children}
</div>
);
}
}
Option.propTypes = {
index: PropTypes.number,
isActive: PropTypes.bool,
changeIndex: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
onSelectOption: PropTypes.func,
supportMultipleSelect: PropTypes.bool,
disableHover: PropTypes.bool,
};
export default Option;

View File

@ -1,12 +1,12 @@
import React, { Fragment, } from 'react';
import PropTypes from 'prop-types';
import { gettext, isOrgContext, username } from '../../utils/constants';
import { Modal, ModalBody } from 'reactstrap';
import { gettext, isOrgContext, username } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api.js';
import { Utils } from '../../utils/utils';
import toaster from '../../components/toast';
import EmptyTip from '../../components/empty-tip';
import Loading from '../../components/loading';
import toaster from '../toast';
import EmptyTip from '../empty-tip';
import Loading from '../loading';
import Department from '../../models/department';
import SeahubModalHeader from '../common/seahub-modal-header';
import DepartmentGroup from './department-detail-widget/department-group';

View File

@ -2,8 +2,8 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Tooltip } from 'reactstrap';
import { gettext, mediaUrl } from '../../../utils/constants';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import EmptyTip from '../../empty-tip';
import Loading from '../../loading';
const ItemPropTypes = {
member: PropTypes.object,

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Loading from '../../../components/loading';
import Loading from '../../loading';
import { isOrgContext } from '../../../utils/constants';
const ItemPropTypes = {

View File

@ -5,7 +5,7 @@ import { Modal, ModalBody } from 'reactstrap';
import { Utils } from '../../utils/utils';
import { gettext, siteRoot, enableRepoSnapshotLabel as showLabel } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import Loading from '../../components/loading';
import Loading from '../loading';
import Paginator from '../../components/paginator';
import ModalPortal from '../../components/modal-portal';
import CommitDetails from '../../components/dialog/commit-details';

View File

@ -4,7 +4,7 @@ import { Link } from '@gatsbyjs/reach-router';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot, isPro, username } from '../../../utils/constants';
import Loading from '../../../components/loading';
import Loading from '../../loading';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import SharePermissionEditor from '../../../components/select-editor/share-permission-editor';

View File

@ -7,7 +7,7 @@ import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { repoShareAdminAPI } from '../../../utils/repo-share-admin-api';
import { gettext, siteRoot } from '../../../utils/constants';
import Loading from '../../../components/loading';
import Loading from '../../loading';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';

View File

@ -7,7 +7,7 @@ import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { repoShareAdminAPI } from '../../../utils/repo-share-admin-api';
import { gettext, siteRoot } from '../../../utils/constants';
import Loading from '../../../components/loading';
import Loading from '../../loading';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog';

View File

@ -4,7 +4,7 @@ import { Link } from '@gatsbyjs/reach-router';
import { Utils } from '../../../utils/utils';
import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot, isPro, username } from '../../../utils/constants';
import Loading from '../../../components/loading';
import Loading from '../../loading';
import toaster from '../../../components/toast';
import EmptyTip from '../../../components/empty-tip';
import SharePermissionEditor from '../../../components/select-editor/share-permission-editor';

View File

@ -6,7 +6,6 @@ import { MODE_TYPE_MAP } from '../../constants';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext } from '../../utils/constants';
import { RepoInfo } from '../../models';
import { ModalPortal } from '@seafile/sf-metadata-ui-component';
import CreateFolder from '../dialog/create-folder-dialog';
const LibraryOption = ({ mode, label, currentMode, onUpdateMode }) => {
@ -193,14 +192,12 @@ class SelectDirentBody extends React.Component {
</ModalFooter>
</Col>
{this.state.showCreateFolderDialog && (
<ModalPortal>
<CreateFolder
parentPath={this.props.selectedPath}
onAddFolder={this.createFolder}
checkDuplicatedName={this.checkDuplicatedName}
addFolderCancel={this.onToggleCreateFolder}
/>
</ModalPortal>
<CreateFolder
parentPath={this.props.selectedPath}
onAddFolder={this.createFolder}
checkDuplicatedName={this.checkDuplicatedName}
addFolderCancel={this.onToggleCreateFolder}
/>
)}
</Row>
);

View File

@ -12,7 +12,7 @@ import { Utils } from '../../utils/utils';
import toaster from '../toast';
import UserSelect from '../user-select';
import { SeahubSelect } from '../common/select';
import Switch from '../common/switch';
import Switch from '../switch';
import '../../css/transfer-dialog.css';
const propTypes = {

View File

@ -16,11 +16,7 @@ const ExtensionPrompts = ({ onExtendedProperties }) => {
onClick={handlePromptsClick}
>
<div className='extension-prompts-icon-wrapper'>
<Icon
symbol={'bell'}
className='extension-prompts-icon'
aria-label={gettext('Bell Icon')}
/>
<Icon symbol="bell" className='extension-prompts-icon' aria-label={gettext('Bell Icon')} />
</div>
<div className='extension-prompts-content'>
<p>

View File

@ -20,14 +20,14 @@
cursor: default;
}
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon {
.dirent-detail-item .dirent-detail-item-name .seafile-multicolor-icon {
margin-right: 6px;
font-size: 14px;
fill: #999;
flex-shrink: 0;
}
.dirent-detail-item .dirent-detail-item-name .sf-metadata-icon-tag {
.dirent-detail-item .dirent-detail-item-name .seafile-multicolor-icon-tag {
position: relative;
top: 1px;
}

View File

@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../icon';
import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/constants';
import './index.css';
@ -15,7 +15,7 @@ const DetailItem = ({ readonly = true, field, className, children }) => {
return (
<div className={classnames('dirent-detail-item', className)}>
<div className="dirent-detail-item-name d-flex">
<div><Icon iconName={icon} /></div>
<div><Icon className="sf-metadata-icon" symbol={icon} /></div>
<span className="dirent-detail-item-name-value">{field.name}</span>
</div>
<div className={classnames('dirent-detail-item-value', { 'editable': !readonly })} >

View File

@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import DetailItem from '../detail-item';
import Formatter from '../../../metadata/components/formatter';
import { CellType } from '../../../metadata/constants';
import { gettext } from '../../../utils/constants';
import { MetadataDetails } from '../../../metadata';

View File

@ -1,14 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import DetailItem from '../../detail-item';
import Collapse from './collapse';
import Formatter from '../../../../metadata/components/formatter';
import { CellType, PRIVATE_COLUMN_KEY } from '../../../../metadata/constants';
import { gettext } from '../../../../utils/constants';
import { Utils } from '../../../../utils/utils';
import { MetadataDetails, useMetadataDetails } from '../../../../metadata';
import ObjectUtils from '../../../../metadata/utils/object-utils';
import ObjectUtils from '../../../../utils/object';
import { getCellValueByColumn, getDateDisplayString, decimalToExposureTime } from '../../../../metadata/utils/cell';
import Collapse from './collapse';
import { useMetadataStatus } from '../../../../hooks';
import { CAPTURE_INFO_SHOW_KEY } from '../../../../constants';
import People from '../../people';

View File

@ -8,7 +8,7 @@ import Dirent from '../../../models/dirent';
import { Detail, Header, Body } from '../detail';
import DirDetails from './dir-details';
import FileDetails from './file-details';
import ObjectUtils from '../../../metadata/utils/object-utils';
import ObjectUtils from '../../../utils/object';
import { MetadataDetailsProvider } from '../../../metadata/hooks';
import { Settings, AI } from '../../../metadata/components/metadata-details';
import { getDirentPath } from './utils';

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import LibDetail from './lib-details';
import DirentDetail from './dirent-details';
import ViewDetails from '../../metadata/components/view-details';
import ObjectUtils from '../../metadata/utils/object-utils';
import ObjectUtils from '../../utils/object';
import { MetadataContext } from '../../metadata';
import { PRIVATE_FILE_TYPE } from '../../constants';
import { METADATA_MODE, TAGS_MODE } from '../dir-view-mode/constants';

View File

@ -1,14 +1,14 @@
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Formatter } from '@seafile/sf-metadata-ui-component';
import toaster from '../toast';
import Loading from '../loading';
import { Detail, Header, Body } from './detail';
import DetailItem from './detail-item';
import Formatter from '../../metadata/components/formatter';
import { Utils } from '../../utils/utils';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import toaster from '../toast';
import { Detail, Header, Body } from './detail';
import Repo from '../../models/repo';
import Loading from '../loading';
import DetailItem from './detail-item';
import { CellType } from '../../metadata/constants';
const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {

View File

@ -0,0 +1,20 @@
.seafile-multicolor-icon-btn {
display: inline-flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.seafile-multicolor-icon-btn-20 {
height: 20px;
width: 20px;
}
.seafile-multicolor-icon-btn-24 {
height: 24px;
width: 24px;
}
.seafile-multicolor-icon-btn:hover {
cursor: pointer;
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Icon from '../icon';
import './index.css';
const IconBtn = ({ size = 20, className, iconClassName, CustomIcon, symbol, iconStyle, onClick, ...params }) => {
return (
<div className={classnames('seafile-multicolor-icon-btn', `seafile-multicolor-icon-btn-${size}`, className)} onClick={onClick ? onClick : () => {}} { ...params }>
{CustomIcon ? CustomIcon : <Icon symbol={symbol} className={iconClassName} style={iconStyle} />}
</div>
);
};
IconBtn.propTypes = {
customIcon: PropTypes.string,
symbol: PropTypes.string,
size: PropTypes.number,
className: PropTypes.string,
iconClassName: PropTypes.string,
iconStyle: PropTypes.object,
onClick: PropTypes.func,
};
export default IconBtn;

View File

@ -1,5 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import '../css/icon.css';
const importAll = (requireContext) => {
@ -13,10 +15,10 @@ try {
}
const Icon = (props) => {
const { className, symbol } = props;
const iconClass = `seafile-multicolor-icon seafile-multicolor-icon-${symbol} ${className || ''}`;
const { className, symbol, style } = props;
const iconClass = classnames('seafile-multicolor-icon', className, `seafile-multicolor-icon-${symbol}`);
return (
<svg className={iconClass}>
<svg className={iconClass} style={style}>
<use xlinkHref={`#${symbol}`} />
</svg>
);
@ -25,6 +27,7 @@ const Icon = (props) => {
Icon.propTypes = {
symbol: PropTypes.string.isRequired,
className: PropTypes.string,
style: PropTypes.object,
};
export default Icon;

View File

@ -1,9 +0,0 @@
import React from 'react';
function Loading() {
return (
<span className="loading-icon loading-tip"></span>
);
}
export default Loading;

View File

@ -0,0 +1,4 @@
.loading-tip.center {
display: block;
margin: 0 auto;
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import './index.css';
function Loading({ className }) {
return (
<span className={classnames('loading-icon loading-tip', className)}></span>
);
}
Loading.propTypes = {
className: PropTypes.string,
};
export default Loading;

View File

@ -4,9 +4,9 @@ import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import CommonAddTool from '../common/common-add-tool';
import SearchInput from '../common/search-input';
import SeahubPopover from '../common/seahub-popover';
import CommonAddTool from '../common-add-tool';
import SearchInput from '../search-input';
import CustomizePopover from '../customize-popover';
import TagItem from './tag-item';
import { KeyCodes, TAG_COLORS } from '../../constants';
@ -147,12 +147,11 @@ class EditFileTagPopover extends React.Component {
}
return (
<SeahubPopover
<CustomizePopover
popoverClassName="edit-filetag-popover"
target={this.props.target}
hideSeahubPopover={this.props.toggleCancel}
hideSeahubPopoverWithEsc={this.props.toggleCancel}
canHideSeahubPopover={true}
hidePopover={this.props.toggleCancel}
hidePopoverWithEsc={this.props.toggleCancel}
>
<SearchInput
className="edit-filetag-popover-input"
@ -187,7 +186,7 @@ class EditFileTagPopover extends React.Component {
footerName={`${gettext('Create a new tag')} '${searchText}'`}
/>
}
</SeahubPopover>
</CustomizePopover>
);
}
}

View File

@ -1,7 +1,7 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Utils } from '../../utils/utils';
import { Utils } from '../utils/utils';
const propTypes = {
placeholder: PropTypes.string,

View File

@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
import ClickOutside from '../../../click-outside';
import Editor from './editor';
import { EDITOR_CONTAINER as Z_INDEX_EDITOR_CONTAINER } from '../../constants/z-index';
import { Utils } from '../../../../utils/utils';
import { getEventClassName } from '../../utils';
import { getEventClassName } from '../../../../utils/dom';
import { checkCellValueChanged } from '../../utils/cell-comparer';
import { getCellValueByColumn } from '../../utils/cell';
import { isCtrlKeyHeldDown, isKeyPrintable } from '../../../../utils/keyboard-utils';

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
import ClickOutside from '../../../click-outside';
import Editor from './editor';
import { Utils } from '../../../../utils/utils';
import { EDITOR_CONTAINER as Z_INDEX_EDITOR_CONTAINER } from '../../constants/z-index';

View File

@ -50,7 +50,7 @@
left: 2px;
}
.sf-metadata-delete-select-tags .sf-metadata-delete-select-remove .sf-metadata-icon-x-01 {
.sf-metadata-delete-select-tags .sf-metadata-delete-select-remove .seafile-multicolor-icon-x-01 {
fill: #666;
font-size: 12px;
}

View File

@ -1,6 +1,6 @@
import React from './index';
import PropTypes from 'prop-types';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import IconBtn from '../../../../icon-btn';
import { getTagColor, getTagName } from '../../../../../tag/utils/cell';
import { getRowById } from '../../../utils/table';
@ -18,7 +18,7 @@ const DeleteTag = ({ value, tagsTable, onDelete }) => {
<div className="sf-metadata-delete-select-tag" key={tagId}>
<div className="sf-metadata-delete-select-tag-color" style={{ backgroundColor: tagColor }}></div>
<div className="sf-metadata-delete-select-tag-name">{tagName}</div>
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(tagId, event)} iconName="x-01" />
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(tagId, event)} symbol="x-01" />
</div>
);
})}

View File

@ -1,7 +1,9 @@
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
import CommonAddTool from '../../../common-add-tool';
import SearchInput from '../../../search-input';
import Icon from '../../../icon';
import DeleteTags from './delete-tags';
import { Utils } from '../../../../utils/utils';
import { KeyCodes } from '../../../../constants';
@ -219,7 +221,7 @@ const TagsEditor = ({
<div className="sf-metadata-tag-name">{tagName}</div>
</div>
<div className="sf-metadata-tags-editor-tag-check-icon">
{isSelected && (<Icon iconName="check-mark" />)}
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
</div>
</div>
</div>
@ -244,7 +246,7 @@ const TagsEditor = ({
{renderOptions()}
</div>
{isShowCreateBtn && (
<CustomizeAddTool
<CommonAddTool
callBack={createTag}
footerName={`${gettext('Add tag')} ${searchValue}`}
className="add-search-result"

View File

@ -271,31 +271,6 @@
border-left-color: #303133;
}
.add-item-btn {
display: flex;
align-items: center;
height: 40px;
font-size: 14px;
font-weight: 500;
border-top: 1px solid #dedede;
background: #fff;
padding: 0 1rem;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.add-item-btn:hover {
cursor: pointer;
background: #f5f5f5;
}
.add-item-btn .sf-metadata-icon-add-table {
margin-right: 10px;
font-size: 12px;
font-weight: 600;
transform: translateY(1px);
}
.formula-formatter-content-item {
margin-right: 10px;
font-size: 13px;

View File

@ -1,7 +1,7 @@
import React, { useMemo, useState } from 'react';
import { gettext } from '../../../utils/constants';
import { KeyCodes } from '../../../constants';
import { isModG, isModShiftG } from '../../../metadata/utils/hotkey';
import { isModG, isModShiftG } from '../../../utils/hotkey';
import SFTableSearcherInput from './searcher-input';
import { checkHasSearchResult } from '../utils/search';

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Loading } from '@seafile/sf-metadata-ui-component';
import Loading from '../../../loading';
import toaster from '../../../toast';
import LoadAllTip from '../load-all-tip';
import { RecordMetrics } from '../../utils/record-metrics';
@ -8,7 +8,7 @@ import { TreeMetrics } from '../../utils/tree-metrics';
import { gettext } from '../../../../utils/constants';
import { CANVAS_RIGHT_INTERVAL } from '../../constants/grid';
import { GRID_FOOTER as Z_INDEX_GRID_FOOTER } from '../../constants/z-index';
import { addClassName, removeClassName } from '../../utils';
import { addClassName, removeClassName } from '../../../../utils/dom';
import { getRecordsFromSelectedRange } from '../../utils/selected-cell-utils';
import './index.css';
@ -151,7 +151,7 @@ class RecordsFooter extends React.Component {
{isLoadingMoreRecords &&
<span className="loading-message ml-4">
<span className="mr-2">{gettext('Loading')}</span>
<Loading />
<Loading className="sf-metadata-loading-tip center" />
</span>
}
</div>

View File

@ -2,7 +2,7 @@
fill: #aaa;
}
.sf-table-header-cell .sf-metadata-icon-drop-down {
.sf-table-header-cell .seafile-multicolor-icon-drop-down {
fill: #aaa;
font-size: 12px;
transform: scale(.8);

View File

@ -2,7 +2,7 @@ import React, { useRef, useCallback, useMemo, isValidElement, useState } from 'r
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { UncontrolledTooltip } from 'reactstrap';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../../../icon';
import ResizeColumnHandle from '../resize-column-handle';
import HeaderDropdownMenu from '../dropdown-menu';
import EventBus from '../../../../common/event-bus';
@ -157,7 +157,7 @@ const Cell = ({
return (
<>
<span className="mr-2" id={`header-icon-${key}`}>
{icon_name && <Icon iconName={icon_name} className="sf-table-column-icon" />}
{icon_name && <Icon symbol={icon_name} className="sf-metadata-icon sf-table-column-icon" />}
</span>
{icon_tooltip &&
<UncontrolledTooltip placement="bottom" target={`header-icon-${key}`} fade={false} trigger="hover" className="sf-table-tooltip">

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { UncontrolledTooltip, DropdownItem } from 'reactstrap';
import classnames from 'classnames';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../../../icon';
const ColumnDropdownItem = ({
disabled = false,
@ -32,7 +32,7 @@ const ColumnDropdownItem = ({
if (!disabled) {
return (
<DropdownItem id={target} onClick={onChange} onMouseEnter={onMouseEnter} className={className}>
<Icon iconName={iconName} />
<Icon symbol={iconName} />
<span className="item-text">{title}</span>
</DropdownItem>
);
@ -47,7 +47,7 @@ const ColumnDropdownItem = ({
onMouseEnter={onMouseEnter}
id={target}
>
<Icon iconName={iconName} />
<Icon symbol={iconName} />
<span className="item-text">{title}</span>
{isShowToolTip && (
<UncontrolledTooltip placement="right" target={target} fade={false} delay={{ show: 0, hide: 0 }} className="sf-table-tooltip">

View File

@ -1,10 +1,10 @@
.sf-table-dropdown-menu .dropdown-item .sf-metadata-icon {
.sf-table-dropdown-menu .dropdown-item .seafile-multicolor-icon {
margin-right: 10px;
font-size: 14px;
fill: #666;
}
.sf-table-dropdown-menu .dropdown-item:hover .sf-metadata-icon {
.sf-table-dropdown-menu .dropdown-item:hover .seafile-multicolor-icon {
fill: #fff;
}
@ -32,6 +32,6 @@
color: #c2c2c2;
}
.sf-table-dropdown-menu .disabled.dropdown-item .sf-metadata-icon {
.sf-table-dropdown-menu .disabled.dropdown-item .seafile-multicolor-icon {
fill: #c2c2c2;
}

View File

@ -1,7 +1,7 @@
import React, { useState, useCallback, cloneElement } from 'react';
import PropTypes from 'prop-types';
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
import { ModalPortal } from '@seafile/sf-metadata-ui-component';
import ModalPortal from '../../../../modal-portal';
import { gettext } from '../../../../../utils/constants';
import { isMobile } from '../../../../../utils/utils';

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, cloneElement } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../../../icon';
import { isEnter } from '../../../../../utils/hotkey';
import './index.css';
@ -45,7 +45,7 @@ const InsertColumn = ({ lastColumn, height, groupOffsetLeft, NewColumnComponent,
<>
<div className="sf-table-header-cell">
<div className="sf-table-cell column insert-column" style={style} id={id} ref={ref}>
<Icon iconName="add-table" />
<Icon symbol="add-table" />
</div>
</div>
{cloneElement(NewColumnComponent, { target: id, onChange: insertColumn })}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../../icon';
import { gettext } from '../../../../utils/constants';
class SelectAll extends Component {
@ -45,7 +45,7 @@ class SelectAll extends Component {
{isMobile ?
<label className='mobile-select-all-container'>
{isSelectedParts ?
(<Icon iconName="partially-selected" />) :
(<Icon symbol="partially-selected" />) :
(
<>
<input
@ -62,7 +62,7 @@ class SelectAll extends Component {
</label> :
<>
{isSelectedParts ?
(<Icon iconName="partially-selected" />) :
(<Icon symbol="partially-selected" />) :
(
<input
id="select-all-checkbox"

View File

@ -1,6 +1,6 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Loading } from '@seafile/sf-metadata-ui-component';
import Loading from '../../../loading';
import { RightScrollbar } from '../../scrollbar';
import Record from './record';
import InteractionMasks from '../../masks/interaction-masks';
@ -522,14 +522,14 @@ class RecordsBody extends Component {
// add top placeholder
if (upperHeight > 0) {
const style = { height: upperHeight, width: '100%' };
const upperRow = <div key="upper-placeholder" className="d-flex align-items-end" style={style}><Loading /></div>;
const upperRow = <div key="upper-placeholder" className="d-flex align-items-end" style={style}><Loading className="sf-metadata-loading-tip center" /></div>;
shownRecords.unshift(upperRow);
}
// add bottom placeholder
if (belowHeight > 0) {
const style = { height: belowHeight, width: '100%' };
const belowRow = <div key="below-placeholder" style={style}><Loading /></div>;
const belowRow = <div key="below-placeholder" style={style}><Loading className="sf-metadata-loading-tip center" /></div>;
shownRecords.push(belowRow);
}
return shownRecords;

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import IconBtn from '../../../../../icon-btn';
import GroupTitle from './group-title';
import { GROUP_HEADER_HEIGHT } from '../../../../constants/group';
import { GROUP_FROZEN_HEADER as Z_INDEX_GROUP_FROZEN_HEADER } from '../../../../constants/z-index';
@ -32,7 +32,7 @@ class GroupHeaderLeft extends Component {
>
<IconBtn
className={classnames('group-toggle-btn', { 'hide': !isExpanded })}
iconName="drop-down"
symbol="drop-down"
onClick={this.props.onExpandGroupToggle}
/>
<GroupTitle

View File

@ -173,13 +173,13 @@
cursor: pointer;
}
.canvas-groups-rows .group-toggle-btn .sf-metadata-icon-drop-down {
.canvas-groups-rows .group-toggle-btn .seafile-multicolor-icon-drop-down {
font-size: 12px;
fill: #666666;
transition: all .3s;
}
.canvas-groups-rows .group-toggle-btn.hide .sf-metadata-icon-drop-down {
.canvas-groups-rows .group-toggle-btn.hide .seafile-multicolor-icon-drop-down {
transform: rotate(-90deg);
}
@ -389,7 +389,7 @@
-moz-transition-property: none;
}
.canvas-groups-rows .group-title .group-cell-value .sf-metadata-group-title-rate-item .sf-metadata-icon {
.canvas-groups-rows .group-title .group-cell-value .sf-metadata-group-title-rate-item .seafile-multicolor-icon {
font-size: 16px;
fill: inherit;
}

View File

@ -8,7 +8,7 @@ import Record from '../record';
import { isShiftKeyDown } from '../../../../../utils/keyboard-utils';
import { RecordMetrics } from '../../../utils/record-metrics';
import { getColumnScrollPosition, getColVisibleEndIdx, getColVisibleStartIdx } from '../../../utils/records-body-utils';
import { addClassName, removeClassName } from '../../../utils';
import { addClassName, removeClassName } from '../../../../../utils/dom';
import { createGroupMetrics, getGroupRecordByIndex, isNestedGroupRow } from '../../../utils/group-metrics';
import { checkIsColumnSupportDirectEdit, checkIsColumnFrozen, checkIsNameColumn, getColumnByIndex, checkIsColumnEditable } from '../../../utils/column';
import { checkIsCellSupportOpenEditor } from '../../../utils/selected-cell-utils';

View File

@ -16,7 +16,8 @@ import { getVisibleBoundaries } from '../../utils/viewport';
import { getColOverScanEndIdx, getColOverScanStartIdx } from '../../utils/grid';
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
import { isMobile } from '../../../../utils/utils';
import { isWindowsBrowser, isWebkitBrowser, addClassName, removeClassName, getEventClassName } from '../../utils';
import { addClassName, removeClassName, getEventClassName } from '../../../../utils/dom';
import { isWindowsBrowser, isWebkitBrowser } from '../../utils';
import EventBus from '../../../common/event-bus';
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
import { CANVAS_RIGHT_INTERVAL } from '../../constants/grid';

View File

@ -83,7 +83,7 @@
flex-shrink: 0;
}
.sf-table-cell .select-all-checkbox-container .sf-metadata-icon-partially-selected {
.sf-table-cell .select-all-checkbox-container .seafile-multicolor-icon-partially-selected {
cursor: pointer;
font-size: 12px;
fill: #2b76f6;
@ -132,7 +132,7 @@
transform: rotate(45deg);
}
.sf-table-cell .mobile-select-all-container .sf-metadata-icon-partially-selected {
.sf-table-cell .mobile-select-all-container .seafile-multicolor-icon-partially-selected {
font-size: 14px;
line-height: 1;
}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Loading } from '@seafile/sf-metadata-ui-component';
import Loading from '../../../loading';
import { RightScrollbar } from '../../scrollbar';
import InteractionMasks from '../../masks/interaction-masks';
import Record from './record';
@ -628,14 +628,14 @@ class TreeBody extends Component {
// add top placeholder
if (upperHeight > 0) {
const style = { height: upperHeight, width: '100%' };
const upperRow = <div key="upper-placeholder" className="d-flex align-items-end" style={style}><Loading /></div>;
const upperRow = <div key="upper-placeholder" className="d-flex align-items-end" style={style}><Loading className="sf-metadata-loading-tip center" /></div>;
shownNodes.unshift(upperRow);
}
// add bottom placeholder
if (belowHeight > 0) {
const style = { height: belowHeight, width: '100%' };
const belowRow = <div key="below-placeholder" style={style}><Loading /></div>;
const belowRow = <div key="below-placeholder" style={style}><Loading className="sf-metadata-loading-tip center" /></div>;
shownNodes.push(belowRow);
}
return shownNodes;

View File

@ -1,23 +1,3 @@
export const addClassName = (originClassName, targetClassName) => {
const originClassNames = originClassName.split(' ');
if (originClassNames.indexOf(targetClassName) > -1) return originClassName;
return originClassName + ' ' + targetClassName;
};
export const removeClassName = (originClassName, targetClassName) => {
let originClassNames = originClassName.split(' ');
const targetClassNameIndex = originClassNames.indexOf(targetClassName);
if (targetClassNameIndex < 0) return originClassName;
originClassNames.splice(targetClassNameIndex, 1);
return originClassNames.join(' ');
};
export const getEventClassName = (e) => {
// svg mouseEvent event.target.className is an object
if (!e || !e.target) return '';
return e.target.getAttribute('class') || '';
};
/* is weiXin built-in browser */
export const isWeiXinBuiltInBrowser = () => {
const agent = navigator.userAgent.toLowerCase();

View File

@ -4,7 +4,7 @@ import { gettext, siteRoot } from '../../utils/constants';
import EmptyTip from '../empty-tip';
import LinkItem from './link-item';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
import Loading from '../../components/loading';
import Loading from '../loading';
const propTypes = {
shareLinks: PropTypes.array.isRequired,

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import '../../../css/switch.css';
import './index.css';
function Switch({ onChange, checked, placeholder, disabled, className, size, textPosition = 'left', setRef }) {
return (

View File

@ -10,3 +10,7 @@
fill: #444;
color: #bdbdbd;
}
.sf-metadata-icon {
vertical-align: -0.15em;
}

View File

@ -0,0 +1,96 @@
.sf-metadata-rc-calendar .rc-calendar-date {
width: 28px;
height: 28px;
border-radius: 50%;
line-height: 30px;
}
.sf-metadata-rc-calendar table thead tr,
.sf-metadata-rc-calendar .rc-calendar-tbody tr {
height: inherit;
}
.sf-metadata-rc-calendar .rc-calendar-cell {
font-size: 12px;
}
.sf-metadata-rc-calendar .rc-calendar-date-panel {
width: 253px;
}
.sf-metadata-rc-calendar .rc-calendar-selected-day .rc-calendar-date {
background-color: #fcecd9;
}
.sf-metadata-rc-calendar .rc-calendar-date:hover,
.sf-metadata-rc-calendar .rc-calendar-year-panel-year:hover,
.sf-metadata-rc-calendar .rc-calendar-month-panel-cell .rc-calendar-month-panel-month:hover {
background-color: #fcecd9;
}
.sf-metadata-rc-calendar .rc-calendar-selected-date .rc-calendar-date,
.sf-metadata-rc-calendar .rc-calendar-selected-date .rc-calendar-date:hover,
.sf-metadata-rc-calendar .rc-calendar-year-panel-selected-cell .rc-calendar-year-panel-year,
.sf-metadata-rc-calendar .rc-calendar-month-panel-selected-cell .rc-calendar-month-panel-month {
background-color: #f09f3f;
}
.sf-metadata-rc-calendar .rc-calendar-next-year-btn:not([href]):not([tabindex]),
.sf-metadata-rc-calendar .rc-calendar-prev-year-btn:not([href]):not([tabindex]),
.sf-metadata-rc-calendar .rc-calendar-next-month-btn:not([href]):not([tabindex]),
.sf-metadata-rc-calendar .rc-calendar-prev-month-btn:not([href]):not([tabindex]) {
color: #666666;
}
.sf-metadata-rc-calendar .rc-calendar-next-year-btn:not([href]):not([tabindex]):hover,
.sf-metadata-rc-calendar .rc-calendar-prev-year-btn:not([href]):not([tabindex]):hover,
.sf-metadata-rc-calendar .rc-calendar-next-month-btn:not([href]):not([tabindex]):hover,
.sf-metadata-rc-calendar .rc-calendar-prev-month-btn:not([href]):not([tabindex]):hover {
color: #666666;
}
.sf-metadata-rc-calendar .rc-calendar-today .rc-calendar-date {
border: none;
position: relative;
}
.sf-metadata-rc-calendar .rc-calendar-today .rc-calendar-date::after {
content: '';
background: #f09f3f;
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
left: 45%;
bottom: 0%;
display: inline-block;
}
.sf-metadata-rc-calendar .rdg-editor-container input.editor-main[readonly] {
background-color: #fff;
}
.sf-metadata-rc-calendar .rc-calendar-time-picker {
z-index: 1;
}
.sf-metadata-rc-calendar .date-picker-container {
box-sizing: border-box;
position: relative;
display: block;
line-height: 1.5;
margin-bottom: 22px;
}
.sf-metadata-rc-calendar .date-picker-container-row-expand {
margin-bottom: 0;
width: 320px;
}
.sf-metadata-rc-calendar .sf-metadata-rc-calendar-clear {
position: absolute;
top: 8px;
left: 225px;
fill: gray;
font-size: 12px;
}

View File

@ -18,7 +18,7 @@
justify-content: center;
}
.sf-metadata-checkbox-editor .sf-metadata-checkbox-editor-content .sf-metadata-icon-check-mark {
.sf-metadata-checkbox-editor .sf-metadata-checkbox-editor-content .seafile-multicolor-icon-check-mark {
fill: #20c933;
}

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../../../components/icon';
import './index.css';
@ -33,7 +33,7 @@ const CheckboxEditor = ({
return (
<div className="sf-metadata-checkbox-editor" onClick={onClickContainer}>
<div className="sf-metadata-checkbox-editor-content" onClick={onChangeValue}>
{value && (<Icon iconName="check-mark" />)}
{value && (<Icon symbol="check-mark" />)}
</div>
</div>
);

View File

@ -42,7 +42,7 @@
white-space: nowrap;
}
.collaborator .collaborator-remove .sf-metadata-icon-x-01 {
.collaborator .collaborator-remove .seafile-multicolor-icon-x-01 {
fill: #666;
font-size: 12px;
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import IconBtn from '../../../../../components/icon-btn';
import { useCollaborators } from '../../../../hooks';
import './index.css';
@ -20,7 +20,7 @@ const DeleteCollaborator = ({ value, onDelete }) => {
<img className="collaborator-avatar m-0" alt={name} src={avatar_url} />
</span>
<span className="collaborator-name text-truncate" title={name} aria-label={name}>{name}</span>
<IconBtn className="collaborator-remove" onClick={(event) => onDelete(email, event)} iconName="x-01" />
<IconBtn className="collaborator-remove" onClick={(event) => onDelete(email, event)} symbol="x-01" />
</div>
);
})}

View File

@ -84,7 +84,7 @@
margin-right: 10px;
}
.sf-metadata-collaborator-editor .collaborator-check-icon .sf-metadata-icon-check-icon {
.sf-metadata-collaborator-editor .collaborator-check-icon .seafile-multicolor-icon-check-icon {
font-size: 12px;
fill: #666;
}

View File

@ -1,7 +1,8 @@
import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, Icon } from '@seafile/sf-metadata-ui-component';
import SearchInput from '../../../../components/search-input';
import Icon from '../../../../components/icon';
import DeleteCollaborator from './delete-collaborator';
import { Utils } from '../../../../utils/utils';
import { KeyCodes } from '../../../../constants';
@ -232,7 +233,7 @@ const CollaboratorEditor = forwardRef(({
</span>
</div>
<div className="collaborator-check-icon">
{isSelected && (<Icon iconName="check-mark" />)}
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
</div>
</div>
</div>

View File

@ -0,0 +1,252 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import localeData from 'dayjs/plugin/localeData';
import utc from 'dayjs/plugin/utc';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import Calendar from '@seafile/seafile-calendar';
import DatePicker from '@seafile/seafile-calendar/lib/Picker';
import dayjs from '../../utils/dayjs';
import { translateCalendar } from '../../../utils/date-format-utils';
import { gettext } from '../../../utils/constants';
import { getEventClassName } from '../../../utils/dom';
import { Utils } from '../../../utils/utils';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en-gb';
import '@seafile/seafile-calendar/assets/index.css';
import '../../../css/metadata-rc-calendar.css';
dayjs.extend(utc);
dayjs.extend(localeData);
dayjs.extend(weekOfYear);
let now = dayjs();
class DateEditor extends Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
this.format = props.format || 'YYYY-MM-DD';
this.calendarContainerRef = React.createRef();
const isZhcn = props.lang === 'zh-cn';
if (isZhcn) {
now = now.locale('zh-cn');
} else {
now = now.locale('en-gb');
}
this.defaultCalendarValue = now.clone();
this.timeFormat = this.format.split(' ')[1] || '';
this.valueSubmitFormat = 'YYYY-MM-DD';
if (this.timeFormat) {
this.valueSubmitFormat = this.valueSubmitFormat + ' ' + this.timeFormat;
}
}
componentDidMount() {
const { lang, value } = this.props;
const isZhcn = lang === 'zh-cn';
if (value && dayjs(value).isValid()) {
if (typeof value === 'string' && value.length === 1 && !isNaN(Number(value, 10))) {
this.timer = setTimeout(() => {
let inputDom = document.getElementsByClassName('rc-calendar-input')[0];
if (inputDom) {
inputDom.value = value;
}
}, 200);
return;
}
let validValue = dayjs(value).isValid() ? dayjs(value) : dayjs(this.defaultCalendarValue);
this.setState({ value: isZhcn ? dayjs(validValue).locale('zh-cn') : dayjs(validValue).locale('en-gb') });
}
document.addEventListener('keydown', this.onHotKey, true);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onHotKey, true);
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
onChange = (value) => {
if (!value) return;
this.setState({ value });
if (Utils.isFunction(this.props.onChange)) {
this.props.onChange(value.format(this.valueSubmitFormat));
}
};
getValue = () => {
const { value } = this.state;
return value ? value.format(this.valueSubmitFormat) : null;
};
getInputNode = () => {
if (!this.datePickerRef) return null;
if (this.datePickerRef.tagName === 'INPUT') {
return this.datePickerRef;
}
return this.datePickerRef.querySelector('input:not([type=hidden])');
};
onBlur = (type) => {
if (this.props.onBlur) this.props.onBlur(type);
};
closeEditor = () => {
this.onBlur();
};
getCalendarContainer = () => {
return document.body;
};
handleMouseDown = (e) => {
e.preventDefault();
};
handleKeyDown = (e) => {
const directionKeyCodes = [37, 38, 39, 40];
if (directionKeyCodes.includes(e.keyCode)) {
e.stopPropagation();
} else if (e.keyCode === 13) {
e.preventDefault();
this.onBlur('enter');
}
};
onClick = (event) => {
event.stopPropagation();
const className = getEventClassName(event);
if (!this.timeFormat && className === 'rc-calendar-date') {
this.timer = setTimeout(() => {
this.closeEditor();
}, 1);
}
};
onClear = () => {
if (Utils.isFunction(this.props.onClear)) {
this.props.onClear();
return;
}
this.setState({ value: null });
};
onHotKey = (e) => {
if (e.keyCode === 27) {
e.stopPropagation();
this.props.onClose && this.props.onClose(true);
}
};
getCalendarFormat = () => {
if (this.format.indexOf('YYYY-MM-DD') > -1) {
let newColumnDataFormat = this.format.replace('YYYY-MM-DD', 'YYYY-M-D');
return [this.format, newColumnDataFormat];
}
if (this.format.indexOf('DD/MM/YYYY') > -1) {
let newColumnDataFormat = this.format.replace('DD/MM/YYYY', 'D/M/YYYY');
return [this.format, newColumnDataFormat];
}
return [this.format];
};
getDefaultMinutesTime = () => {
const { value } = this.props;
if (!this.timeFormat) return '';
if (value) return dayjs(value).format('HH:mm');
return '';
};
onClickRightPanelTime = () => {
if (this.timeFormat.indexOf('ss') > 0) return;
setTimeout(() => {
this.closeEditor();
}, 1);
};
render() {
const { isReadOnly, lang } = this.props;
const state = this.state;
if (isReadOnly) return (
<input
className="ant-calendar-picker-input ant-input form-control"
value={state.value ? state.value.format(this.format) : ''}
disabled={true}
/>
);
const calendarFormat = this.getCalendarFormat();
const defaultMinutesTime = this.getDefaultMinutesTime();
const calendar = (
<Calendar
className="sf-metadata-rc-calendar"
locale={translateCalendar(lang)}
style={{ zIndex: 1060 }}
dateInputPlaceholder={gettext('Enter date')}
format={calendarFormat}
defaultValue={this.defaultCalendarValue}
showDateInput={true}
focusablePanel={false}
showHourAndMinute={Boolean(this.timeFormat)}
defaultMinutesTime={defaultMinutesTime}
onClear={this.onClear}
onClickRightPanelTime={this.onClickRightPanelTime}
/>
);
return (
<div className="date-picker-container" ref={ref => this.datePickerRef = ref} onKeyDown={(e) => this.handleKeyDown(e)} onClick={(e) => this.onClick(e)}>
<DatePicker
calendar={calendar}
value={state.value}
onChange={this.onChange}
getCalendarContainer={this.getCalendarContainer}
onOpenChange={this.onOpenChange}
open={true}
style={{ zIndex: 1060 }}
>
{
({ value }) => {
return (
<span tabIndex="0">
<input
ref={ref => this.inputRef = ref}
placeholder={this.format ? this.format : gettext('Please select')}
tabIndex="-1"
readOnly
className="ant-calendar-picker-input ant-input form-control"
value={value ? value.format(this.format) : ''}
onMouseDown={this.handleMouseDown}
/>
<div ref={this.calendarContainerRef} />
</span>
);
}
}
</DatePicker>
</div>
);
}
}
DateEditor.propTypes = {
isReadOnly: PropTypes.bool,
format: PropTypes.string,
lang: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
DateEditor.defaultProps = {
format: 'YYYY-MM-DD',
lang: 'zh-cn',
isReadOnly: false,
};
export default DateEditor;

View File

@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
import ClickOutside from '../../../../components/click-outside';
import Editor from '../editor';
import { Utils } from '../../../../utils/utils';
import { getEventClassName } from '../../../utils/common';
import { getEventClassName } from '../../../../utils/dom';
import { isCellValueChanged, getCellValueByColumn } from '../../../utils/cell';
import { canEditCell } from '../../../utils/column';
import { isCtrlKeyHeldDown, isKeyPrintable } from '../../../utils/keyboard-utils';
import { isCtrlKeyHeldDown, isKeyPrintable } from '../../../../utils/keyboard-utils';
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEYS, metadataZIndexes, CellType } from '../../../constants';
class NormalEditorContainer extends React.Component {

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
import ClickOutside from '../../../../components/click-outside';
import Editor from '../editor';
import { Utils } from '../../../../utils/utils';
import { isCellValueChanged, getCellValueByColumn, getColumnOptionNameById, getColumnOptionNamesByIds } from '../../../utils/cell';

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SfCalendar } from '@seafile/sf-metadata-ui-component';
import DateEditor from './date-editor';
import FileNameEditor from './file-name-editor';
import TextEditor from './text-editor';
import NumberEditor from './number-editor';
@ -23,7 +23,7 @@ const Editor = React.forwardRef((props, ref) => {
return (<TextEditor ref={ref} { ...props } />);
}
case CellType.DATE: {
return (<SfCalendar ref={ref} { ...props } lang={lang} />);
return (<DateEditor ref={ref} { ...props } lang={lang} />);
}
case CellType.NUMBER: {
return (<NumberEditor ref={ref} {...props} />);

View File

@ -23,7 +23,7 @@
left: 2px;
}
.sf-metadata-delete-select-options .sf-metadata-delete-select-option .sf-metadata-icon-x-01 {
.sf-metadata-delete-select-options .sf-metadata-delete-select-option .seafile-multicolor-icon-x-01 {
fill: inherit;
font-size: 12px;
}

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import IconBtn from '../../../../../components/icon-btn';
import { gettext } from '../../../../../utils/constants';
import { DELETED_OPTION_TIPS, DELETED_OPTION_BACKGROUND_COLOR } from '../../../../constants';
@ -41,7 +41,7 @@ const DeleteOption = ({ value, options, onDelete }) => {
return (
<div key={id} className="sf-metadata-delete-select-option" style={style}>
<span className="sf-metadata-delete-select-option-name text-truncate" title={name} aria-label={name}>{name}</span>
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(id, event)} iconName="x-01" />
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(id, event)} symbol="x-01" />
</div>
);
})}

View File

@ -1,7 +1,9 @@
import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
import CommonAddTool from '../../../../components/common-add-tool';
import SearchInput from '../../../../components/search-input';
import Icon from '../../../../components/icon';
import DeleteOption from './delete-options';
import { Utils } from '../../../../utils/utils';
import { getColumnOptionIdsByNames } from '../../../utils/cell';
@ -236,7 +238,7 @@ const MultipleSelectEditor = forwardRef(({
</span>
</div>
<div className="single-select-check-icon">
{isSelected && (<Icon iconName="check-mark" />)}
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
</div>
</div>
</div>
@ -261,7 +263,7 @@ const MultipleSelectEditor = forwardRef(({
{renderOptions()}
</div>
{isShowCreateBtn && (
<CustomizeAddTool
<CommonAddTool
callBack={createOption}
footerName={`${gettext('Add option')} ${searchValue}`}
className="add-search-result"

View File

@ -12,7 +12,7 @@
opacity: 1 !important;
}
.sf-metadata-rate-item .sf-metadata-icon {
.sf-metadata-rate-item .seafile-multicolor-icon {
fill: inherit;
font-size: 16px;
}

View File

@ -2,7 +2,7 @@ import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import { UncontrolledTooltip } from 'reactstrap';
import classnames from 'classnames';
import { Icon } from '@seafile/sf-metadata-ui-component';
import Icon from '../../../../components/icon';
import { DEFAULT_RATE_DATA } from '../../../constants';
const RateItem = ({
@ -51,7 +51,7 @@ const RateItem = ({
className={classnames('sf-metadata-rate-item', { 'active': value >= index })}
ref={ref}
>
<Icon iconName={type || 'rate'} />
<Icon className="sf-metadata-icon" symbol={type || 'rate'} />
</div>
{enterIndex !== -1 && (
<UncontrolledTooltip placement='bottom' target={ref} modifiers={[{ name: 'preventOverflow', options: { boundary: document.body } }]} className="sf-metadata-tooltip">

View File

@ -97,7 +97,7 @@
text-align: center;
}
.sf-metadata-single-select-editor .single-select-check-icon .sf-metadata-icon-check-icon {
.sf-metadata-single-select-editor .single-select-check-icon .seafile-multicolor-icon-check-icon {
font-size: 12px;
fill: #666;
}

View File

@ -1,7 +1,9 @@
import React, { forwardRef, useMemo, useImperativeHandle, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
import CommonAddTool from '../../../../components/common-add-tool';
import SearchInput from '../../../../components/search-input';
import Icon from '../../../../components/icon';
import { gettext } from '../../../../utils/constants';
import { Utils } from '../../../../utils/utils';
import { KeyCodes } from '../../../../constants';
@ -232,7 +234,7 @@ const SingleSelectEditor = forwardRef(({
</span>
</div>
<div className="single-select-check-icon">
{isSelected && (<Icon iconName="check-mark" />)}
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
</div>
</div>
</div>
@ -256,7 +258,7 @@ const SingleSelectEditor = forwardRef(({
{renderOptions()}
</div>
{isShowCreateBtn && (
<CustomizeAddTool
<CommonAddTool
callBack={createOption}
footerName={`${gettext('Add option')} ${searchValue}`}
className="add-search-result"

View File

@ -50,7 +50,7 @@
left: 2px;
}
.sf-metadata-delete-select-tags .sf-metadata-delete-select-remove .sf-metadata-icon-x-01 {
.sf-metadata-delete-select-tags .sf-metadata-delete-select-remove .seafile-multicolor-icon-x-01 {
fill: #666;
font-size: 12px;
}

View File

@ -1,7 +1,7 @@
import React from './index';
import PropTypes from 'prop-types';
import { IconBtn } from '@seafile/sf-metadata-ui-component';
import { getRowById } from '../../../../utils/table';
import IconBtn from '../../../../../components/icon-btn';
import { getRowById } from '../../../../../components/sf-table/utils/table';
import { getTagColor, getTagName } from '../../../../../tag/utils/cell';
import './index.css';
@ -18,7 +18,7 @@ const DeleteTag = ({ value, tags, onDelete }) => {
<div className="sf-metadata-delete-select-tag" key={tagId}>
<div className="sf-metadata-delete-select-tag-color" style={{ backgroundColor: tagColor }}></div>
<div className="sf-metadata-delete-select-tag-name">{tagName}</div>
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(tagId, event)} iconName="x-01" />
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(tagId, event)} symbol="x-01" />
</div>
);
})}

View File

@ -1,17 +1,19 @@
import React, { forwardRef, useMemo, useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
import CommonAddTool from '../../../../components/common-add-tool';
import SearchInput from '../../../../components/search-input';
import Icon from '../../../../components/icon';
import DeleteTags from './delete-tags';
import { Utils } from '../../../../utils/utils';
import { KeyCodes } from '../../../../constants';
import { gettext } from '../../../../utils/constants';
import { useTags } from '../../../../tag/hooks';
import { getTagColor, getTagId, getTagName, getTagsByNameOrColor, getTagByNameOrColor } from '../../../../tag/utils/cell';
import { getRecordIdFromRecord } from '../../../utils/cell';
import { getRowById } from '../../../utils/table';
import { getRowById } from '../../../../components/sf-table/utils/table';
import { SELECT_OPTION_COLORS } from '../../../constants';
import { PRIVATE_COLUMN_KEY as TAG_PRIVATE_COLUMN_KEY } from '../../../../tag/constants';
import DeleteTags from './delete-tags';
import './index.css';
@ -228,7 +230,7 @@ const TagsEditor = forwardRef(({
<div className="sf-metadata-tag-name">{tagName}</div>
</div>
<div className="sf-metadata-tags-editor-tag-check-icon">
{isSelected && (<Icon iconName="check-mark" />)}
{isSelected && (<Icon className="sf-metadata-icon" symbol="check-mark" />)}
</div>
</div>
</div>
@ -253,7 +255,7 @@ const TagsEditor = forwardRef(({
{renderOptions()}
</div>
{isShowCreateBtn && (
<CustomizeAddTool
<CommonAddTool
callBack={createTag}
footerName={`${gettext('Add tag')} ${searchValue}`}
className="add-search-result"

View File

@ -0,0 +1,68 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Collaborator from './collaborator';
import { isValidEmail } from '../../utils/validate/email';
const AsyncCollaborator = ({ value, mediaUrl, api, collaborators, collaboratorsCache, updateCollaboratorsCache }) => {
const [collaborator, setCollaborator] = useState(null);
useEffect(() => {
let isMounted = true;
if (!value) {
isMounted && setCollaborator(null);
return () => isMounted = false;
}
let collaborator = collaborators && collaborators.find(c => c.email === value);
if (collaborator) {
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
const defaultAvatarUrl = `${mediaUrl}/avatars/default.png`;
if (value === 'anonymous') {
collaborator = {
name: 'anonymous',
avatar_url: defaultAvatarUrl,
};
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
collaborator = collaboratorsCache[value];
if (collaborator) {
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
if (!isValidEmail(value)) {
collaborator = {
email: value,
name: value,
avatar_url: defaultAvatarUrl,
};
updateCollaboratorsCache(collaborator);
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
api && api(value, (userMap) => {
collaborator = userMap[value];
updateCollaboratorsCache(collaborator);
isMounted && setCollaborator(collaborator);
});
return () => isMounted = false;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!collaborator) return null;
return (
<Collaborator collaborator={collaborator} />
);
};
AsyncCollaborator.propTypes = {
value: PropTypes.string,
};
export default AsyncCollaborator;

View File

@ -0,0 +1,13 @@
.sf-metadata-ui.checkbox-formatter {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.sf-metadata-ui.checkbox-formatter .seafile-multicolor-icon-check-mark {
font-size: 14px;
font-weight: 600;
fill: #20c933;
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Icon from '../../../../components/icon';
import './index.css';
const CheckboxFormatter = ({ value, className, children: emptyFormatter }) => {
if (!value) return emptyFormatter;
return (
<div className={classnames('sf-metadata-ui cell-formatter-container checkbox-formatter', className)}>
<Icon symbol="check-mark"/>
</div>
);
};
CheckboxFormatter.propTypes = {
value: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.any,
};
export default CheckboxFormatter;

View File

@ -0,0 +1,59 @@
.sf-metadata-ui.collaborator-item {
display: inline-flex;
align-items: center;
margin-right: 10px;
padding: 0 8px 0 2px;
height: 20px;
font-size: 13px;
border-radius: 10px;
background: #eaeaea;
width: fit-content;
max-width: 200px;
}
.sf-metadata-ui.collaborator-item .collaborator-avatar,
.sf-metadata-ui.collaborator-item .collaborator-name,
.sf-metadata-ui.collaborator-item .collaborator-remove {
height: 20px;
line-height: 20px;
}
.sf-metadata-ui.collaborator-item .collaborator-avatar {
display: flex;
align-items: center;
justify-content: center;
margin: 0 5px 0 0;
flex-shrink: 0;
}
.sf-metadata-ui.collaborator-item .collaborator-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sf-metadata-ui.collaborator-item .collaborator-avatar img {
width: 16px;
height: 16px;
border-radius: 50%;
}
.sf-metadata-ui.collaborator-item .collaborator-remove {
display: inline-block;
width: 14px;
margin: 0 -2px 0 2px;
flex-shrink: 0;
}
.sf-metadata-ui.collaborator-item .collaborator-remove .seafile-multicolor-icon {
display: inline-block;
font-size: 12px;
color: #909090;
transform: scale(.8);
cursor: pointer;
}
.sf-metadata-ui.collaborator-item .collaborator-remove .seafile-multicolor-icon:hover {
color: #666666;
cursor: pointer;
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../../../../components/icon';
import './index.css';
const Collaborator = ({ enableDelete = false, collaborator, onDelete }) => {
if (!collaborator) return null;
return (
<div className="sf-metadata-ui collaborator-item" title={collaborator.name}>
<span className="collaborator-avatar">
<img className="collaborator-avatar-icon" alt={collaborator.name} src={collaborator.avatar_url} />
</span>
<span className="collaborator-name">{collaborator.name}</span>
{enableDelete && (
<span className="collaborator-remove" onClick={onDelete}>
<Icon symbol="delete" />
</span>
)}
</div>
);
};
Collaborator.propTypes = {
collaborator: PropTypes.shape({
name: PropTypes.string.isRequired,
avatar_url: PropTypes.string.isRequired,
email: PropTypes.string,
}),
enableDelete: PropTypes.bool,
onDeleteCollaborator: PropTypes.func,
};
export default Collaborator;

View File

@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import AsyncCollaborator from './async-collaborator';
const CollaboratorsFormatter = ({ value, className, children: emptyFormatter, ...params }) => {
if (!Array.isArray(value) || value.length === 0) return emptyFormatter || null;
const validValue = value.filter(item => item);
if (validValue.length === 0) return emptyFormatter || null;
return (
<div className={classnames('sf-metadata-ui cell-formatter-container collaborators-formatter', className)}>
{value.map(email => <AsyncCollaborator key={email} { ...params } value={email} />)}
</div>
);
};
CollaboratorsFormatter.propTypes = {
value: PropTypes.array,
className: PropTypes.string,
children: PropTypes.any,
};
export default CollaboratorsFormatter;

View File

@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Collaborator from './collaborator';
import { isValidEmail } from '../../utils/validate/email';
const CreatorFormatter = ({ value, mediaUrl, className, api, collaborators = [], collaboratorsCache = {}, updateCollaboratorsCache, children: emptyFormatter }) => {
const [collaborator, setCollaborator] = useState(null);
useEffect(() => {
let isMounted = true;
if (!value) {
isMounted && setCollaborator(null);
return () => isMounted = false;
}
let collaborator = collaborators && collaborators.find(c => c.email === value);
if (collaborator) {
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
const defaultAvatarUrl = `${mediaUrl}/avatars/default.png`;
if (value === 'anonymous') {
collaborator = {
name: 'anonymous',
avatar_url: defaultAvatarUrl,
};
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
collaborator = collaboratorsCache[value];
if (collaborator) {
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
if (!isValidEmail(value)) {
collaborator = {
email: value,
name: value,
avatar_url: defaultAvatarUrl,
};
updateCollaboratorsCache && updateCollaboratorsCache(collaborator);
isMounted && setCollaborator(collaborator);
return () => isMounted = false;
}
api && api(value, (userMap) => {
collaborator = userMap[value];
Object.values(userMap).forEach(user => {
updateCollaboratorsCache && updateCollaboratorsCache(user);
});
isMounted && setCollaborator(collaborator);
});
return () => isMounted = false;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!collaborator) return emptyFormatter || null;
return (
<div className={classnames('sf-metadata-ui cell-formatter-container creator-formatter', className)}>
<Collaborator collaborator={collaborator} />
</div>
);
};
CreatorFormatter.propTypes = {
value: PropTypes.string,
className: PropTypes.string,
children: PropTypes.any,
};
export default CreatorFormatter;

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import dayjs from 'dayjs';
const CTimeFormatter = ({ value, className, children: emptyFormatter }) => {
if (!value) return emptyFormatter || null;
const valueFormat = dayjs(value).format('YYYY-MM-DD HH:mm:ss');
return (
<div
className={classnames('sf-metadata-ui cell-formatter-container ctime-formatter', className)}
title={valueFormat}
>
{valueFormat}
</div>
);
};
CTimeFormatter.propTypes = {
value: PropTypes.string.isRequired,
className: PropTypes.string,
children: PropTypes.any,
};
export default CTimeFormatter;

View File

@ -0,0 +1,3 @@
.sf-metadata-ui.date-formatter {
text-align: right;
}

View File

@ -0,0 +1,30 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { getDateDisplayString } from '../../../utils/cell/column/date';
import './index.css';
const DateFormatter = ({ value, format, className, children: emptyFormatter }) => {
const displayValue = useMemo(() => {
return getDateDisplayString(value, format);
}, [value, format]);
if (!displayValue) return emptyFormatter || null;
return (
<div
className={classnames('sf-metadata-ui cell-formatter-container date-formatter', className)}
title={displayValue}
>
{displayValue}
</div>
);
};
DateFormatter.propTypes = {
value: PropTypes.any,
className: PropTypes.string,
children: PropTypes.any,
};
export default DateFormatter;

Some files were not shown because too many files have changed in this diff Show More