diff --git a/frontend/src/metadata/components/metadata-details/index.js b/frontend/src/metadata/components/metadata-details/index.js index d764882c20..7a06fa29c5 100644 --- a/frontend/src/metadata/components/metadata-details/index.js +++ b/frontend/src/metadata/components/metadata-details/index.js @@ -7,10 +7,10 @@ import DetailItem from '../../../components/dirent-detail/detail-item'; import { Utils } from '../../../utils/utils'; import metadataAPI from '../../api'; import Column from '../../model/metadata/column'; -import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById, getFileNameFromRecord } from '../../utils/cell'; +import { getCellValueByColumn, getOptionName, getColumnOptionNamesByIds, getColumnOptionNameById, getFileNameFromRecord, geRecordIdFromRecord, getFileObjIdFromRecord } from '../../utils/cell'; import { normalizeFields } from './utils'; import { gettext } from '../../../utils/constants'; -import { CellType, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants'; +import { CellType, EVENT_BUS_TYPE, PREDEFINED_COLUMN_KEYS, PRIVATE_COLUMN_KEY } from '../../constants'; import { getColumnOptions, getColumnOriginName } from '../../utils/column'; import { SYSTEM_FOLDERS } from './constants'; import Location from './location'; @@ -22,46 +22,24 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord const [metadata, setMetadata] = useState({ record: {}, fields: [] }); const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]); - useEffect(() => { - setLoading(true); - if (SYSTEM_FOLDERS.find(folderPath => filePath.startsWith(folderPath))) { - setLoading(false); - return; - } - - const dirName = Utils.getDirName(filePath); - const fileName = Utils.getFileName(filePath); - let parentDir = direntType === 'file' ? dirName : dirName.slice(0, dirName.length - fileName.length - 1); - if (!parentDir.startsWith('/')) { - parentDir = '/' + parentDir; - } - metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => { - const { results, metadata } = res.data; - const record = Array.isArray(results) && results.length > 0 ? results[0] : {}; - const fields = normalizeFields(metadata).map(field => new Column(field)); - updateRecord && updateRecord(record); - setMetadata({ record, fields }); - setLoading(false); - }).catch(error => { - const errMessage = Utils.getErrorMsg(error); - toaster.danger(errMessage); - setLoading(false); - }); - }, [repoID, filePath, direntType, updateRecord]); - const onChange = useCallback((fieldKey, newValue) => { const { record, fields } = metadata; const field = fields.find(f => f.key === fieldKey); const fileName = getColumnOriginName(field); + const recordId = geRecordIdFromRecord(record); + const fileObjId = getFileObjIdFromRecord(record); let update = { [fileName]: newValue }; if (field.type === CellType.SINGLE_SELECT) { update = { [fileName]: getColumnOptionNameById(field, newValue) }; } else if (field.type === CellType.MULTIPLE_SELECT) { update = { [fileName]: newValue ? getColumnOptionNamesByIds(field, newValue) : [] }; } - metadataAPI.modifyRecord(repoID, record._id, update, record._obj_id).then(res => { + metadataAPI.modifyRecord(repoID, recordId, update, fileObjId).then(res => { const newMetadata = { ...metadata, record: { ...record, ...update } }; setMetadata(newMetadata); + if (window?.sfMetadataContext?.eventBus) { + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, recordId, update); + } }).catch(error => { const errorMsg = Utils.getErrorMsg(error); toaster.danger(errorMsg); @@ -98,6 +76,48 @@ const MetadataDetails = ({ repoID, filePath, repoInfo, direntType, updateRecord }); }, [repoID, metadata]); + const localRecordChanged = useCallback((recordId, updates) => { + if (geRecordIdFromRecord(metadata?.record) !== recordId) return; + const newMetadata = { ...metadata, record: { ...metadata.record, ...updates } }; + setMetadata(newMetadata); + }, [metadata]); + + useEffect(() => { + setLoading(true); + if (SYSTEM_FOLDERS.find(folderPath => filePath.startsWith(folderPath))) { + setLoading(false); + return; + } + + const dirName = Utils.getDirName(filePath); + const fileName = Utils.getFileName(filePath); + let parentDir = direntType === 'file' ? dirName : dirName.slice(0, dirName.length - fileName.length - 1); + if (!parentDir.startsWith('/')) { + parentDir = '/' + parentDir; + } + metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => { + const { results, metadata } = res.data; + const record = Array.isArray(results) && results.length > 0 ? results[0] : {}; + const fields = normalizeFields(metadata).map(field => new Column(field)); + updateRecord && updateRecord(record); + setMetadata({ record, fields }); + setLoading(false); + }).catch(error => { + const errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + setLoading(false); + }); + }, [repoID, filePath, direntType, updateRecord]); + + useEffect(() => { + const eventBus = window?.sfMetadataContext?.eventBus; + if (!eventBus) return; + const unsubscribeLocalRecordChanged = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, localRecordChanged); + return () => { + unsubscribeLocalRecordChanged(); + }; + }, [localRecordChanged]); + if (isLoading) return null; const { fields, record } = metadata; if (!record._id) return null; diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index 4e6c6663f8..b183c2ca69 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -3,16 +3,16 @@ */ export const EVENT_BUS_TYPE = { - QUERY_COLLABORATORS: 'query-collaborators', - QUERY_COLLABORATOR: 'query-collaborator', - UPDATE_TABLE_ROWS: 'update-table-rows', + QUERY_COLLABORATORS: 'query_collaborators', + QUERY_COLLABORATOR: 'query_collaborator', + UPDATE_TABLE_ROWS: 'update_table_rows', // table - LOCAL_TABLE_CHANGED: 'local-table-changed', - SERVER_TABLE_CHANGED: 'server-table-changed', - TABLE_ERROR: 'table-error', - OPEN_EDITOR: 'open-editor', - CLOSE_EDITOR: 'close-editor', + LOCAL_TABLE_CHANGED: 'local_table_changed', + SERVER_TABLE_CHANGED: 'server_table_changed', + TABLE_ERROR: 'table_error', + OPEN_EDITOR: 'open_editor', + CLOSE_EDITOR: 'close_editor', SELECT_CELL: 'select_cell', SELECT_START: 'select_start', SELECT_UPDATE: 'select_update', @@ -21,14 +21,16 @@ export const EVENT_BUS_TYPE = { SELECT_NONE: 'select_none', COPY_CELLS: 'copy_cells', PASTE_CELLS: 'paste_cells', - SEARCH_CELLS: 'search-cells', - CLOSE_SEARCH_CELLS: 'close-search-cells', - OPEN_SELECT: 'open-select', + SEARCH_CELLS: 'search_cells', + CLOSE_SEARCH_CELLS: 'close_search_cells', + OPEN_SELECT: 'open_select', UPDATE_LINKED_RECORDS: 'update_linked_records', SELECT_COLUMN: 'select_column', DRAG_ENTER: 'drag_enter', COLLAPSE_ALL_GROUPS: 'collapse_all_groups', EXPAND_ALL_GROUPS: 'expand_all_groups', + LOCAL_RECORD_CHANGED: 'local_record_changed', + LOCAL_RECORD_DETAIL_CHANGED: 'local_record_detail_changed', // metadata RELOAD_DATA: 'reload_data', diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index 986ace488c..18dd3efc18 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -70,6 +70,10 @@ export const MetadataViewProvider = ({ window.sfMetadataStore.modifySettings(settings); }, []); + const updateLocalRecord = useCallback((recordId, update) => { + window.sfMetadataStore.modifyLocalRecord(recordId, update); + }, []); + // init useEffect(() => { setLoading(true); @@ -103,6 +107,7 @@ export const MetadataViewProvider = ({ const unsubscribeModifyHiddenColumns = eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_HIDDEN_COLUMNS, modifyHiddenColumns); const unsubscribeModifyColumnOrder = eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_COLUMN_ORDER, modifyColumnOrder); const unsubscribeModifySettings = eventBus.subscribe(EVENT_BUS_TYPE.MODIFY_SETTINGS, modifySettings); + const unsubscribeLocalRecordChanged = eventBus.subscribe(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, updateLocalRecord); return () => { if (window.sfMetadataContext) { @@ -120,6 +125,7 @@ export const MetadataViewProvider = ({ unsubscribeModifyHiddenColumns(); unsubscribeModifyColumnOrder(); unsubscribeModifySettings(); + unsubscribeLocalRecordChanged(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [repoID, viewID]); diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js index e738ad9e6f..295d1aa01c 100644 --- a/frontend/src/metadata/store/index.js +++ b/frontend/src/metadata/store/index.js @@ -408,6 +408,17 @@ class Store { this.applyOperation(operation); } + modifyLocalRecord(row_id, updates) { + const type = OPERATION_TYPE.MODIFY_LOCAL_RECORD; + const operation = this.createOperation({ + type, + row_id, + repo_id: this.repoId, + updates + }); + this.applyOperation(operation); + } + modifyFilters(filterConjunction, filters, basicFilters = []) { const type = OPERATION_TYPE.MODIFY_FILTERS; const operation = this.createOperation({ diff --git a/frontend/src/metadata/store/operations/apply.js b/frontend/src/metadata/store/operations/apply.js index 5f95501a0d..e7f1765ae6 100644 --- a/frontend/src/metadata/store/operations/apply.js +++ b/frontend/src/metadata/store/operations/apply.js @@ -96,6 +96,27 @@ export default function apply(data, operation) { data.id_row_map[row_id] = updatedRow; return data; } + case OPERATION_TYPE.MODIFY_LOCAL_RECORD: { + const { row_id, updates } = operation; + const { rows } = data; + const modifyTime = dayjs().utc().format(UTC_FORMAT_DEFAULT); + const modifier = window.sfMetadataContext.getUsername(); + let updatedRows = [...rows]; + rows.forEach((row, index) => { + const { _id: rowId } = row; + if (rowId === row_id && updates) { + const updatedRow = Object.assign({}, row, updates, { + '_mtime': modifyTime, + '_last_modifier': modifier, + }); + updatedRows[index] = updatedRow; + data.id_row_map[rowId] = updatedRow; + } + }); + + data.rows = updatedRows; + return data; + } case OPERATION_TYPE.MODIFY_FILTERS: { const { filter_conjunction, filters, basic_filters } = operation; data.view.filter_conjunction = filter_conjunction; diff --git a/frontend/src/metadata/store/operations/constants.js b/frontend/src/metadata/store/operations/constants.js index c611cd8c96..02b918c2d5 100644 --- a/frontend/src/metadata/store/operations/constants.js +++ b/frontend/src/metadata/store/operations/constants.js @@ -10,6 +10,7 @@ export const OPERATION_TYPE = { LOCK_RECORD_VIA_BUTTON: 'lock_record_via_button', MODIFY_RECORD_VIA_BUTTON: 'modify_record_via_button', MODIFY_SETTINGS: 'modify_settings', + MODIFY_LOCAL_RECORD: 'modify_local_record', // column INSERT_COLUMN: 'insert_column', @@ -53,6 +54,7 @@ export const OPERATION_ATTRIBUTES = { [OPERATION_TYPE.RENAME_PEOPLE_NAME]: ['repo_id', 'people_id', 'new_name', 'old_name'], [OPERATION_TYPE.DELETE_PEOPLE_PHOTOS]: ['repo_id', 'people_id', 'deleted_photos'], [OPERATION_TYPE.MODIFY_SETTINGS]: ['repo_id', 'view_id', 'settings'], + [OPERATION_TYPE.MODIFY_LOCAL_RECORD]: ['repo_id', 'row_id', 'updates'], }; export const UNDO_OPERATION_TYPE = [ @@ -64,6 +66,7 @@ export const UNDO_OPERATION_TYPE = [ // only apply operation on the local export const LOCAL_APPLY_OPERATION_TYPE = [ OPERATION_TYPE.MODIFY_COLUMN_WIDTH, + OPERATION_TYPE.MODIFY_LOCAL_RECORD, ]; // apply operation after exec operation on the server diff --git a/frontend/src/metadata/views/kanban/boards/board/card/index.css b/frontend/src/metadata/views/kanban/boards/board/card/index.css index 1c57c2eb3d..40f8794a16 100644 --- a/frontend/src/metadata/views/kanban/boards/board/card/index.css +++ b/frontend/src/metadata/views/kanban/boards/board/card/index.css @@ -29,6 +29,7 @@ .sf-metadata-kanban-card .sf-metadata-kanban-card-header .sf-metadata-file-name { text-decoration: none; + flex: unset; } .sf-metadata-kanban-card .sf-metadata-kanban-card-header .sf-metadata-file-name:hover { diff --git a/frontend/src/metadata/views/kanban/boards/board/index.css b/frontend/src/metadata/views/kanban/boards/board/index.css index 2889ce35fb..ccd86f511f 100644 --- a/frontend/src/metadata/views/kanban/boards/board/index.css +++ b/frontend/src/metadata/views/kanban/boards/board/index.css @@ -4,6 +4,14 @@ margin-right: 16px; } +.sf-metadata-view-kanban-board .smooth-dnd-container.vertical { + height: calc(100% - 32px); +} + +.sf-metadata-view-kanban-board .smooth-dnd-container.vertical .smooth-dnd-draggable-wrapper:last-child .sf-metadata-kanban-card { + margin-bottom: 0; +} + .sf-metadata-view-kanban-board .smooth-dnd-container.vertical > .smooth-dnd-draggable-wrapper { overflow: visible; } diff --git a/frontend/src/metadata/views/kanban/boards/board/index.js b/frontend/src/metadata/views/kanban/boards/board/index.js index bfcd2eb77e..8c9a20a29b 100644 --- a/frontend/src/metadata/views/kanban/boards/board/index.js +++ b/frontend/src/metadata/views/kanban/boards/board/index.js @@ -27,6 +27,7 @@ const Board = ({ onUnFreezed, onOpenFile, onSelectCard, + updateDragging, }) => { const [isDraggingOver, setDraggingOver] = useState(false); const boardName = useMemo(() => `sf_metadata_kanban_board_${board.key}`, [board]); @@ -34,8 +35,9 @@ const Board = ({ const { metadata } = useMetadataView(); const onDragStart = useCallback(({ payload }) => { + updateDragging(true); return payload; - }, []); + }, [updateDragging]); const onDragEnd = useCallback((targetBoardIndex, result) => { if (isDraggingOver) { @@ -44,12 +46,11 @@ const Board = ({ const { addedIndex, payload } = result; const { boardIndex: sourceBoardIndex, cardIndex: sourceCardIndex } = payload; - if (sourceBoardIndex === targetBoardIndex) return; - if (sourceCardIndex !== null && addedIndex !== null) { + if (sourceBoardIndex !== targetBoardIndex && sourceCardIndex !== null && addedIndex !== null) { onMove(targetBoardIndex, sourceBoardIndex, sourceCardIndex); } - - }, [isDraggingOver, onMove]); + setTimeout(() => updateDragging(false), 0); + }, [isDraggingOver, onMove, updateDragging]); return (
@@ -120,6 +121,7 @@ Board.propTypes = { onUnFreezed: PropTypes.func, onOpenFile: PropTypes.func.isRequired, onSelectCard: PropTypes.func.isRequired, + updateDragging: PropTypes.func.isRequired, }; export default Board; diff --git a/frontend/src/metadata/views/kanban/boards/index.css b/frontend/src/metadata/views/kanban/boards/index.css index b660169e6c..f63ac6716c 100644 --- a/frontend/src/metadata/views/kanban/boards/index.css +++ b/frontend/src/metadata/views/kanban/boards/index.css @@ -2,12 +2,10 @@ flex: 1; height: 100%; width: 100%; - overflow: hidden; + overflow: auto; } .sf-metadata-view-kanban-boards .smooth-dnd-container.horizontal { - height: 100%; - overflow: auto; white-space: nowrap; display: flex; padding: 16px; diff --git a/frontend/src/metadata/views/kanban/boards/index.js b/frontend/src/metadata/views/kanban/boards/index.js index c0296990fa..5129b21c0c 100644 --- a/frontend/src/metadata/views/kanban/boards/index.js +++ b/frontend/src/metadata/views/kanban/boards/index.js @@ -23,6 +23,7 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => { const [haveFreezed, setHaveFreezed] = useState(false); const [isImagePreviewerVisible, setImagePreviewerVisible] = useState(false); const [selectedCard, setSelectedCard] = useState(''); + const [isDragging, setDragging] = useState(false); const currentImageRef = useRef(null); const containerRef = useRef(null); @@ -188,6 +189,7 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => { const onSelectCard = useCallback((record) => { const recordId = geRecordIdFromRecord(record); + if (selectedCard === recordId) return; const name = getFileNameFromRecord(record); const path = getParentDirFromRecord(record); const isDir = checkIsDir(record); @@ -201,13 +203,17 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => { setSelectedCard(recordId); onCloseSettings(); showDirentDetail(); - }, [onCloseSettings, showDirentDetail, updateCurrentDirent]); + }, [selectedCard, onCloseSettings, showDirentDetail, updateCurrentDirent]); const handleClickOutside = useCallback((event) => { - if (!containerRef.current.contains(event.target)) return; + if (isDragging) return; setSelectedCard(null); updateCurrentDirent(); - }, [updateCurrentDirent]); + }, [isDragging, updateCurrentDirent]); + + const updateDragging = useCallback((isDragging) => { + setDragging(isDragging); + }, []); useEffect(() => { if (!isDirentDetailShow) { @@ -250,6 +256,7 @@ const Boards = ({ modifyRecord, modifyColumnData, onCloseSettings }) => { onUnFreezed={onUnFreezed} onOpenFile={onOpenFile} onSelectCard={onSelectCard} + updateDragging={updateDragging} /> ); })} diff --git a/frontend/src/metadata/views/kanban/index.js b/frontend/src/metadata/views/kanban/index.js index 928678a33b..b91e921f96 100644 --- a/frontend/src/metadata/views/kanban/index.js +++ b/frontend/src/metadata/views/kanban/index.js @@ -25,7 +25,8 @@ const Kanban = () => { error && toaster.danger(error); }, success_callback: () => { - // do nothing + const eventBus = window.sfMetadataContext.eventBus; + eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, rowId, updates); }, }); }, [store]);