1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-10-21 19:00:12 +00:00

[metadata] 'card' view: added a 'settings' panel (#8267)

This commit is contained in:
llj
2025-09-26 18:18:02 +08:00
committed by GitHub
parent 966058ff15
commit f5f4c30d96
10 changed files with 120 additions and 95 deletions

View File

@@ -63,7 +63,7 @@ const CardViewToolbar = ({
{!readOnly && (
<IconBtn
symbol="set-up"
className="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-setting d-none"
className="sf-metadata-view-tool-operation-btn sf-metadata-view-tool-setting"
size={24}
role="button"
aria-label={gettext('Settings')}

View File

@@ -168,6 +168,13 @@ export const KANBAN_SETTINGS_KEYS = {
COLUMNS: 'columns', // display and order
};
export const CARD_SETTINGS_KEYS = {
HIDE_EMPTY_VALUE: 'hide_empty_value',
SHOW_COLUMN_NAME: 'show_column_name',
TEXT_WRAP: 'text_wrap',
COLUMNS: 'columns', // display and order
};
export const VIEW_DEFAULT_SETTINGS = {
[VIEW_TYPE.TABLE]: {},
[VIEW_TYPE.GALLERY]: {},
@@ -180,7 +187,12 @@ export const VIEW_DEFAULT_SETTINGS = {
[KANBAN_SETTINGS_KEYS.TEXT_WRAP]: false,
[KANBAN_SETTINGS_KEYS.COLUMNS]: [],
},
[VIEW_TYPE.CARD]: {}
[VIEW_TYPE.CARD]: {
[CARD_SETTINGS_KEYS.HIDE_EMPTY_VALUE]: false,
[CARD_SETTINGS_KEYS.SHOW_COLUMN_NAME]: false,
[CARD_SETTINGS_KEYS.TEXT_WRAP]: false,
[CARD_SETTINGS_KEYS.COLUMNS]: [],
}
};
export const VIEW_PROPERTY_KEYS = {

View File

@@ -30,6 +30,25 @@
font-weight: 500;
}
.sf-metadata-card-item .sf-metadata-card-item-field {
margin-top: 10px;
}
.sf-metadata-card-item .sf-metadata-card-item-field-name {
display: block;
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.sf-metadata-card-item .sf-metadata-card-item-field .sf-metadata-record-cell-empty {
display: inline-block;
background-color: #f0f0f0;
border-radius: 4px;
height: 8px;
width: 20px;
}
.sf-metadata-card-item .sf-metadata-ui {
overflow: hidden;
text-overflow: ellipsis;
@@ -65,37 +84,37 @@
transform: translateY(-1px);
}
.sf-metadata-card-item .sf-metadata-card-item-record .collaborators-formatter {
.sf-metadata-card-item .sf-metadata-card-item-field .collaborators-formatter {
display: flex;
flex-wrap: wrap;
}
.sf-metadata-card-item .sf-metadata-card-item-record .collaborators-formatter .collaborator-item {
.sf-metadata-card-item .sf-metadata-card-item-field .collaborators-formatter .collaborator-item {
margin-top: 2px;
margin-bottom: 2px;
}
.sf-metadata-card-item .sf-metadata-card-item-record .sf-metadata-checkbox-formatter {
.sf-metadata-card-item .sf-metadata-card-item-field .sf-metadata-checkbox-formatter {
justify-content: flex-start;
padding-left: 2px;
}
.sf-metadata-card-item .sf-metadata-card-item-record .sf-metadata-multiple-select-formatter {
.sf-metadata-card-item .sf-metadata-card-item-field .sf-metadata-multiple-select-formatter {
flex-wrap: wrap;
}
.sf-metadata-card-item .sf-metadata-card-item-record .sf-metadata-multiple-select-formatter .sf-metadata-ui-select-option {
.sf-metadata-card-item .sf-metadata-card-item-field .sf-metadata-multiple-select-formatter .sf-metadata-ui-select-option {
margin-top: 2px;
margin-bottom: 2px;
}
.sf-metadata-view-kanban-boards-text-wrap .sf-metadata-card-item-record .text-formatter {
.sf-metadata-view-card-items-container-text-wrap .sf-metadata-card-item-field .text-formatter {
white-space: normal;
max-height: 68px;
}
.sf-metadata-card-item .long-text-formatter,
.sf-metadata-view-kanban-boards-text-wrap .sf-metadata-card-item-record .long-text-formatter {
.sf-metadata-view-card-items-container-text-wrap .sf-metadata-card-item-field .long-text-formatter {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;

View File

@@ -4,6 +4,7 @@ import classnames from 'classnames';
import Formatter from './formatter';
import {
getCellValueByColumn,
isValidCellValue,
getParentDirFromRecord, getFileMTimeFromRecord
} from '../../../../utils/cell';
import { Utils } from '../../../../../utils/utils';
@@ -18,6 +19,9 @@ const CardItem = ({
tagsData,
fileNameColumn,
mtimeColumn,
displayColumns,
displayEmptyValue,
displayColumnName,
onOpenFile,
onSelectCard,
onContextMenu,
@@ -80,6 +84,28 @@ const CardItem = ({
<div className="sf-metadata-card-item-text-container">
<Formatter value={fileNameValue} column={fileNameColumn} record={record} onFileNameClick={handleFilenameClick} tagsData={tagsData} />
<Formatter value={mtimeValue} format="relativeTime" column={mtimeColumn} record={record} tagsData={tagsData} />
{displayColumns.map((column) => {
const value = getCellValueByColumn(record, column);
if (!displayEmptyValue && !isValidCellValue(value)) {
if (displayColumnName) {
return (
<div className="sf-metadata-card-item-field" key={column.key}>
<span className="sf-metadata-card-item-field-name">{column.name}</span>
</div>
);
}
return null;
}
return (
<div className="sf-metadata-card-item-field" key={column.key}>
{displayColumnName && (
<span className="sf-metadata-card-item-field-name">{column.name}</span>
)}
<Formatter value={value} column={column} record={record} tagsData={tagsData} />
</div>
);
})}
</div>
</article>
);

View File

@@ -1,3 +1,4 @@
.sf-metadata-view-card-items-container {
padding: 20px;
grid-gap: 20px;
}

View File

@@ -2,6 +2,7 @@ import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react'
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useMetadataView } from '../../../hooks/metadata-view';
import { CARD_SETTINGS_KEYS } from '../../../constants';
import { gettext } from '../../../../utils/constants';
import { getRecordIdFromRecord, getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
import { openFile } from '../../../utils/file';
@@ -36,6 +37,18 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
return metadata.key_column_map['_file_mtime'];
}, [metadata.key_column_map]);
const displayColumns = useMemo(() => {
const displayColumnsConfig = metadata.view.settings[CARD_SETTINGS_KEYS.COLUMNS];
if (!displayColumnsConfig) return [];
return displayColumnsConfig
.filter(config => config.shown)
.map(config => metadata.key_column_map[config.key]);
}, [metadata.key_column_map, metadata.view.settings]);
const displayEmptyValue = useMemo(() => !metadata.view.settings[CARD_SETTINGS_KEYS.HIDE_EMPTY_VALUE], [metadata.view.settings]);
const displayColumnName = useMemo(() => metadata.view.settings[CARD_SETTINGS_KEYS.SHOW_COLUMN_NAME], [metadata.view.settings]);
const textWrap = useMemo(() => metadata.view.settings[CARD_SETTINGS_KEYS.TEXT_WRAP], [metadata.view.settings]);
const records = useMemo(() => {
const { rows } = metadata;
return rows || [];
@@ -126,7 +139,8 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
<>
<div
ref={containerRef}
className={classnames('sf-metadata-view-card-items-container d-flex flex-wrap', {
className={classnames('sf-metadata-view-card-items-container d-flex flex-wrap h-100 o-auto', {
'sf-metadata-view-card-items-container-text-wrap': textWrap
})}
onClick={handleClickOutside}
>
@@ -140,6 +154,9 @@ const CardItems = ({ modifyRecord, deleteRecords, modifyColumnData, onCloseSetti
tagsData={tagsData}
fileNameColumn={fileNameColumn}
mtimeColumn={mtimeColumn}
displayColumns={displayColumns}
displayEmptyValue={displayEmptyValue}
displayColumnName={displayColumnName}
onOpenFile={onOpenFile}
onSelectCard={onSelectCard}
onContextMenu={(e) => onContextMenu(e, record._id)}

View File

@@ -1,3 +0,0 @@
.sf-metadata-view-card {
padding: 20px;
}

View File

@@ -4,12 +4,7 @@ import { EVENT_BUS_TYPE } from '../../constants';
import CardItems from './card-items';
import Settings from './settings';
import './index.css';
const Card = () => {
// Thu Sep 11 18:29:51 CST 2025
// 'Settings' will be implemented later
const [isShowSettings, setShowSettings] = useState(false);
const {
@@ -46,7 +41,7 @@ const Card = () => {
return (
<div className="sf-metadata-container">
<div className="sf-metadata-view-card flex-fill o-auto">
<div className="sf-metadata-view-card flex-fill o-hidden position-relative">
<CardItems
modifyRecord={modifyRecord}
deleteRecords={deleteRecords}
@@ -54,7 +49,7 @@ const Card = () => {
onCloseSettings={closeSettings}
/>
{isShowSettings && (
<div className="sf-metadata-view-setting-panel sf-metadata-view-card-setting h-100">
<div className="sf-metadata-view-setting-panel sf-metadata-view-card-setting h-100 position-absolute end-0 top-0">
<Settings
columns={columns}
columnsMap={metadata.key_column_map}

View File

@@ -1,4 +1,4 @@
.sf-metadata-view-kanban-setting-panel {
.sf-metadata-view-card-setting-panel {
min-width: 300px;
height: 100%;
display: flex;
@@ -7,7 +7,7 @@
background: var(--bs-body-bg);
}
.sf-metadata-view-kanban-setting-panel .setting-panel-header {
.sf-metadata-view-card-setting-panel .setting-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -15,7 +15,7 @@
border-bottom: 1px solid var(--bs-border-secondary-color);
}
.sf-metadata-view-kanban-setting-panel .setting-panel-body {
.sf-metadata-view-card-setting-panel .setting-panel-body {
width: 100%;
height: 100%;
padding: 16px 16px 50px;
@@ -23,26 +23,26 @@
overflow: auto;
}
.sf-metadata-view-kanban-setting-panel .setting-panel-body .setting-item {
.sf-metadata-view-card-setting-panel .setting-panel-body .setting-item {
margin-bottom: 14px;
}
.sf-metadata-view-kanban-setting-panel .setting-item .setting-item-label {
.sf-metadata-view-card-setting-panel .setting-item .setting-item-label {
display: inline-block;
margin-bottom: 8px;
}
.sf-metadata-view-kanban-setting-panel .setting-item .custom-switch-description {
.sf-metadata-view-card-setting-panel .setting-item .custom-switch-description {
margin: 0;
}
.sf-metadata-view-kanban-setting-panel .setting-item .switch-setting-item {
.sf-metadata-view-card-setting-panel .setting-item .switch-setting-item {
padding: 0;
display: flex;
justify-content: space-between;
}
.sf-metadata-view-kanban-setting-panel .custom-switch {
.sf-metadata-view-card-setting-panel .custom-switch {
width: 100%;
padding: 0;
display: flex;
@@ -51,18 +51,18 @@
cursor: pointer;
}
.sf-metadata-view-kanban-setting-panel .custom-switch-indicator{
.sf-metadata-view-card-setting-panel .custom-switch-indicator {
border-radius: 6px;
height: 12px;
width: 22px;
margin-right: 8px;
}
.sf-metadata-view-kanban-setting-panel .custom-switch .custom-switch-indicator:before {
.sf-metadata-view-card-setting-panel .custom-switch .custom-switch-indicator:before {
height: 8px;
width: 8px;
}
.sf-metadata-view-kanban-setting-panel .custom-switch .custom-switch-input:checked~.custom-switch-indicator:before {
.sf-metadata-view-card-setting-panel .custom-switch .custom-switch-input:checked~.custom-switch-indicator:before {
left: 12px;
}

View File

@@ -1,12 +1,10 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import Icon from '../../../../components/icon';
import OpIcon from '../../../../components/op-icon';
import Switch from '../../../../components/switch';
import Selector from '../../../components/selector';
import FieldDisplaySettings from '../../../components/data-process-setter/field-display-settings';
import { gettext } from '../../../../utils/constants';
import { CellType, COLUMNS_ICON_CONFIG, KANBAN_SETTINGS_KEYS } from '../../../constants';
import { COLUMNS_ICON_CONFIG, CARD_SETTINGS_KEYS } from '../../../constants';
import { getColumnByKey } from '../../../utils/column';
import { useMetadataStatus } from '../../../../hooks';
@@ -21,45 +19,26 @@ const Settings = ({
}) => {
const { globalHiddenColumns } = useMetadataStatus();
const validColumns = useMemo(() => columns.filter(column => !globalHiddenColumns.includes(column.key)), [columns, globalHiddenColumns]);
const groupByColumnOptions = useMemo(() => {
return validColumns
.filter(col => col.type === CellType.SINGLE_SELECT || col.type === CellType.COLLABORATOR)
.map(col => ({
value: col.key,
label: (
<>
<span className="sf-metadata-select-icon"><Icon className="sf-metadata-icon" symbol={COLUMNS_ICON_CONFIG[col.type]} /></span>
<span>{col.name}</span>
</>
)
}));
}, [validColumns]);
const titleColumnOptions = useMemo(() => {
return validColumns
.map(col => ({
value: col.key,
label: (
<>
<span className="sf-metadata-select-icon"><Icon className="sf-metadata-icon" symbol={COLUMNS_ICON_CONFIG[col.type]} /></span>
<span>{col.name}</span>
</>
)
}));
}, [validColumns]);
const displayColumns = useMemo(() => {
const displayColumnsConfig = settings[KANBAN_SETTINGS_KEYS.COLUMNS].filter(column => !globalHiddenColumns.includes(column.key));
const titleColumnKey = settings[KANBAN_SETTINGS_KEYS.TITLE_COLUMN_KEY];
const filteredColumns = validColumns.filter(item => item.key !== titleColumnKey);
if (!displayColumnsConfig) return filteredColumns.map(column => ({ ...column, shown: false }));
const displayColumnsConfig = settings[CARD_SETTINGS_KEYS.COLUMNS].filter(column => !globalHiddenColumns.includes(column.key));
const nameColumnKey = '_name';
const mtimeColumnKey = '_file_mtime';
const filteredColumns = validColumns.filter(item => item.key !== nameColumnKey && item.key !== mtimeColumnKey);
if (!displayColumnsConfig) {
return filteredColumns.map(column => ({ ...column, shown: false }));
}
const validDisplayColumnsConfig = displayColumnsConfig.map(columnConfig => {
const column = columnsMap[columnConfig.key];
if (column) return { ...column, shown: columnConfig.shown };
return null;
}).filter(column => column && column.key !== titleColumnKey);
}).filter(column => column && column.key !== nameColumnKey && column.key !== mtimeColumnKey);
const addedColumns = filteredColumns
.filter(column => !getColumnByKey(validDisplayColumnsConfig, column.key))
.map(column => ({ ...column, shown: false }));
return [...validDisplayColumnsConfig, ...addedColumns];
}, [validColumns, columnsMap, settings, globalHiddenColumns]);
@@ -74,7 +53,7 @@ const Settings = ({
if (columnConfig.key === key) return { ...columnConfig, shown };
return columnConfig;
});
handleUpdateSettings(KANBAN_SETTINGS_KEYS.COLUMNS, newDisplayColumnsConfig);
handleUpdateSettings(CARD_SETTINGS_KEYS.COLUMNS, newDisplayColumnsConfig);
}, [displayColumnsConfig, handleUpdateSettings]);
const onMoveField = useCallback((sourceKey, targetKey) => {
@@ -84,63 +63,42 @@ const Settings = ({
if (sourceIndex === -1 || targetIndex === -1) return;
newDisplayColumnsConfig.splice(sourceIndex, 1, displayColumnsConfig[targetIndex]);
newDisplayColumnsConfig.splice(targetIndex, 1, displayColumnsConfig[sourceIndex]);
handleUpdateSettings(KANBAN_SETTINGS_KEYS.COLUMNS, newDisplayColumnsConfig);
handleUpdateSettings(CARD_SETTINGS_KEYS.COLUMNS, newDisplayColumnsConfig);
}, [displayColumnsConfig, handleUpdateSettings]);
const onToggleFieldsVisibility = useCallback((visibility) => {
const newDisplayColumnsConfig = displayColumnsConfig.map(columnConfig => ({ ...columnConfig, shown: visibility }));
handleUpdateSettings(KANBAN_SETTINGS_KEYS.COLUMNS, newDisplayColumnsConfig);
handleUpdateSettings(CARD_SETTINGS_KEYS.COLUMNS, newDisplayColumnsConfig);
}, [displayColumnsConfig, handleUpdateSettings]);
return (
<div className="sf-metadata-view-kanban-setting-panel">
<div className="sf-metadata-view-card-setting-panel">
<div className="setting-panel-header">
<h5 className="m-0">{gettext('Settings')}</h5>
<OpIcon className='sf3-font sf3-font-x-01 op-icon' op={onClose} title={gettext('Close')} />
</div>
<div className="setting-panel-body">
<div className="setting-item">
<span className="setting-item-label">{gettext('Group by')}</span>
<Selector
settingKey={KANBAN_SETTINGS_KEYS.GROUP_BY_COLUMN_KEY}
value={settings[KANBAN_SETTINGS_KEYS.GROUP_BY_COLUMN_KEY]}
defaultValue={groupByColumnOptions[0]?.value}
options={groupByColumnOptions}
onChange={handleUpdateSettings}
/>
</div>
<div className="sf-metadata-setting-divide-line"></div>
<div className="setting-item">
<span className="setting-item-label">{gettext('Title property')}</span>
<Selector
settingKey={KANBAN_SETTINGS_KEYS.TITLE_COLUMN_KEY}
value={settings[KANBAN_SETTINGS_KEYS.TITLE_COLUMN_KEY]}
options={titleColumnOptions}
onChange={handleUpdateSettings}
/>
</div>
<div className="sf-metadata-setting-divide-line"></div>
<div className="setting-item">
<Switch
placeholder={gettext('Don\'t show empty values')}
checked={settings[KANBAN_SETTINGS_KEYS.HIDE_EMPTY_VALUE] || false}
onChange={() => handleUpdateSettings(KANBAN_SETTINGS_KEYS.HIDE_EMPTY_VALUE, !settings[KANBAN_SETTINGS_KEYS.HIDE_EMPTY_VALUE])}
checked={settings[CARD_SETTINGS_KEYS.HIDE_EMPTY_VALUE] || false}
onChange={() => handleUpdateSettings(CARD_SETTINGS_KEYS.HIDE_EMPTY_VALUE, !settings[CARD_SETTINGS_KEYS.HIDE_EMPTY_VALUE])}
/>
</div>
<div className="sf-metadata-setting-divide-line"></div>
<div className="setting-item">
<Switch
placeholder={gettext('Show property names')}
checked={settings[KANBAN_SETTINGS_KEYS.SHOW_COLUMN_NAME] || false}
onChange={() => handleUpdateSettings(KANBAN_SETTINGS_KEYS.SHOW_COLUMN_NAME, !settings[KANBAN_SETTINGS_KEYS.SHOW_COLUMN_NAME])}
checked={settings[CARD_SETTINGS_KEYS.SHOW_COLUMN_NAME] || false}
onChange={() => handleUpdateSettings(CARD_SETTINGS_KEYS.SHOW_COLUMN_NAME, !settings[CARD_SETTINGS_KEYS.SHOW_COLUMN_NAME])}
/>
</div>
<div className="sf-metadata-setting-divide-line"></div>
<div className="setting-item">
<Switch
placeholder={gettext('Text wraps')}
checked={settings[KANBAN_SETTINGS_KEYS.TEXT_WRAP] || false}
onChange={() => handleUpdateSettings(KANBAN_SETTINGS_KEYS.TEXT_WRAP, !settings[KANBAN_SETTINGS_KEYS.TEXT_WRAP])}
checked={settings[CARD_SETTINGS_KEYS.TEXT_WRAP] || false}
onChange={() => handleUpdateSettings(CARD_SETTINGS_KEYS.TEXT_WRAP, !settings[CARD_SETTINGS_KEYS.TEXT_WRAP])}
/>
</div>
<div className="sf-metadata-setting-divide-line"></div>