diff --git a/frontend/src/metadata/store/index.js b/frontend/src/metadata/store/index.js index a07a40a2f5d..64a08bd3978 100644 --- a/frontend/src/metadata/store/index.js +++ b/frontend/src/metadata/store/index.js @@ -622,23 +622,6 @@ class Store { }); this.applyOperation(operation); }; - - // map - deleteLocationPhotos = (rows_ids) => { - if (!Array.isArray(rows_ids) || rows_ids.length === 0) return; - - const type = OPERATION_TYPE.DELETE_LOCATION_PHOTOS; - const valid_rows_ids = rows_ids.filter((rowId) => { - const row = getRowById(this.data, rowId); - return row && this.context.canModifyRow(row); - }); - const deleted_rows = valid_rows_ids.map((rowId) => getRowById(this.data, rowId)); - const operation = this.createOperation({ - type, repo_id: this.repoId, rows_ids, deleted_rows - }); - this.applyOperation(operation); - }; - } export default Store; diff --git a/frontend/src/metadata/store/operations/apply.js b/frontend/src/metadata/store/operations/apply.js index 64df8f7eca7..7e809ea710b 100644 --- a/frontend/src/metadata/store/operations/apply.js +++ b/frontend/src/metadata/store/operations/apply.js @@ -335,21 +335,6 @@ export default function apply(data, operation) { return data; } - // map - case OPERATION_TYPE.DELETE_LOCATION_PHOTOS: { - const { rows_ids } = operation; - const idNeedDeletedMap = rows_ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); - data.rows = data.rows.filter((row) => !idNeedDeletedMap[row._id]); - data.recordsCount = data.rows.length; - // delete rows in id_row_map - rows_ids.forEach(rowId => { - delete data.id_row_map[rowId]; - }); - - data.row_ids = data.row_ids.filter(row_id => !idNeedDeletedMap[row_id]); - return data; - } - default: { return data; } diff --git a/frontend/src/metadata/store/operations/constants.js b/frontend/src/metadata/store/operations/constants.js index ee4ce655038..2872778e2a8 100644 --- a/frontend/src/metadata/store/operations/constants.js +++ b/frontend/src/metadata/store/operations/constants.js @@ -34,9 +34,6 @@ export const OPERATION_TYPE = { // tag UPDATE_FILE_TAGS: 'update_file_tags', - - // map - DELETE_LOCATION_PHOTOS: 'delete_location_photos', }; export const COLUMN_DATA_OPERATION_TYPE = { @@ -77,8 +74,6 @@ export const OPERATION_ATTRIBUTES = { [OPERATION_TYPE.MODIFY_SETTINGS]: ['repo_id', 'view_id', 'settings'], [OPERATION_TYPE.UPDATE_FILE_TAGS]: ['repo_id', 'file_tags_data'], - - [OPERATION_TYPE.DELETE_LOCATION_PHOTOS]: ['repo_id', 'rows_ids', 'deleted_rows'], }; export const UNDO_OPERATION_TYPE = [ diff --git a/frontend/src/metadata/views/map/cluster-photos/index.js b/frontend/src/metadata/views/map/cluster-photos/index.js index 921b1645e70..4fc69e70c2d 100644 --- a/frontend/src/metadata/views/map/cluster-photos/index.js +++ b/frontend/src/metadata/views/map/cluster-photos/index.js @@ -1,114 +1,43 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import deepCopy from 'deep-copy'; -import { CenteredLoading } from '@seafile/sf-metadata-ui-component'; import Gallery from '../../gallery/main'; import { gettext } from '../../../../utils/constants'; -import { EVENT_BUS_TYPE, PER_LOAD_NUMBER } from '../../../constants'; +import { EVENT_BUS_TYPE } from '../../../constants'; import metadataAPI from '../../../api'; -import { normalizeColumns } from '../../../utils/column'; -import Metadata from '../../../model/metadata'; import { Utils } from '../../../../utils/utils'; import toaster from '../../../../components/toast'; import { useMetadataView } from '../../../hooks/metadata-view'; import './index.css'; -const ClusterPhotos = ({ view, markerIds, onClose, onDelete }) => { - const [isLoading, setLoading] = useState(true); - const [metadata, setMetadata] = useState({ rows: [] }); - - const { deleteFilesCallback } = useMetadataView(); +const ClusterPhotos = ({ metadata, markerIds, onClose }) => { + const { store, duplicateRecord, addFolder } = useMetadataView(); const repoID = window.sfMetadataContext.getSetting('repoID'); - const loadData = useCallback((view) => { - setLoading(true); - const params = { - view_id: view._id, - start: 0, - limit: PER_LOAD_NUMBER, - }; - metadataAPI.getMetadata(repoID, params).then(res => { - const rows = res?.data?.results || []; - const filteredRows = rows.filter(row => markerIds.includes(row._id)); - const columns = normalizeColumns(res?.data?.metadata); - const metadata = new Metadata({ rows: filteredRows, columns, view }); - metadata.hasMore = rows.length >= PER_LOAD_NUMBER; - setMetadata(metadata); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); - setLoading(false); - }).catch(error => { - const errorMessage = Utils.getErrorMsg(error); - toaster.danger(errorMessage); - setLoading(false); - }); - }, [repoID, markerIds]); - - const deletedByIds = useCallback((ids) => { - if (!Array.isArray(ids) || ids.length === 0) return; + const clusterMetadata = useMemo(() => { + const filteredRows = metadata.rows.filter(row => markerIds.includes(row._id)); const newMetadata = deepCopy(metadata); - const idNeedDeletedMap = ids.reduce((currIdNeedDeletedMap, rowId) => ({ ...currIdNeedDeletedMap, [rowId]: true }), {}); - newMetadata.rows = newMetadata.rows.filter((row) => !idNeedDeletedMap[row._id]); - newMetadata.row_ids = newMetadata.row_ids.filter((id) => !idNeedDeletedMap[id]); - - // delete rows in id_row_map - ids.forEach(rowId => { - delete newMetadata.id_row_map[rowId]; - }); - newMetadata.recordsCount = newMetadata.row_ids.length; - setMetadata(newMetadata); - - if (newMetadata.rows.length === 0) { - onClose && onClose(); - } - - onDelete(ids); - }, [metadata, onClose, onDelete]); + newMetadata.rows = filteredRows; + return newMetadata; + }, [metadata, markerIds]); const handelDelete = useCallback((deletedImages, { success_callback } = {}) => { if (!deletedImages.length) return; let recordIds = []; - let paths = []; - let fileNames = []; - deletedImages.forEach((record) => { - const { id, parentDir, name } = record || {}; - if (parentDir && name) { - const path = Utils.joinPath(parentDir, name); - paths.push(path); - fileNames.push(name); - recordIds.push(id); - } - }); - window.sfMetadataContext.batchDeleteFiles(repoID, paths).then(res => { - deletedByIds(recordIds); - deleteFilesCallback(paths, fileNames); - let msg = fileNames.length > 1 - ? gettext('Successfully deleted {name} and {n} other items') - : gettext('Successfully deleted {name}'); - msg = msg.replace('{name}', fileNames[0]) - .replace('{n}', fileNames.length - 1); - toaster.success(msg); - success_callback && success_callback(); - }).catch(error => { - toaster.danger(gettext('Failed to delete records')); - }); - }, [deleteFilesCallback, repoID, deletedByIds]); + deletedImages.forEach((record) => recordIds.push(record.id)); + store.deleteRecords(recordIds, { success_callback }); + }, [store]); const handleViewChange = useCallback((update) => { - metadataAPI.modifyView(repoID, view._id, update).then(res => { - const newView = { ...view, ...update }; - loadData(newView); + metadataAPI.modifyView(repoID, metadata.view._id, update).then(res => { + }).catch(error => { const errorMessage = Utils.getErrorMsg(error); toaster.danger(errorMessage); }); - }, [view, repoID, loadData]); - - useEffect(() => { - loadData({ _id: view._id, sorts: view.sorts }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [metadata, repoID,]); useEffect(() => { const unsubscribeViewChange = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.MAP_GALLERY_VIEW_CHANGE, handleViewChange); @@ -119,27 +48,22 @@ const ClusterPhotos = ({ view, markerIds, onClose, onDelete }) => { }, []); return ( - isLoading ? ( - - ) : ( -
-
-
- -
-
{gettext('Location')}
+
+
+
+
- +
{gettext('Location')}
- ) + +
); }; ClusterPhotos.propTypes = { - view: PropTypes.object, + metadata: PropTypes.object, markerIds: PropTypes.array, onClose: PropTypes.func, - onDelete: PropTypes.func, }; export default ClusterPhotos; diff --git a/frontend/src/metadata/views/map/index.js b/frontend/src/metadata/views/map/index.js index cf57e2bda61..a4d51bf9eb7 100644 --- a/frontend/src/metadata/views/map/index.js +++ b/frontend/src/metadata/views/map/index.js @@ -14,7 +14,7 @@ import './index.css'; const Map = () => { const [showGallery, setShowGallery] = useState(false); const [markerIds, setMarkerIds] = useState([]); - const { metadata, store } = useMetadataView(); + const { metadata } = useMetadataView(); const repoID = window.sfMetadataContext.getSetting('repoID'); @@ -51,10 +51,6 @@ const Map = () => { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.TOGGLE_MAP_VIEW_TOOLBAR, false); }, []); - const onDeleteLocationPhotos = useCallback((ids) => { - store.deleteLocationPhotos(ids); - }, [store]); - useEffect(() => { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.MAP_VIEW, metadata.view); }, [metadata.view]); @@ -62,7 +58,7 @@ const Map = () => { return ( <> {showGallery ? ( - + ) : (
)} diff --git a/frontend/src/metadata/views/map/main.js b/frontend/src/metadata/views/map/main.js index d1f04d94c0c..e9770b9fc48 100644 --- a/frontend/src/metadata/views/map/main.js +++ b/frontend/src/metadata/views/map/main.js @@ -71,10 +71,26 @@ const Main = ({ validImages, onOpen }) => { } }, []); + const saveMapState = useCallback(() => { + if (!mapRef.current) return; + const point = mapRef.current.getCenter && mapRef.current.getCenter(); + const zoom = mapRef.current.getZoom && mapRef.current.getZoom(); + window.sfMetadataContext.localStorage.setItem('map-center', point); + window.sfMetadataContext.localStorage.setItem('map-zoom', zoom); + }, []); + + const loadMapState = useCallback(() => { + const savedCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; + const savedZoom = window.sfMetadataContext.localStorage.getItem('map-zoom') || DEFAULT_ZOOM; + return { center: savedCenter, zoom: savedZoom }; + }, []); + const onClickMarker = useCallback((e, markers) => { + saveMapState(); + const imageIds = markers.map(marker => marker._imageId); onOpen(imageIds); - }, [onOpen]); + }, [onOpen, saveMapState]); const renderMarkersBatch = useCallback(() => { if (!validImages.length || !clusterRef.current) return; @@ -110,23 +126,23 @@ const Main = ({ validImages, onOpen }) => { const renderBaiduMap = useCallback(() => { if (!mapRef.current || !window.BMap.Map) return; - let mapCenter = window.sfMetadataContext.localStorage.getItem('map-center') || DEFAULT_POSITION; + let { center, zoom } = loadMapState(); // ask for user location if (navigator.geolocation) { navigator.geolocation.getCurrentPosition((userInfo) => { - mapCenter = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; - window.sfMetadataContext.localStorage.setItem('map-center', mapCenter); + center = { lng: userInfo.coords.longitude, lat: userInfo.coords.latitude }; + window.sfMetadataContext.localStorage.setItem('map-center', center); }); } - if (!isValidPosition(mapCenter?.lng, mapCenter?.lat)) return; + if (!isValidPosition(center?.lng, center?.lat)) return; - const gcPosition = wgs84_to_gcj02(mapCenter.lng, mapCenter.lat); + const gcPosition = wgs84_to_gcj02(center.lng, center.lat); const bdPosition = gcj02_to_bd09(gcPosition.lng, gcPosition.lat); const { lng, lat } = bdPosition; mapRef.current = new window.BMap.Map('sf-metadata-map-container', { enableMapClick: false }); const point = new window.BMap.Point(lng, lat); - mapRef.current.centerAndZoom(point, DEFAULT_ZOOM); + mapRef.current.centerAndZoom(point, zoom); mapRef.current.enableScrollWheelZoom(true); const savedValue = window.sfMetadataContext.localStorage.getItem('map-type'); @@ -138,7 +154,7 @@ const Main = ({ validImages, onOpen }) => { batchIndexRef.current = 0; renderMarkersBatch(); - }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch, getMapType]); + }, [addMapController, initializeClusterer, initializeUserMarker, renderMarkersBatch, getMapType, loadMapState]); useEffect(() => { const switchMapTypeSubscribe = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.SWITCH_MAP_TYPE, (newType) => { @@ -150,7 +166,7 @@ const Main = ({ validImages, onOpen }) => { switchMapTypeSubscribe(); }; - }, [getMapType]); + }, [getMapType, saveMapState]); useEffect(() => { if (mapInfo.type === MAP_PROVIDER.B_MAP) { diff --git a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js index b58ecd096ea..b882bfbe114 100644 --- a/frontend/src/metadata/views/map/overlay/custom-image-overlay.js +++ b/frontend/src/metadata/views/map/overlay/custom-image-overlay.js @@ -30,13 +30,30 @@ const customImageOverlay = (center, image, callback) => { this._div.append(label); const eventHandler = (event) => { - event.stopPropagation(); event.preventDefault(); this._callback && this._callback(event, [{ _imageId: this._imageId }]); }; if (Utils.isDesktop()) { - this._div.addEventListener('click', eventHandler); + let clickTimeout; + this._div.addEventListener('click', (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { + eventHandler(event); + clickTimeout = null; + }, 300); + }); + this._div.addEventListener('dblclick', (e) => { + e.preventDefault(); + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + }); } else { this._div.addEventListener('touchend', eventHandler); } diff --git a/media/js/map/marker-clusterer.js b/media/js/map/marker-clusterer.js index 5f7e3100f0d..41f1aca44f1 100644 --- a/media/js/map/marker-clusterer.js +++ b/media/js/map/marker-clusterer.js @@ -580,14 +580,29 @@ var BMapLib = window.BMapLib = BMapLib || {}; var thatMap = this._map; var thatBounds = this.getBounds(); - this._clusterMarker.addEventListener("click",(event) => { - // thatMap.setViewport(thatBounds); + let clickTimeout; + this._clusterMarker.addEventListener("click", (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { if (this._markerClusterer && typeof this._markerClusterer.getCallback() === 'function') { - const markers = this._markers; - this._markerClusterer.getCallback()(event, markers); + const markers = this._markers; + this._markerClusterer.getCallback()(event, markers); } + clickTimeout = null; + }, 300); // Delay to differentiate between single and double click }); + this._clusterMarker.addEventListener("dblclick", (event) => { + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + // Do nothing on double click + }); }; /**