mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-26 15:26:19 +00:00
table location editor (#8130)
Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
@@ -13,6 +13,7 @@ const POPUP_EDITOR_COLUMN_TYPES = [
|
|||||||
CellType.LONG_TEXT,
|
CellType.LONG_TEXT,
|
||||||
CellType.LINK,
|
CellType.LINK,
|
||||||
CellType.TAGS,
|
CellType.TAGS,
|
||||||
|
CellType.GEOLOCATION,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PREVIEW_EDITOR_COLUMN_TYPES = [
|
const PREVIEW_EDITOR_COLUMN_TYPES = [
|
||||||
|
@@ -4,7 +4,7 @@ import classnames from 'classnames';
|
|||||||
import ClickOutside from '../../../../components/click-outside';
|
import ClickOutside from '../../../../components/click-outside';
|
||||||
import Editor from '../editor';
|
import Editor from '../editor';
|
||||||
import { Utils } from '../../../../utils/utils';
|
import { Utils } from '../../../../utils/utils';
|
||||||
import { isCellValueChanged, getCellValueByColumn, getColumnOptionNameById, getColumnOptionNamesByIds } from '../../../utils/cell';
|
import { isCellValueChanged, getCellValueByColumn, getColumnOptionNameById, getColumnOptionNamesByIds, getFileNameFromRecord } from '../../../utils/cell';
|
||||||
import { canEditCell, getColumnOriginName } from '../../../utils/column';
|
import { canEditCell, getColumnOriginName } from '../../../utils/column';
|
||||||
import { CellType, metadataZIndexes, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEYS } from '../../../constants';
|
import { CellType, metadataZIndexes, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEYS } from '../../../constants';
|
||||||
|
|
||||||
@@ -65,6 +65,14 @@ class PopupEditorContainer extends React.Component {
|
|||||||
createEditor = () => {
|
createEditor = () => {
|
||||||
const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData, onSelectTag, onDeselectTag } = this.props;
|
const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData, onSelectTag, onDeselectTag } = this.props;
|
||||||
const readOnly = !canEditCell(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type);
|
const readOnly = !canEditCell(column, record, true) || NOT_SUPPORT_EDITOR_COLUMN_TYPES.includes(column.type);
|
||||||
|
|
||||||
|
if (column.type === CellType.GEOLOCATION) {
|
||||||
|
const fileName = getFileNameFromRecord(record);
|
||||||
|
if (!Utils.imageCheck(fileName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const value = this.getInitialValue(readOnly);
|
const value = this.getInitialValue(readOnly);
|
||||||
|
|
||||||
let editorProps = {
|
let editorProps = {
|
||||||
@@ -146,10 +154,12 @@ class PopupEditorContainer extends React.Component {
|
|||||||
const columnName = getColumnOriginName(column);
|
const columnName = getColumnOriginName(column);
|
||||||
const { key: columnKey } = column;
|
const { key: columnKey } = column;
|
||||||
let oldValue = originalOldCellValue;
|
let oldValue = originalOldCellValue;
|
||||||
if (this.getEditor().getOldValue) {
|
|
||||||
|
if (this.getEditor() && this.getEditor().getOldValue) {
|
||||||
const original = this.getEditor().getOldValue();
|
const original = this.getEditor().getOldValue();
|
||||||
oldValue = original[Object.keys(original)[0]];
|
oldValue = original[Object.keys(original)[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldRowData = { [columnName]: oldValue };
|
const oldRowData = { [columnName]: oldValue };
|
||||||
const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue }
|
const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue }
|
||||||
return { oldRowData, originalOldRowData };
|
return { oldRowData, originalOldRowData };
|
||||||
@@ -161,6 +171,16 @@ class PopupEditorContainer extends React.Component {
|
|||||||
if (!record._id) return;
|
if (!record._id) return;
|
||||||
const { key: columnKey, type: columnType } = column;
|
const { key: columnKey, type: columnType } = column;
|
||||||
if (columnType === CellType.TAGS) return;
|
if (columnType === CellType.TAGS) return;
|
||||||
|
if (columnType === CellType.GEOLOCATION) {
|
||||||
|
// For non-image files, there might be no editor, so check before calling onClose
|
||||||
|
if (this.getEditor() && this.getEditor().onClose) {
|
||||||
|
this.getEditor().onClose();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.getEditor()) return;
|
||||||
|
|
||||||
const newValue = this.getEditor().getValue();
|
const newValue = this.getEditor().getValue();
|
||||||
let updated = columnType === CellType.DATE ? { [columnKey]: newValue } : newValue;
|
let updated = columnType === CellType.DATE ? { [columnKey]: newValue } : newValue;
|
||||||
if (columnType === CellType.SINGLE_SELECT) {
|
if (columnType === CellType.SINGLE_SELECT) {
|
||||||
@@ -203,6 +223,8 @@ class PopupEditorContainer extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
isNewValueValid = (value) => {
|
isNewValueValid = (value) => {
|
||||||
|
if (!this.getEditor()) return true;
|
||||||
|
|
||||||
if (Utils.isFunction(this.getEditor().validate)) {
|
if (Utils.isFunction(this.getEditor().validate)) {
|
||||||
const isValid = this.getEditor().validate(value);
|
const isValid = this.getEditor().validate(value);
|
||||||
this.setState({ isInvalid: !isValid });
|
this.setState({ isInvalid: !isValid });
|
||||||
@@ -229,6 +251,12 @@ class PopupEditorContainer extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const editor = this.createEditor();
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickOutside onClickOutside={this.onClickOutside}>
|
<ClickOutside onClickOutside={this.onClickOutside}>
|
||||||
<div
|
<div
|
||||||
@@ -237,7 +265,7 @@ class PopupEditorContainer extends React.Component {
|
|||||||
onContextMenu={this.handleRightClick}
|
onContextMenu={this.handleRightClick}
|
||||||
ref={this.props.innerRef}
|
ref={this.props.innerRef}
|
||||||
>
|
>
|
||||||
{this.createEditor()}
|
{editor}
|
||||||
</div>
|
</div>
|
||||||
</ClickOutside>
|
</ClickOutside>
|
||||||
);
|
);
|
||||||
|
@@ -9,6 +9,7 @@ import MultipleSelectEditor from './multiple-select-editor';
|
|||||||
import CollaboratorEditor from './collaborator-editor';
|
import CollaboratorEditor from './collaborator-editor';
|
||||||
import LongTextEditor from './long-text-editor';
|
import LongTextEditor from './long-text-editor';
|
||||||
import TagsEditor from './tags-editor';
|
import TagsEditor from './tags-editor';
|
||||||
|
import TableGeolocationEditor from './geolocation-editor/table-geolocation-editor';
|
||||||
import { lang } from '../../../utils/constants';
|
import { lang } from '../../../utils/constants';
|
||||||
import { CellType } from '../../constants';
|
import { CellType } from '../../constants';
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ const Editor = React.forwardRef((props, ref) => {
|
|||||||
case CellType.TAGS: {
|
case CellType.TAGS: {
|
||||||
return (<TagsEditor ref={ref} { ...props } />);
|
return (<TagsEditor ref={ref} { ...props } />);
|
||||||
}
|
}
|
||||||
|
case CellType.GEOLOCATION: {
|
||||||
|
return (<TableGeolocationEditor ref={ref} { ...props } />);
|
||||||
|
}
|
||||||
case CellType.LINK: {
|
case CellType.LINK: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
.sf-table-geolocation-editor {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-table-geolocation-editor .text-muted {
|
||||||
|
color: var(--bs-body-secondary-color) !important;
|
||||||
|
}
|
@@ -0,0 +1,171 @@
|
|||||||
|
import React, { useState, useCallback, forwardRef, useImperativeHandle, useEffect, useRef } from 'react';
|
||||||
|
import { Modal } from 'reactstrap';
|
||||||
|
import GeolocationEditor from './index';
|
||||||
|
import { EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../../constants';
|
||||||
|
import { getRecordIdFromRecord } from '../../../utils/cell';
|
||||||
|
import toaster from '../../../../components/toast';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
|
||||||
|
import './table-geolocation-editor.css';
|
||||||
|
|
||||||
|
const TableGeolocationEditor = forwardRef(({ value, onCommit, onClose, record, column, columns, ...props }, ref) => {
|
||||||
|
const [isFullScreen, setFullScreen] = useState(false);
|
||||||
|
const [currentValue, setCurrentValue] = useState(value);
|
||||||
|
const [isReadyToEraseLocation, setReadyToEraseLocation] = useState(false);
|
||||||
|
const [editorStyle, setEditorStyle] = useState({});
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onClose: () => closeEditor()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Calculate viewport-aware positioning
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFullScreen && editorRef.current) {
|
||||||
|
const editorElement = editorRef.current;
|
||||||
|
const parent = editorElement.parentElement;
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
const parentRect = parent.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
|
||||||
|
const editorWidth = 500;
|
||||||
|
const editorHeight = 434;
|
||||||
|
|
||||||
|
const cellWidth = parentRect.width;
|
||||||
|
const cellHeight = parentRect.height;
|
||||||
|
|
||||||
|
let adjustedStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: '1050',
|
||||||
|
width: `${editorWidth}px`,
|
||||||
|
height: `${editorHeight}px`
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = (cellWidth - editorWidth) / 2;
|
||||||
|
let top = cellHeight + 5;
|
||||||
|
|
||||||
|
const rightEdge = parentRect.left + left + editorWidth;
|
||||||
|
if (rightEdge > viewportWidth) {
|
||||||
|
left = viewportWidth - parentRect.left - editorWidth - 10;
|
||||||
|
}
|
||||||
|
if (parentRect.left + left < 0) {
|
||||||
|
left = -parentRect.left + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bottomEdge = parentRect.top + top + editorHeight;
|
||||||
|
if (bottomEdge > viewportHeight) {
|
||||||
|
top = -editorHeight - 5;
|
||||||
|
|
||||||
|
if (parentRect.top + top < 0) {
|
||||||
|
top = -parentRect.top + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustedStyle.left = `${left}px`;
|
||||||
|
adjustedStyle.top = `${top}px`;
|
||||||
|
|
||||||
|
setEditorStyle(adjustedStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isFullScreen]);
|
||||||
|
|
||||||
|
const closeEditor = useCallback(() => {
|
||||||
|
if (isReadyToEraseLocation) {
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
const recordId = getRecordIdFromRecord(record);
|
||||||
|
|
||||||
|
window.sfMetadataContext.modifyRecord(repoID, recordId, {
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED]: null
|
||||||
|
}).then(() => {
|
||||||
|
return window.sfMetadataContext.modifyRecord(repoID, recordId, {
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION]: null
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
setCurrentValue(null);
|
||||||
|
setReadyToEraseLocation(false);
|
||||||
|
if (window.sfMetadataContext?.eventBus) {
|
||||||
|
const updates = {
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION]: null,
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED]: null
|
||||||
|
};
|
||||||
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { recordId }, updates);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
toaster.danger(gettext('Failed to modify records'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose && onClose();
|
||||||
|
}, [isReadyToEraseLocation, onClose, record]);
|
||||||
|
|
||||||
|
const onFullScreen = useCallback(() => {
|
||||||
|
setFullScreen(!isFullScreen);
|
||||||
|
}, [isFullScreen]);
|
||||||
|
|
||||||
|
const onSubmit = useCallback((locationData) => {
|
||||||
|
const { position, location_translated } = locationData;
|
||||||
|
|
||||||
|
// Update location data using direct API calls
|
||||||
|
const repoID = window.sfMetadataContext.getSetting('repoID');
|
||||||
|
const recordId = getRecordIdFromRecord(record);
|
||||||
|
|
||||||
|
// First update location_translated, then location
|
||||||
|
window.sfMetadataContext.modifyRecord(repoID, recordId, {
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED]: location_translated
|
||||||
|
}).then(() => {
|
||||||
|
return window.sfMetadataContext.modifyRecord(repoID, recordId, {
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION]: position
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
setCurrentValue(position);
|
||||||
|
setFullScreen(false);
|
||||||
|
onClose && onClose();
|
||||||
|
|
||||||
|
// Dispatch local update event for real-time UI updates
|
||||||
|
if (window.sfMetadataContext?.eventBus) {
|
||||||
|
const updates = {
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION]: position,
|
||||||
|
[PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED]: location_translated
|
||||||
|
};
|
||||||
|
window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { recordId }, updates);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
toaster.danger(gettext('Failed to modify records'));
|
||||||
|
});
|
||||||
|
}, [record, onClose]);
|
||||||
|
|
||||||
|
const onReadyToEraseLocation = useCallback(() => {
|
||||||
|
setReadyToEraseLocation(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-table-geolocation-editor" ref={editorRef} style={editorStyle}>
|
||||||
|
{!isFullScreen ? (
|
||||||
|
<GeolocationEditor
|
||||||
|
position={currentValue}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onFullScreen={onFullScreen}
|
||||||
|
onReadyToEraseLocation={onReadyToEraseLocation}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Modal
|
||||||
|
size='lg'
|
||||||
|
isOpen={true}
|
||||||
|
toggle={onFullScreen}
|
||||||
|
zIndex={1052}
|
||||||
|
>
|
||||||
|
<GeolocationEditor
|
||||||
|
position={currentValue}
|
||||||
|
isFullScreen={isFullScreen}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onFullScreen={onFullScreen}
|
||||||
|
onReadyToEraseLocation={onReadyToEraseLocation}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TableGeolocationEditor;
|
Reference in New Issue
Block a user