From 65127e335a727d714efae75fc1a2f8fe3d656921 Mon Sep 17 00:00:00 2001 From: mdelsinne44 Date: Thu, 19 Sep 2024 17:01:54 +0200 Subject: [PATCH 1/3] Add support for copying and pasting features in the feature grid - Implemented [`copyFeatures`] epic to handle the COPY_FEATURES action: - Retrieves the current state from the store. - Uses to get the selected features. - Stores the selected features in [`localStorage`] as a JSON string to facilitate copying to the clipboard. - Implemented [`pasteFeature`] epic to handle the PASTE_FEATURES action: - Currently returns an empty observable, ready for future implementation. - Will retrieve the copied features from [`localStorage`] for pasting. --- web/client/actions/featuregrid.js | 26 ++++++++++++++++++ .../data/featuregrid/toolbars/Toolbar.jsx | 27 +++++++++++++++++++ web/client/epics/featuregrid.js | 23 ++++++++++++++++ .../plugins/featuregrid/panels/index.jsx | 2 ++ .../plugins/featuregrid/toolbarEvents.js | 4 +++ web/client/reducers/featuregrid.js | 11 ++++++++ web/client/selectors/featuregrid.js | 1 + web/client/translations/data.en-US.json | 2 ++ web/client/translations/data.fr-FR.json | 2 ++ 9 files changed, 98 insertions(+) diff --git a/web/client/actions/featuregrid.js b/web/client/actions/featuregrid.js index ec62c14b6c..67e977d162 100644 --- a/web/client/actions/featuregrid.js +++ b/web/client/actions/featuregrid.js @@ -15,6 +15,8 @@ export const TOGGLE_MODE = 'FEATUREGRID:TOGGLE_MODE'; export const TOGGLE_FEATURES_SELECTION = 'FEATUREGRID:TOGGLE_FEATURES_SELECTION'; export const FEATURES_MODIFIED = 'FEATUREGRID:FEATURES_MODIFIED'; export const CREATE_NEW_FEATURE = "FEATUREGRID:NEW_FEATURE"; +export const COPY_FEATURES = "FEATUREGRID:COPY_FEATURES"; // added to support copy features +export const PASTE_FEATURES = "FEATUREGRID:PASTE_FEATURES"; // added to support paste features export const SAVE_CHANGES = 'FEATUREGRID:SAVE_CHANGES'; export const SAVING = 'FEATUREGRID:SAVING'; export const START_EDITING_FEATURE = 'FEATUREGRID:START_EDITING_FEATURE'; @@ -268,6 +270,30 @@ export function createNewFeatures(features) { features }; } + +/** + * Copy features to clipboard + * @returns {object} action + */ +export function copyFeatures() { + return { + type: COPY_FEATURES + }; +} + +/** + * Paste features from clipboard + * @returns {object} action + */ +export function pasteFeatures() { + const stringifiedFeatures = localStorage.getItem('features'); + const featuresJSON = JSON.parse(stringifiedFeatures); + return { + type: PASTE_FEATURES, + featuresJSON + }; +} + export function saveChanges() { return { type: SAVE_CHANGES diff --git a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx index 47a5b15e19..f7b4409da6 100644 --- a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx +++ b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx @@ -45,6 +45,14 @@ const standardButtons = { visible={mode === "VIEW" && showAdvancedFilterButton} onClick={events.showQueryPanel} glyph="filter"/>), + copyFeaturesViewMode: ({disabled, mode, hasGeometry, selectedCount, hasSupportedGeometry = true, events = {}}) => ( 0} + onClick={events.copyFeatures} + glyph="duplicate"/>), zoomAll: ({disabled, disableZoomAll = false, mode, events = {}}) => (), + copyFeaturesEditMode: ({disabled, mode, hasGeometry, hasNewFeatures, hasChanges, selectedCount, hasSupportedGeometry = true, events = {}}) => ( 0} + onClick={events.copyFeatures} + glyph="duplicate"/>), + pasteFeatures: ({disabled, mode, copiedCount, hasNewFeatures, hasChanges, hasSupportedGeometry = true, events = {}}) => ( 0} + onClick={events.pasteFeatures} + glyph="paste"/>), addFeature: ({disabled, mode, hasNewFeatures, hasChanges, hasSupportedGeometry = true, events = {}}) => ( action$.ofType(G return Rx.Observable.of(geometryChanged(changedFeatures)).concat(enableEdit); }); + +/** + * copy feature action flow + * @memberof epics.featuregrid + */ +export const copyFeatures = (action$, store) => action$.ofType(COPY_FEATURES) + .switchMap( () => { + const state = store.getState(); + // setter + localStorage.setItem('features', JSON.stringify(selectedFeaturesSelector(state))); + return Rx.Observable.empty(); + }); +/** + * paste geometry action flow + * @memberof epics.featuregrid + */ +export const pasteFeature = (action$) => action$.ofType(PASTE_FEATURES) + .switchMap( () => { + return Rx.Observable.empty(); + }); + /** * Manage delete geometry action flow * @memberof epics.featuregrid diff --git a/web/client/plugins/featuregrid/panels/index.jsx b/web/client/plugins/featuregrid/panels/index.jsx index d07fc5af1b..57f7188722 100644 --- a/web/client/plugins/featuregrid/panels/index.jsx +++ b/web/client/plugins/featuregrid/panels/index.jsx @@ -40,6 +40,7 @@ import { isSavingSelector, isSimpleGeomSelector, modeSelector, + copiedFeaturesCount, selectedFeaturesCount, selectedLayerNameSelector, showAgainSelector, @@ -79,6 +80,7 @@ const Toolbar = connect( saved: isSavedSelector, mode: modeSelector, hasChanges: hasChangesSelector, + copiedCount: copiedFeaturesCount, hasNewFeatures: hasNewFeaturesSelector, hasGeometry: hasGeometrySelector, syncPopover: (state) => ({ diff --git a/web/client/plugins/featuregrid/toolbarEvents.js b/web/client/plugins/featuregrid/toolbarEvents.js index 1422729a1c..fe75457269 100644 --- a/web/client/plugins/featuregrid/toolbarEvents.js +++ b/web/client/plugins/featuregrid/toolbarEvents.js @@ -7,6 +7,8 @@ import { setTimeSync, toggleShowAgain, createNewFeatures, + copyFeatures, + pasteFeatures, startEditingFeature, startDrawingFeature, deleteGeometry, @@ -23,6 +25,8 @@ import { export default { createFeature: () => createNewFeatures([{}]), + copyFeatures: () => copyFeatures(), + pasteFeatures: () => pasteFeatures([{}]), saveChanges: () => saveChanges(), clearFeatureEditing: () => toggleTool("clearConfirm", true), deleteGeometry: () => deleteGeometry(), diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js index 4ecbd3438f..4337e7e6c7 100644 --- a/web/client/reducers/featuregrid.js +++ b/web/client/reducers/featuregrid.js @@ -17,6 +17,7 @@ import { SET_FEATURES, FEATURES_MODIFIED, CREATE_NEW_FEATURE, + PASTE_FEATURES, SAVING, SAVE_SUCCESS, SAVE_ERROR, @@ -284,6 +285,16 @@ function featuregrid(state = emptyResultsState, action) { })) }); } + case PASTE_FEATURES: { + let id = uuid.v1(); + return assign({}, state, { + newFeatures: action.featuresJSON.map(f => ({...f, _new: true, id: f.id ? f.id : id, type: "Feature", + geometry: f.geometry, + properties: f.properties + })), + select: [] + }); + } case SAVE_ERROR: { return assign({}, state, { deleteConfirm: false, diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js index 8f460fa483..aefafae7f8 100644 --- a/web/client/selectors/featuregrid.js +++ b/web/client/selectors/featuregrid.js @@ -137,6 +137,7 @@ export const getCustomizedAttributes = state => { }; export const modeSelector = state => state && state.featuregrid && state.featuregrid.mode; export const selectedFeaturesCount = state => (selectedFeaturesSelector(state) || []).length; +export const copiedFeaturesCount = JSON.parse(localStorage.getItem('features'))?.length; export const changesMapSelector = state => toChangesMap(changesSelector(state)); export const hasGeometrySelector = state => hasGeometrySelectedFeature(state); export const showAgainSelector = state => get(state, "featuregrid.showAgain", false); diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 142808850b..2400b25f75 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -1898,6 +1898,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Sync map with filter ", "synchPopoverText": "Use this tool to synchronize the map with the selected filter", "notShowAgain": " Don't show this message again", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index dd4c09fbb2..a3662e04d2 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -1899,6 +1899,8 @@ } }, "toolbar": { + "copyFeatures": "Copier les entités", + "pasteFeatures": "Coller les entités", "synchPopoverTitle": "Synchroniser la carte avec un filtre ", "synchPopoverText": "Utilisez cet outil pour synchroniser la carte avec le filtre sélectionné", "notShowAgain": " Ne plus afficher ce message", From 95a6737dd5f879d82c65471d71509c875feb19e8 Mon Sep 17 00:00:00 2001 From: mdelsinne44 Date: Fri, 20 Sep 2024 14:13:21 +0200 Subject: [PATCH 2/3] bug fix on copiedFeatureCount --- web/client/selectors/featuregrid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js index aefafae7f8..8bb3bfd2fa 100644 --- a/web/client/selectors/featuregrid.js +++ b/web/client/selectors/featuregrid.js @@ -137,7 +137,7 @@ export const getCustomizedAttributes = state => { }; export const modeSelector = state => state && state.featuregrid && state.featuregrid.mode; export const selectedFeaturesCount = state => (selectedFeaturesSelector(state) || []).length; -export const copiedFeaturesCount = JSON.parse(localStorage.getItem('features'))?.length; +export const copiedFeaturesCount = state => state && JSON.parse(localStorage.getItem('features'))?.length; export const changesMapSelector = state => toChangesMap(changesSelector(state)); export const hasGeometrySelector = state => hasGeometrySelectedFeature(state); export const showAgainSelector = state => get(state, "featuregrid.showAgain", false); From 62f78c70ad2896c9e388954870cc2a4f719081a7 Mon Sep 17 00:00:00 2001 From: mdelsinne44 Date: Fri, 20 Sep 2024 14:24:36 +0200 Subject: [PATCH 3/3] add translations --- web/client/translations/data.da-DK.json | 2 ++ web/client/translations/data.de-DE.json | 2 ++ web/client/translations/data.es-ES.json | 2 ++ web/client/translations/data.fi-FI.json | 2 ++ web/client/translations/data.hr-HR.json | 2 ++ web/client/translations/data.is-IS.json | 2 ++ web/client/translations/data.it-IT.json | 2 ++ web/client/translations/data.nl-NL.json | 2 ++ web/client/translations/data.pt-PT.json | 2 ++ web/client/translations/data.sk-SK.json | 2 ++ web/client/translations/data.sv-SE.json | 2 ++ web/client/translations/data.vi-VN.json | 2 ++ web/client/translations/data.zh-ZH.json | 2 ++ 13 files changed, 26 insertions(+) diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json index 264a792164..94e0f98abf 100644 --- a/web/client/translations/data.da-DK.json +++ b/web/client/translations/data.da-DK.json @@ -1733,6 +1733,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Sync map with filter ", "synchPopoverText": "Use this tool to synchronize the map with the selected filter", "notShowAgain": " Don't show this message again", diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 93e34246d9..343286babd 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -1937,6 +1937,8 @@ } }, "toolbar": { + "copyFeatures": "Funktionen kopieren", + "pasteFeatures": "Funktionen einfügen", "synchPopoverTitle": "Karte mit Filter synchronisieren ", "synchPopoverText": "Verwenden Sie dieses Werkzeug, um die Karte mit dem ausgewählten Filter zu synchronisieren", "notShowAgain": "Diese Nachricht nicht erneut anzeigen", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 8cf00a2f41..1560132053 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -1899,6 +1899,8 @@ } }, "toolbar": { + "copyFeatures": "Copiar entidades", + "pasteFeatures": "Pegar entidades", "synchPopoverTitle": "Sincronizar mapa con filtro ", "synchPopoverText": "Utilice esta herramienta para sincronizar el mapa con el filtro seleccionado", "notShowAgain": " No mostrar este mensaje de nuevo", diff --git a/web/client/translations/data.fi-FI.json b/web/client/translations/data.fi-FI.json index 1c50b8c287..ab8747d0c9 100644 --- a/web/client/translations/data.fi-FI.json +++ b/web/client/translations/data.fi-FI.json @@ -1397,6 +1397,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Synkronoi kartta suodattimen kanssa ", "synchPopoverText": "Käytä tätä työkalua synkronoidaksesi kartta valitun suodattimen kanssa", "notShowAgain": "Älä näytä tätä viestiä uudelleen", diff --git a/web/client/translations/data.hr-HR.json b/web/client/translations/data.hr-HR.json index ab967049f4..2b04322090 100644 --- a/web/client/translations/data.hr-HR.json +++ b/web/client/translations/data.hr-HR.json @@ -1354,6 +1354,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Sinkroniziraj kartu sa filtrom ", "synchPopoverText": "Koristi ovaj alat za sinkronizaciju karte sa odabranim filtrom", "notShowAgain": " Ne prikazuj više ovu poruku", diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json index 7fae728924..574481d182 100644 --- a/web/client/translations/data.is-IS.json +++ b/web/client/translations/data.is-IS.json @@ -1757,6 +1757,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Sync map with filter ", "synchPopoverText": "Use this tool to synchronize the map with the selected filter", "notShowAgain": " Don't show this message again", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 18064a3674..9c12ee4aad 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -1898,6 +1898,8 @@ } }, "toolbar": { + "copyFeatures": "Copia entità", + "pasteFeatures": "Incolla entità", "synchPopoverTitle": "Sincronizza la mappa con il filtro ", "synchPopoverText": "Usa questa icona per sincronizzare la mappa con il filtro selezionato", "notShowAgain": " Non mostrare più", diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json index 5b5e48d667..1f6b2449c8 100644 --- a/web/client/translations/data.nl-NL.json +++ b/web/client/translations/data.nl-NL.json @@ -1891,6 +1891,8 @@ } }, "toolbar": { + "copyFeatures": "Functies kopiëren", + "pasteFeatures": "Functies plakken", "synchPopoverTitle": "Synchroniseer kaart met filter ", "synchPopoverText": "Gebruik deze tool om de kaart te synchroniseren met de geselecteerde filter", "notShowAgain": " Laat dit bericht niet meer zien", diff --git a/web/client/translations/data.pt-PT.json b/web/client/translations/data.pt-PT.json index 8181ada449..8a525ae897 100644 --- a/web/client/translations/data.pt-PT.json +++ b/web/client/translations/data.pt-PT.json @@ -1330,6 +1330,8 @@ } }, "toolbar": { + "copyFeatures": "Copiar entidades", + "pasteFeatures": "Colar entidades", "synchPopoverTitle": "Sync map with filter ", "synchPopoverText": "Use this tool to synchronize the map with the selected filter", "notShowAgain": " Don't show this message again", diff --git a/web/client/translations/data.sk-SK.json b/web/client/translations/data.sk-SK.json index 658c7f8e63..588229288f 100644 --- a/web/client/translations/data.sk-SK.json +++ b/web/client/translations/data.sk-SK.json @@ -1638,6 +1638,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Synchronizovať mapu s filtrom ", "synchPopoverText": "Použi tento nástroj pre synchronizáciu mapy s vybraným filtrom", "notShowAgain": " Túto správu už nezobrazovať", diff --git a/web/client/translations/data.sv-SE.json b/web/client/translations/data.sv-SE.json index c77073f50b..6d478dfe12 100644 --- a/web/client/translations/data.sv-SE.json +++ b/web/client/translations/data.sv-SE.json @@ -1618,6 +1618,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Synkronisera karta med filter", "synchPopoverText": "Använd det här verktyget för att synkronisera kartan med det valda filtret", "notShowAgain": "Visa inte detta meddelande igen", diff --git a/web/client/translations/data.vi-VN.json b/web/client/translations/data.vi-VN.json index 80cbc1703d..d1eb954622 100644 --- a/web/client/translations/data.vi-VN.json +++ b/web/client/translations/data.vi-VN.json @@ -427,6 +427,8 @@ "resultInfoVirtual": "{total, plural, =0 {không mục nào} =1 {{total} Mục số {total}} other {{total} Mục}}", "selectall": "Chọn tất cả", "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "addGeom": "Thêm một hình cho hình hiện có", "addNewFeatures": "Thêm tính năng mới", "advancedFilter": "Tìm kiếm nâng cao", diff --git a/web/client/translations/data.zh-ZH.json b/web/client/translations/data.zh-ZH.json index 4f136adee9..17dfa7d237 100644 --- a/web/client/translations/data.zh-ZH.json +++ b/web/client/translations/data.zh-ZH.json @@ -1307,6 +1307,8 @@ } }, "toolbar": { + "copyFeatures": "Copy features", + "pasteFeatures": "Paste features", "synchPopoverTitle": "Sync map with filter ", "synchPopoverText": "Use this tool to synchronize the map with the selected filter", "notShowAgain": " Don't show this message again",