1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-05 00:43:53 +00:00

feat: metadata filter (#6430)

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-07-26 17:15:52 +08:00
committed by GitHub
parent 3a06447faf
commit 1d2ee1ac52
33 changed files with 364 additions and 211 deletions

View File

@@ -18,7 +18,6 @@
"no-prototype-builtins": "off",
"no-restricted-globals": "off",
"brace-style": "off",
"no-console": "off",
"no-cond-assign": "off",
"no-var": "off",
"no-case-declarations": "off",

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { SeafileMetadata } from '../../metadata';
import { Utils } from '../../utils/utils';
import { gettext, siteRoot, lang, mediaUrl } from '../../utils/constants';
import { gettext, siteRoot, mediaUrl } from '../../utils/constants';
import SeafileMarkdownViewer from '../seafile-markdown-viewer';
const propTypes = {
@@ -54,13 +54,8 @@ class DirColumnFile extends React.Component {
if (this.props.content === '__sf-metadata') {
const { repoID, currentRepoInfo, metadataViewId } = this.props;
window.sfMetadata = {
siteRoot,
lang,
mediaUrl,
};
return (<SeafileMetadata repoID={repoID} currentRepoInfo={currentRepoInfo} viewID={metadataViewId} />);
return (<SeafileMetadata mediaUrl={mediaUrl} repoID={repoID} repoInfo={currentRepoInfo} viewID={metadataViewId} />);
}
return (

View File

@@ -27,7 +27,10 @@ export {
export {
PRIVATE_COLUMN_KEY,
PRIVATE_COLUMN_KEYS
PRIVATE_COLUMN_KEYS,
EDITABLE_PRIVATE_COLUMN_KEYS,
EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
DELETABLE_PRIVATE_COLUMN_KEY,
} from './private';
export {

View File

@@ -46,3 +46,25 @@ export const PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.FILE_STATUS,
PRIVATE_COLUMN_KEY.LOCATION,
];
export const EDITABLE_PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.FILE_COLLABORATORS,
PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME,
PRIVATE_COLUMN_KEY.FILE_KEYWORDS,
PRIVATE_COLUMN_KEY.FILE_SUMMARY,
PRIVATE_COLUMN_KEY.FILE_EXPIRED,
PRIVATE_COLUMN_KEY.FILE_STATUS,
];
export const EDITABLE_DATA_PRIVATE_COLUMN_KEYS = [
];
export const DELETABLE_PRIVATE_COLUMN_KEY = [
PRIVATE_COLUMN_KEY.FILE_COLLABORATORS,
PRIVATE_COLUMN_KEY.FILE_EXPIRE_TIME,
PRIVATE_COLUMN_KEY.FILE_KEYWORDS,
PRIVATE_COLUMN_KEY.FILE_SUMMARY,
PRIVATE_COLUMN_KEY.FILE_EXPIRED,
PRIVATE_COLUMN_KEY.FILE_STATUS,
];

View File

@@ -24,6 +24,9 @@ export {
VIEW_NOT_DISPLAY_COLUMN_KEYS,
PREDEFINED_COLUMN_KEYS,
GEOLOCATION_FORMAT,
EDITABLE_PRIVATE_COLUMN_KEYS,
EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
DELETABLE_PRIVATE_COLUMN_KEY,
} from './column';
export {
FILTER_CONJUNCTION_TYPE,

View File

@@ -50,6 +50,9 @@ export {
NOT_DISPLAY_COLUMN_KEYS,
VIEW_NOT_DISPLAY_COLUMN_KEYS,
PREDEFINED_COLUMN_KEYS,
EDITABLE_PRIVATE_COLUMN_KEYS,
EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
DELETABLE_PRIVATE_COLUMN_KEY,
} from './constants';
export {

View File

@@ -8,6 +8,7 @@ import {
FILTER_ERR_MSG,
} from '../../constants/filter';
import { isDateColumn } from '../column/date';
import { getColumnOptions } from '../column';
const TERM_TYPE_MAP = {
NUMBER: 'number',
@@ -16,6 +17,11 @@ const TERM_TYPE_MAP = {
ARRAY: 'array',
};
const PREDICATES_REQUIRE_ARRAY_TERM = [
FILTER_PREDICATE_TYPE.IS_ANY_OF,
FILTER_PREDICATE_TYPE.IS_NONE_OF,
];
const TEXT_COLUMN_TYPES = [CellType.TEXT, CellType.FILE_NAME];
const CHECK_EMPTY_PREDICATES = [FILTER_PREDICATE_TYPE.EMPTY, FILTER_PREDICATE_TYPE.NOT_EMPTY];
@@ -142,7 +148,7 @@ class ValidateFilter {
if (CHECK_EMPTY_PREDICATES.includes(predicate)) {
return true;
}
if (array_type === CellType.SINGLE_SELECT || array_type === CellType.DEPARTMENT_SINGLE_SELECT) {
if (array_type === CellType.SINGLE_SELECT) {
return this.validatePredicate(predicate, { type: CellType.MULTIPLE_SELECT });
}
if (COLLABORATOR_COLUMN_TYPES.includes(array_type)) {
@@ -223,9 +229,13 @@ class ValidateFilter {
static isValidTerm(term, predicate, modifier, filterColumn) {
switch (filterColumn.type) {
case CellType.TEXT:
case CellType.GEOLOCATION:
case CellType.FILE_NAME: {
return this.isValidTermType(term, TERM_TYPE_MAP.STRING);
}
case CellType.NUMBER: {
return this.isValidTermType(term, TERM_TYPE_MAP.NUMBER);
}
case CellType.CHECKBOX:
case CellType.BOOL: {
@@ -244,6 +254,24 @@ class ValidateFilter {
}
return this.isValidTermType(term, TERM_TYPE_MAP.STRING);
}
case CellType.SINGLE_SELECT: {
const options = getColumnOptions(filterColumn);
if (PREDICATES_REQUIRE_ARRAY_TERM.includes(predicate)) {
if (!this.isValidTermType(term, TERM_TYPE_MAP.ARRAY)) {
return false;
}
// contains deleted option(s)
return this.isValidSelectedOptions(term, options);
}
if (!this.isValidTermType(term, TERM_TYPE_MAP.STRING)) {
return false;
}
// invalid filter_term if selected option is deleted
return !!options.find((option) => term === option.id);
}
default: {
return false;
}

View File

@@ -57,7 +57,7 @@ const FileNameEditor = ({ column, record, onCommitCancel }) => {
if (fileType === 'image') {
const fileExt = fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase();
const isGIF = fileExt === 'gif';
const useThumbnail = window.sfMetadataContext.getSetting('currentRepoInfo')?.encrypted;
const useThumbnail = window.sfMetadataContext.getSetting('repoInfo')?.encrypted;
let src = '';
if (useThumbnail && !isGIF) {
src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`;

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, UncontrolledPopover } from 'reactstrap';
import classnames from 'classnames';
import { CellType, DEFAULT_DATE_FORMAT } from '../../../_basic';
import { CellType, DEFAULT_DATE_FORMAT, PRIVATE_COLUMN_KEY } from '../../../_basic';
import { gettext } from '../../../utils';
import ObjectUtils from '../../../utils/object-utils';
import { ValidateColumnFormFields } from './utils';
@@ -11,6 +11,7 @@ import { useMetadata } from '../../../hooks';
import Name from './name';
import Type from './type';
import Data from './data';
import { getDefaultFileStatusOptions } from '../../../utils/column-utils';
import './index.css';
@@ -73,6 +74,10 @@ const ColumnPopover = ({ target, onChange }) => {
} else if (column.type === CellType.DATE) {
data = { format: DEFAULT_DATE_FORMAT };
}
} else {
if (column.type === CellType.SINGLE_SELECT && column.key === PRIVATE_COLUMN_KEY.FILE_STATUS) {
data = { options: getDefaultFileStatusOptions() };
}
}
}
onChange(columnName, column.type, { key: column.unique ? column.key : '', data });

View File

@@ -1,4 +1,4 @@
.filter-popover .popover {
.sf-metadata-filter-popover .popover {
max-width: none;
min-width: 300px;
}

View File

@@ -155,7 +155,7 @@ class FilterPopover extends Component {
target={target}
fade={false}
hideArrow={true}
className="filter-popover"
className="sf-metadata-filter-popover"
boundariesElement={document.body}
>
{({ scheduleUpdate }) => (
@@ -180,7 +180,7 @@ class FilterPopover extends Component {
addIconClassName="popover-add-icon"
/>
{this.isNeedSubmit() && (
<div className='filter-popover-footer'>
<div className='sf-metadata-filter-popover-footer'>
<Button className='mr-2' onClick={this.onClosePopover}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={this.state.isSubmitDisabled} onClick={this.onSubmitFilters}>{gettext('Submit')}</Button>
</div>

View File

@@ -40,7 +40,7 @@ class FilterItemUtils {
<div className='select-option-name single-option-name'>
<div className="single-select-option" style={{ background: option.color, color: option.textColor || null }} title={option.name} aria-label={option.name}>{option.name}</div>
<div className='single-check-icon'>
{selectedOption?.id === option.id && <i className="option-edit sf-metadata-font sf-metadata-icon-check-mark"></i>}
{selectedOption?.id === option.id && (<Icon iconName="check-mark" />)}
</div>
</div>
)
@@ -54,7 +54,7 @@ class FilterItemUtils {
<div className='select-option-name multiple-option-name'>
<div className="multiple-select-option" style={{ background: option.color, color: option.textColor }} title={option.name} aria-label={option.name}>{option.name}</div>
<div className='multiple-check-icon'>
{filterTerm.indexOf(option.id) > -1 && <i className="option-edit sf-metadata-font sf-metadata-icon-check-mark"></i>}
{filterTerm.indexOf(option.id) > -1 && (<Icon iconName="check-mark" />)}
</div>
</div>
)

View File

@@ -0,0 +1,13 @@
.sf-metadata-selector-collaborator.sf-metadata-select .option {
line-height: 20px;
padding: 5px 10px 5px 10px !important;
}
.sf-metadata-selector-collaborator.sf-metadata-select .option:hover {
background-color: #f7f7f7;
color: #212529;
}
.sf-metadata-selector-collaborator.sf-metadata-select .selected-option-show {
text-overflow: clip;
}

View File

@@ -1,8 +1,10 @@
import React, { Fragment, useMemo } from 'react';
import PropTypes from 'prop-types';
import { CustomizeSelect, Icon } from '@seafile/sf-metadata-ui-component';
import { FILTER_PREDICATE_TYPE } from '../../../../_basic';
import { gettext } from '../../../../utils';
import { FILTER_PREDICATE_TYPE } from '../../../../../../_basic';
import { gettext } from '../../../../../../utils';
import './index.css';
const CollaboratorFilter = ({ isLocked, filterIndex, filterTerm, collaborators, placeholder, filter_predicate, onSelectCollaborator }) => {
const supportMultipleSelectOptions = useMemo(() => {
@@ -73,7 +75,7 @@ const CollaboratorFilter = ({ isLocked, filterIndex, filterTerm, collaborators,
return (
<CustomizeSelect
className="selector-collaborator"
className="sf-metadata-selector-collaborator"
value={selectValue ? { label: selectValue } : {}}
onSelectOption={onSelectCollaborator}
options={options}

View File

@@ -0,0 +1,49 @@
.sf-metadata-selector-single-select .selected-option .single-select-option,
.sf-metadata-selector-multiple-select .selected-option .multiple-select-option {
display: inline-block;
margin: 0;
}
.sf-metadata-filters .filters-list .single-select-option,
.sf-metadata-filters .filters-list .multiple-select-option {
border-radius: 10px;
display: block;
font-size: 13px;
line-height: 20px;
margin: 0;
max-width: 150px;
overflow: hidden;
padding: 0 10px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
width: -webkit-min-content;
width: min-content;
}
.sf-metadata-selector-single-select .option,
.sf-metadata-selector-multiple-select .option {
height: 30px;
padding: 0 10px;
}
.sf-metadata-selector-single-select .option:hover,
.sf-metadata-selector-multiple-select .option:hover {
background-color: #f7f7f7;
color: #212529;
}
.sf-metadata-select .select-placeholder {
color: #868E96;
}
.sf-metadata-selector-multiple-select.sf-metadata-select .selected-option-show,
.sf-metadata-selector-single-select.sf-metadata-select .selected-option-show {
text-overflow: clip;
}
.sf-metadata-selector-single-select .select-option-name,
.sf-metadata-selector-multiple-select .select-option-name {
margin-top: 5px;
justify-content: space-between;
}

View File

@@ -9,17 +9,20 @@ import {
filterTermModifierIsWithin,
isDateColumn,
FILTER_ERR_MSG,
} from '../../../../_basic';
getSelectColumnOptions,
} from '../../../../../_basic';
import CollaboratorFilter from './collaborator-filter';
import FilterCalendar from './filter-calendar';
import FilterItemUtils from './filter-item-utils';
import FilterCalendar from '../filter-calendar';
import FilterItemUtils from '../filter-item-utils';
import {
getFilterByColumn, getUpdatedFilterBySelectSingle, getUpdatedFilterBySelectMultiple,
getUpdatedFilterByCreator, getUpdatedFilterByCollaborator, getColumnOptions, getUpdatedFilterByPredicate,
} from '../../../../utils/filters-utils';
import { isCheckboxColumn } from '../../../../utils/column-utils';
import { gettext } from '../../../../utils';
import { DELETED_OPTION_BACKGROUND_COLOR, DELETED_OPTION_TIPS } from '../../../../constants';
} from '../../../../../utils/filters-utils';
import { isCheckboxColumn } from '../../../../../utils/column-utils';
import { gettext } from '../../../../../utils';
import { DELETED_OPTION_BACKGROUND_COLOR, DELETED_OPTION_TIPS } from '../../../../../constants';
import './index.css';
const propTypes = {
index: PropTypes.number.isRequired,
@@ -333,7 +336,7 @@ class FilterItem extends React.Component {
});
return (
<CustomizeSelect
className="selector-multiple-select"
className="sf-metadata-selector-multiple-select"
value={selectedOptionNames}
options={dataOptions}
onSelectOption={this.onSelectMultiple}
@@ -346,6 +349,12 @@ class FilterItem extends React.Component {
);
};
getAllCollaborators = () => {
const collaborators = window.sfMetadata.collaborators;
const collaboratorsCache = window.sfMetadata.collaboratorsCache;
return [...collaborators, ...Object.values(collaboratorsCache)];
};
renderFilterTerm = (filterColumn) => {
const { index, filter, collaborators } = this.props;
const { type } = filterColumn;
@@ -410,6 +419,57 @@ class FilterItem extends React.Component {
case CellType.CHECKBOX: {
return this.getInputComponent('checkbox');
}
case CellType.SINGLE_SELECT: {
// get options
const options = getSelectColumnOptions(filterColumn);
if ([FILTER_PREDICATE_TYPE.IS_ANY_OF, FILTER_PREDICATE_TYPE.IS_NONE_OF].includes(filter_predicate)) {
return this.renderMultipleSelectOption(options, filter_term);
}
let selectedOptionDom = { label: null };
if (filter_term) {
let selectedOption = options.find(option => option.id === filter_term);
const className = 'select-option-name single-select-option';
const style = selectedOption ?
{ background: selectedOption.color, color: selectedOption.textColor || null } :
{ background: DELETED_OPTION_BACKGROUND_COLOR };
const selectedOptionName = selectedOption ? selectedOption.name : gettext('deleted option');
selectedOptionDom = { label: (
<span className={className} style={style} title={selectedOptionName} aria-label={selectedOptionName}>{selectedOptionName}</span>
) };
}
let dataOptions = options.map(option => {
return FilterItemUtils.generatorSingleSelectOption(option);
});
return (
<CustomizeSelect
className="sf-metadata-selector-single-select"
value={selectedOptionDom}
options={dataOptions || []}
onSelectOption={this.onSelectSingle}
placeholder={gettext('Select an option')}
searchable={true}
searchPlaceholder={gettext('Search option')}
noOptionsPlaceholder={gettext('No options available')}
isInModal={this.props.isInModal}
/>
);
}
case CellType.COLLABORATOR: {
if (filter_predicate === FILTER_PREDICATE_TYPE.INCLUDE_ME) return null;
const allCollaborators = this.getAllCollaborators();
return (
<CollaboratorFilter
filterIndex={index}
filterTerm={filter_term || []}
filter_predicate={filter_predicate}
collaborators={allCollaborators}
placeholder={gettext('Select collaborators')}
onSelectCollaborator={this.onSelectCollaborator}
/>
);
}
default: {
return null;
}

View File

@@ -226,8 +226,8 @@
.filters-list .multiple-check-icon .sf-metadata-icon-check-mark,
.filters-list .collaborator-check-icon .sf-metadata-icon-check-mark {
fill: #798d99;
font-size: 12px;
color: #798d99;
}
.user-select-item,

View File

@@ -1,4 +1,4 @@
.filter-popover-footer {
.sf-metadata-filter-popover-footer {
display: flex;
align-items: center;
justify-content: flex-end;
@@ -25,6 +25,6 @@
fill: #c2c2c2;
}
.filter-popover .popover-add-tool.disabled {
.sf-metadata-filter-popover .popover-add-tool.disabled {
color: #c2c2c2;
}

View File

@@ -1,3 +1,9 @@
.sf-metadata-edit-column-options-popover .popover {
margin-left: -4px;
margin-top: 4px;
max-width: 600px;
}
.sf-metadata-edit-column-options-container {
min-width: 400px;
height: auto;
@@ -12,6 +18,13 @@
color: #212529;
}
.sf-metadata-edit-column-options-container .none-search-result {
height: 100px;
opacity: .5;
padding: 10px;
width: 100%;
}
.sf-metadata-edit-column-options-container .sf-metadata-select-options-list {
margin-bottom: 0;
margin-top: 1rem;
@@ -19,3 +32,13 @@
overflow: auto;
padding: 0;
}
.sf-metadata-edit-column-options-container .sf-metadata-add-option {
border-top: none;
color: #666;
}
.sf-metadata-edit-column-options-container .sf-metadata-add-option .sf-metadata-add-option-icon {
fill: #666;
font-weight: 600;
}

View File

@@ -157,7 +157,7 @@ const OptionsPopover = ({ target, column, onToggle, onSubmit }) => {
<>
<CustomizePopover
target={target}
className="sf-metadata-edit-column-options"
className="sf-metadata-edit-column-options-popover"
canHide={!deletingOptionId}
hide={onToggle}
hideWithEsc={onToggle}

View File

@@ -1,20 +1,20 @@
.sort-popover .popover {
.sf-metadata-sort-popover .popover {
max-width: none;
min-width: 400px;
}
.sort-popover .sorts-list {
.sf-metadata-sort-popover .sorts-list {
min-height: 120px;
max-height: 100%;
padding: 15px;
}
.sort-popover .sorts-list .option-group {
.sf-metadata-sort-popover .sorts-list .option-group {
overflow: auto;
max-height: 360px;
}
.sort-popover .empty-sorts-container {
.sf-metadata-sort-popover .empty-sorts-container {
min-height: 80px;
padding: 16px;
}
@@ -51,7 +51,9 @@
}
.sorts-list .empty-sorts-list {
color: #666666;
color: #666;
font-size: 14px;
padding: 0.25rem 0.25rem 0.25rem 0;
}
.delete-sort .sf-metadata-icon-fork-number {

View File

@@ -245,7 +245,7 @@ class SortPopover extends Component {
target={target}
fade={false}
hideArrow={true}
className="sort-popover"
className="sf-metadata-sort-popover"
boundariesElement={document.body}
>
<div ref={ref => this.sortPopoverRef = ref} onClick={this.onPopoverInsideClick}>
@@ -264,7 +264,7 @@ class SortPopover extends Component {
/>
}
{(this.isNeedSubmit() && !readonly) && (
<div className='sort-popover-footer'>
<div className='sf-metadata-sort-popover-footer'>
<Button className='mr-2' onClick={this.onClosePopover}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={this.state.isSubmitDisabled} onClick={this.onSubmitSorts}>{gettext('Submit')}</Button>
</div>

View File

@@ -49,122 +49,6 @@
box-shadow: inset 0 0 0 2px rgb(0 0 0 / 10%);
}
.sf-metadata-wrapper .table-right-operations .new-record-btn button {
display: flex;
align-items: center;
justify-content: center;
height: 23px;
font-weight: 400;
border-color: rgba(0, 0, 0, 0.05);
}
.sf-metadata-wrapper .table-right-operations .more-operation-add-record {
padding: 0;
}
.sf-metadata-wrapper .table-right-operations .more-operation-add-record:not(:disabled):not(.disabled):active:focus {
box-shadow: none;
}
.sf-metadata-wrapper .table-right-operations .more-operation-add-record .dropdown {
display: inline-block;
width: 100%;
height: 100%;
}
.sf-metadata-wrapper .table-right-operations .add-record-dropdown-menu {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.sf-metadata-dropdown-menu.add-record {
margin-top: 4px;
}
.sf-metadata-wrapper .table-right-operations .more-operation-add-record .dropdown .sf-metadata-dropdown-menu {
margin-top: 2px;
}
.sf-metadata-wrapper .table-right-operations .more-operation-add-record .toggle-icon {
display: inline-block;
font-size: 12px;
transform: scale(0.8);
margin-top: 1px;
}
.sf-metadata-wrapper .table-right-operations .new-record {
font-size: 14px;
line-height: 1.5rem;
}
.sf-metadata-wrapper .table-right-operations .table-search-box .input-icon-addon.search-poll-button {
display: flex;
right: 25px;
height: 30px;
line-height: 30px;
left: auto;
text-align: center;
font-size: 12px;
min-width: 35px;
pointer-events: all;
}
.sf-metadata-wrapper .table-right-operations .table-search-box .search-poll-button .search-description {
height: 30px;
line-height: 30px;
color: #666666;
}
.search-poll-button .sf-metadata-font {
font-size: 12px;
cursor: pointer;
color: #212529;
}
.mobile-search-exchange-btn {
width: 30px;
height: 30px;
line-height: 30px;
background-color: #e5e5e5;
color: #212529;
display: block;
}
.mobile-search-exchange-btn:hover {
background-color: #ededed;
color: #666666;
}
.mobile-search-exchange-btn.mobile-search-upward {
border-radius: 2px 0 0 2px;
transform: scale(0.8, 0.8) translateX(8px);
}
.mobile-search-exchange-btn.mobile-search-backward {
border-radius: 0 2px 2px 0;
transform: scale(0.8, 0.8);
}
.search-text-clear {
cursor: pointer;
min-width: 25px;
pointer-events: all;
font-style: normal;
font-size: 18px;
font-weight: 700;
text-align: center;
line-height: 30px;
height: 30px;
color: #999;
}
.search-text-clear:hover {
color: #212529;
}
.sf-metadata-result.success {
display: flex;
flex-direction: column;

View File

@@ -28,13 +28,12 @@ const Cell = React.memo(({
const className = useMemo(() => {
const { type } = column;
const canEditable = window.sfMetadataContext.canModifyCell(column);
return classnames('sf-metadata-result-table-cell', `sf-metadata-result-table-${type}-cell`, {
return classnames('sf-metadata-result-table-cell', `sf-metadata-result-table-${type}-cell`, highlightClassName, {
'table-cell-uneditable': !canEditable || !TABLE_SUPPORT_EDIT_TYPE_MAP[type],
[highlightClassName]: highlightClassName,
'last-cell': isLastCell,
'table-last--frozen': isLastFrozenCell,
'cell-selected': isCellSelected,
// 'draging-file-to-cell': ,
// 'dragging-file-to-cell': ,
// 'row-comment-cell': ,
});
}, [column, highlightClassName, isLastCell, isLastFrozenCell, isCellSelected]);

View File

@@ -1,18 +1,28 @@
import React, { useCallback } from 'react';
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';
const ColumnDropdownItem = ({ disabled, iconName, target, title, tip, className, onChange, onMouseEnter }) => {
const [isShowToolTip, setToolTipShow] = useState(false);
useEffect(() => {
if (disabled) {
setToolTipShow(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onClick = useCallback((event) => {
event.preventDefault();
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
}, []);
if (!disabled) {
return (
<DropdownItem onClick={onChange} onMouseEnter={onMouseEnter} className={className}>
<DropdownItem id={target} onClick={onChange} onMouseEnter={onMouseEnter} className={className}>
<Icon iconName={iconName} />
<span className="item-text">{title}</span>
</DropdownItem>
@@ -20,6 +30,7 @@ const ColumnDropdownItem = ({ disabled, iconName, target, title, tip, className,
}
return (
<>
<DropdownItem
className={classnames('disabled', className)}
toggle={true}
@@ -29,12 +40,13 @@ const ColumnDropdownItem = ({ disabled, iconName, target, title, tip, className,
>
<Icon iconName={iconName} />
<span className="item-text">{title}</span>
{disabled &&
{isShowToolTip && (
<UncontrolledTooltip placement="right" target={target} fade={false} delay={{ show: 0, hide: 0 }}>
{tip}
</UncontrolledTooltip>
}
)}
</DropdownItem>
</>
);
};

View File

@@ -1,3 +1,7 @@
.sf-metadata-column-dropdown-menu {
margin-top: 20px;
}
.sf-metadata-column-dropdown-menu .dropdown-item .sf-metadata-icon {
margin-right: 10px;
font-size: 14px;
@@ -20,3 +24,18 @@
.sf-metadata-column-dropdown-menu .dropdown-toggle:hover::after {
color: #fff;
}
.sf-metadata-column-dropdown-menu .dropdown-item.disabled,
.sf-metadata-column-dropdown-menu .dropdown-item:disabled {
pointer-events: unset !important;
}
.sf-metadata-column-dropdown-menu .disabled.dropdown-item:hover {
background-color: unset;
cursor: default;
color: #c2c2c2;
}
.sf-metadata-column-dropdown-menu .disabled.dropdown-item .sf-metadata-icon {
fill: #c2c2c2;
}

View File

@@ -4,7 +4,6 @@ import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem as DefaultDropdown
import classnames from 'classnames';
import { ModalPortal, Icon } from '@seafile/sf-metadata-ui-component';
import { isMobile, gettext } from '../../../../../../../utils';
import { isFrozen } from '../../../../../../../utils/column-utils';
import DropdownItem from './dropdown-item';
import { CellType, DEFAULT_DATE_FORMAT, getDateDisplayString } from '../../../../../../../_basic';
import { RenamePopover, OptionsPopover } from '../../../../../../popover';
@@ -153,15 +152,12 @@ const HeaderDropdownMenu = ({ column, renameColumn, modifyColumnData, deleteColu
}, [today, column, isMenuShow, isSubMenuShow, onChangeDateFormat, openSubMenu]);
const renderDropdownMenu = useCallback(() => {
let menuStyle = { transform: 'none' };
const { type } = column;
if (!isFrozen(column)) {
menuStyle['top'] = -5; // - (container padding + menu margin)
menuStyle['left'] = - (column.width - 30); // column width - container width - padding
}
const canModifyColumnData = window.sfMetadataContext.canModifyColumn(column);
const canModifyColumnData = window.sfMetadataContext.canModifyColumnData(column);
const canDeleteColumn = window.sfMetadataContext.canDeleteColumn(column);
const canRenameColumn = window.sfMetadataContext.canRenameColumn(column);
return (
<DropdownMenu style={menuStyle} ref={menuRef} className="sf-metadata-column-dropdown-menu">
<DropdownMenu ref={menuRef} className="sf-metadata-column-dropdown-menu">
<div ref={dropdownDomRef}>
{type === CellType.SINGLE_SELECT && (
<>
@@ -200,7 +196,7 @@ const HeaderDropdownMenu = ({ column, renameColumn, modifyColumnData, deleteColu
<DefaultDropdownItem key="divider-item" divider />
)}
<DropdownItem
disabled={!canModifyColumnData}
disabled={!canRenameColumn}
target="sf-metadata-rename-column"
iconName="rename"
title={gettext('Rename Column')}
@@ -209,7 +205,7 @@ const HeaderDropdownMenu = ({ column, renameColumn, modifyColumnData, deleteColu
onMouseEnter={hideSubMenu}
/>
<DropdownItem
disabled={!canModifyColumnData}
disabled={!canDeleteColumn}
target="sf-metadata-delete-column"
iconName="delete"
title={gettext('Delete Column')}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { UncontrolledTooltip } from 'reactstrap';
import { Icon } from '@seafile/sf-metadata-ui-component';
import { COLUMNS_ICON_CONFIG, COLUMNS_ICON_NAME, PRIVATE_COLUMN_KEYS } from '../../../../../../_basic';
import { COLUMNS_ICON_CONFIG, COLUMNS_ICON_NAME } from '../../../../../../_basic';
import ResizeColumnHandle from './resize-column-handle';
import { EVENT_BUS_TYPE } from '../../../../../../constants';
import DropdownMenu from './dropdown-menu';
@@ -28,7 +28,6 @@ const Cell = ({
const canEditColumnInfo = useMemo(() => {
if (isHideTriangle) return false;
if (PRIVATE_COLUMN_KEYS.includes(column.key)) return false;
return window.sfMetadataContext.canModifyColumn(column);
}, [isHideTriangle, column]);

View File

@@ -1,5 +1,6 @@
import metadataAPI from '../api';
import { UserService, LocalStorage } from './_basic';
import { UserService, LocalStorage, PRIVATE_COLUMN_KEYS, EDITABLE_DATA_PRIVATE_COLUMN_KEYS,
EDITABLE_PRIVATE_COLUMN_KEYS, PREDEFINED_COLUMN_KEYS } from './_basic';
import EventBus from '../../components/common/event-bus';
import { username } from '../../utils/constants';
@@ -12,6 +13,7 @@ class Context {
this.userService = null;
this.eventBus = null;
this.hasInit = false;
this.permission = 'r';
}
async init({ otherSettings }) {
@@ -21,7 +23,7 @@ class Context {
this.settings = otherSettings || {};
// init metadataAPI
const { mediaUrl } = this.settings;
const { mediaUrl, repoInfo } = this.settings;
this.metadataAPI = metadataAPI;
// init localStorage
@@ -34,6 +36,8 @@ class Context {
const eventBus = new EventBus();
this.eventBus = eventBus;
this.permission = repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw';
this.hasInit = true;
}
@@ -44,6 +48,7 @@ class Context {
this.userService = null;
this.eventBus = null;
this.hasInit = false;
this.permission = 'r';
};
getSetting = (key) => {
@@ -81,22 +86,46 @@ class Context {
return this.metadataAPI.getView(repoID, viewId);
};
getPermission = () => {
return this.permission;
};
canModifyCell = (column) => {
if (this.permission === 'r') return false;
const { editable } = column;
if (!editable) return false;
return true;
};
canModifyRow = (row) => {
if (this.permission === 'r') return false;
return true;
};
canModifyColumn = (column) => {
if (this.permission === 'r') return false;
if (PRIVATE_COLUMN_KEYS.includes(column.key) && !EDITABLE_PRIVATE_COLUMN_KEYS.includes(column.key)) return false;
return true;
};
getPermission = () => {
return 'rw';
canRenameColumn = (column) => {
if (this.permission === 'r') return false;
if (PRIVATE_COLUMN_KEYS.includes(column.key)) return false;
return true;
};
canModifyColumnData = (column) => {
if (this.permission === 'r') return false;
const { key } = column;
if (PRIVATE_COLUMN_KEYS.includes(key)) return EDITABLE_DATA_PRIVATE_COLUMN_KEYS.includes(key);
return true;
};
canDeleteColumn = (column) => {
if (this.permission === 'r') return false;
const { key } = column;
if (PRIVATE_COLUMN_KEYS.includes(key)) return PREDEFINED_COLUMN_KEYS.includes(key);
return true;
};
getCollaboratorsFromCache = () => {

View File

@@ -19,6 +19,12 @@ export const CollaboratorsProvider = ({
setCollaborators(store?.collaborators || []);
}, [store?.collaborators]);
useEffect(() => {
if (!window.sfMetadata) return;
window.sfMetadata.collaborators = collaborators;
window.sfMetadata.collaboratorsCache = collaboratorsCache;
}, [collaborators, collaboratorsCache]);
const updateCollaboratorsCache = useCallback((user) => {
const newCollaboratorsCache = { ...collaboratorsCacheRef.current, [user.email]: user };
collaboratorsCacheRef.current = newCollaboratorsCache;

View File

@@ -12,7 +12,6 @@ export const MetadataProvider = ({
children,
repoID,
viewID,
currentRepoInfo,
...params
}) => {
const [isLoading, setLoading] = useState(true);
@@ -37,11 +36,11 @@ export const MetadataProvider = ({
setLoading(true);
// init context
const context = new Context();
window.sfMetadata = {};
window.sfMetadataContext = context;
window.sfMetadataContext.init({ otherSettings: params });
window.sfMetadataContext.setSetting('viewID', viewID);
window.sfMetadataContext.setSetting('repoID', repoID);
window.sfMetadataContext.setSetting('currentRepoInfo', currentRepoInfo);
storeRef.current = new Store({ context: window.sfMetadataContext, repoId: repoID, viewId: viewID });
window.sfMetadataStore = storeRef.current;
storeRef.current.initStartIndex();
@@ -59,6 +58,7 @@ export const MetadataProvider = ({
const unsubscribeUpdateRows = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_TABLE_ROWS, updateMetadata);
return () => {
window.sfMetadata = {};
window.sfMetadataContext.destroy();
window.sfMetadataStore.destroy();
unsubscribeServerTableChanged();
@@ -67,7 +67,7 @@ export const MetadataProvider = ({
unsubscribeUpdateRows();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, viewID, currentRepoInfo]);
}, [repoID, viewID]);
return (
<MetadataContext.Provider value={{ isLoading, metadata, store: storeRef.current }}>

View File

@@ -1,5 +1,5 @@
import { normalizeColumnData } from '../../utils/column-utils';
import { CellType } from '../../_basic';
import { CellType, PRIVATE_COLUMN_KEYS, EDITABLE_PRIVATE_COLUMN_KEYS } from '../../_basic';
class Column {
constructor(object) {
@@ -8,10 +8,15 @@ class Column {
this.type = object.type || '';
this.data = object.data || null;
this.width = object.width || 200;
this.editable = object.editable || !this.key.startsWith('_') && this.type !== CellType.LONG_TEXT || false;
this.editable = this.enable_edit(this.key, this.type);
this.data = normalizeColumnData(this);
}
enable_edit = (key, type) => {
if (PRIVATE_COLUMN_KEYS.includes(key)) return EDITABLE_PRIVATE_COLUMN_KEYS.includes(key);
return type !== CellType.LONG_TEXT;
};
}
export default Column;

View File

@@ -255,26 +255,25 @@ const getFileTypeColumnData = (column) => {
'_audio': { name: gettext('Audio'), color: '#FBD44A', textColor: '#FFFFFF', borderColor: '#E5C142', id: '_audio' },
'_code': { name: gettext('Code'), color: '#4ad8fb', textColor: '#FFFFFF', borderColor: '#4283e5', id: '_code' },
};
let newData = { ...data };
newData.options = Array.isArray(newData.options) ? newData.options.map(o => {
return { ..._OPTIONS[o.name] };
}) : Object.keys(_OPTIONS);
newData.options = Array.isArray(data.options) ? data.options.map(o => {
return _OPTIONS[o.name];
}) : Object.values(_OPTIONS);
return newData;
};
export const getDefaultFileStatusOptions = () => {
return [
{ name: gettext('Draft'), color: '#EED5FF', textColor: '#202428', id: '_draft' },
{ name: gettext('In review'), color: '#FFFDCF', textColor: '#202428', id: '_in_review' },
{ name: gettext('Done'), color: '#59CB74', textColor: '#FFFFFF', borderColor: '#844BD2', id: '_done' },
];
};
const getFileStatusColumnData = (column) => {
const { data } = column;
const _OPTIONS = {
'_draft': { name: gettext('Draft'), color: '#EED5FF', textColor: '#202428', id: '_draft' },
'_in_review': { name: gettext('In review'), color: '#FFFDCF', textColor: '#202428', id: '_in_review' },
'_done': { name: gettext('Done'), color: '#59CB74', textColor: '#FFFFFF', borderColor: '#844BD2', id: '_done' },
};
let newData = { ...data };
newData.options = Array.isArray(newData.options) ? newData.options.map(o => {
return { ..._OPTIONS[o.name] };
}) : Object.keys(_OPTIONS);
newData.options = Array.isArray(data?.options) ? data.options : getDefaultFileStatusOptions();
return newData;
};
@@ -305,7 +304,6 @@ export const normalizeColumns = (columns) => {
type: columnType,
name: getColumnName(key, name),
width: columnsWidth[key] || 200,
editable: !key.startsWith('_') && columnType !== CellType.LONG_TEXT
};
}).filter(column => !NOT_DISPLAY_COLUMN_KEYS.includes(column.key));
let displayColumns = [];
@@ -332,7 +330,6 @@ export function canEdit(col, record, enableCellSelect) {
if (col.editable != null && typeof (col.editable) === 'function') {
return enableCellSelect === true && col.editable(record);
}
console.log(col);
return enableCellSelect === true && !!col.editable;
}