From 9ebabb9648134d524a86e4db515a10690f0be5b9 Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Fri, 27 Sep 2024 11:42:44 +0200 Subject: [PATCH 1/8] Map: renamed altitude into elevation --- core/i18n/resources/en/common.js | 2 +- server/modules/geo/api/geoApi.js | 16 ++++++++-------- webapp/service/api/index.js | 2 +- webapp/service/api/map/index.js | 4 ++-- .../CoordinateAttributePopUp.js | 14 +++++++------- .../SamplingPointDataItemPopup.js | 6 +++--- .../common/{useAltitude.js => useElevation.js} | 14 +++++++------- 7 files changed, 29 insertions(+), 29 deletions(-) rename webapp/views/App/views/Data/MapView/common/{useAltitude.js => useElevation.js} (60%) diff --git a/core/i18n/resources/en/common.js b/core/i18n/resources/en/common.js index 55a73726f8..0404a3d223 100644 --- a/core/i18n/resources/en/common.js +++ b/core/i18n/resources/en/common.js @@ -805,8 +805,8 @@ Merge cannot be performed.`, mapView: { createRecord: 'Create new record', editRecord: 'Edit record', - altitude: 'Altitude (m)', earthMap: 'Earth Map', + elevation: 'Elevation (m)', locationEditInfo: 'Double click on the map or drag the marker to update the location', locationUpdated: 'Location updated', options: { diff --git a/server/modules/geo/api/geoApi.js b/server/modules/geo/api/geoApi.js index a12aa925cb..5bb00bb0e0 100644 --- a/server/modules/geo/api/geoApi.js +++ b/server/modules/geo/api/geoApi.js @@ -14,8 +14,8 @@ import { PlanetApi } from './planetApi' const uriPrefix = '/survey/:surveyId/geo/' const whispApiUrl = 'https://whisp.openforis.org/api/' -// free altitude API urls -const altitudeApiUrls = [ +// free elevation API urls +const elevationApiUrls = [ ({ lat, lng }) => `https://api.opentopodata.org/v1/aster30m?locations=${lat},${lng}`, ({ lat, lng }) => `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lng}`, ] @@ -78,20 +78,20 @@ export const init = (app) => { } ) - app.get(`${uriPrefix}map/altitude`, AuthMiddleware.requireMapUsePermission, async (req, res) => { + app.get(`${uriPrefix}map/elevation`, AuthMiddleware.requireMapUsePermission, async (req, res) => { const { lat, lng } = Request.getParams(req) - let altitude = null - await Promises.each(altitudeApiUrls, async (urlPattern) => { - if (!Objects.isEmpty(altitude)) return + let elevation = null + await Promises.each(elevationApiUrls, async (urlPattern) => { + if (!Objects.isEmpty(elevation)) return try { const url = urlPattern({ lat, lng }) const { data } = await axios.get(url, { timeout: 10000 }) - altitude = data?.results?.[0]?.elevation + elevation = data?.results?.[0]?.elevation } catch (error) { // ignore it } }) - res.json(altitude) + res.json(elevation) }) app.get(`${uriPrefix}map/wmts/capabilities`, AuthMiddleware.requireMapUsePermission, async (req, res, next) => { diff --git a/webapp/service/api/index.js b/webapp/service/api/index.js index c34a3f14f4..2f43707b9c 100644 --- a/webapp/service/api/index.js +++ b/webapp/service/api/index.js @@ -55,7 +55,7 @@ export { updateDataQuerySummary, deleteDataQuerySummary, } from './dataQuery' -export { fetchAvailableMapPeriods, fetchAltitude, testMapApiKey, fetchMapWmtsCapabilities } from './map' +export { fetchAvailableMapPeriods, fetchElevation, testMapApiKey, fetchMapWmtsCapabilities } from './map' export { fetchSurveyFull, fetchSurveys, diff --git a/webapp/service/api/map/index.js b/webapp/service/api/map/index.js index 77aceeb5d2..c53a059b89 100644 --- a/webapp/service/api/map/index.js +++ b/webapp/service/api/map/index.js @@ -67,9 +67,9 @@ export const fetchAvailableMapPeriods = async ({ provider, periodType }) => { return null } -export const fetchAltitude = async ({ surveyId, lat, lng }) => { +export const fetchElevation = async ({ surveyId, lat, lng }) => { try { - const { data } = await axios.get(`/api/survey/${surveyId}/geo/map/altitude`, { params: { lat, lng } }) + const { data } = await axios.get(`/api/survey/${surveyId}/geo/map/elevation`, { params: { lat, lng } }) return data } catch { return null diff --git a/webapp/views/App/views/Data/MapView/GeoAttributeDataLayer/CoordinateAttributePopUp.js b/webapp/views/App/views/Data/MapView/GeoAttributeDataLayer/CoordinateAttributePopUp.js index 2eda29cd9a..db75dced0b 100644 --- a/webapp/views/App/views/Data/MapView/GeoAttributeDataLayer/CoordinateAttributePopUp.js +++ b/webapp/views/App/views/Data/MapView/GeoAttributeDataLayer/CoordinateAttributePopUp.js @@ -22,7 +22,7 @@ import { useSurvey, useSurveyPreferredLang, useSurveyInfo } from '@webapp/store/ import { useUserName } from '@webapp/store/user/hooks' import { useI18n } from '@webapp/store/system' -import { useAltitude } from '../common/useAltitude' +import { useElevation } from '../common/useElevation' import { WhispMenuButton } from './WhispMenuButton' const getEarthMapUrl = (geojson) => `https://earthmap.org/?aoi=global&polygon=${JSON.stringify(geojson)}` @@ -52,7 +52,7 @@ const buildPath = ({ survey, attributeDef, ancestorsKeys, lang }) => { return pathParts.join(' -> ') } -const generateContent = ({ i18n, recordOwnerName, point, path, altitude }) => { +const generateContent = ({ i18n, recordOwnerName, point, path, elevation }) => { const coordinateNumericFieldPrecision = point.srs === DEFAULT_SRS.code ? 6 : NaN const xFormatted = NumberUtils.roundToPrecision(point.x, coordinateNumericFieldPrecision) const yFormatted = NumberUtils.roundToPrecision(point.y, coordinateNumericFieldPrecision) @@ -61,7 +61,7 @@ const generateContent = ({ i18n, recordOwnerName, point, path, altitude }) => { * **X**: ${xFormatted} * **Y**: ${yFormatted} * **SRS**: ${point.srs} -* **${i18n.t('mapView.altitude')}**: ${altitude} +* **${i18n.t('mapView.elevation')}**: ${elevation} * **${i18n.t('common.owner')}**: ${recordOwnerName ?? '...'}` return content } @@ -82,8 +82,8 @@ export const CoordinateAttributePopUp = (props) => { const [open, setOpen] = useState(false) const pointLatLong = PointFactory.createInstance({ x: longitude, y: latitude }) - // fetch altitude and record owner name only when popup is open - const altitude = useAltitude({ survey, point: pointLatLong, active: open }) + // fetch elevation and record owner name only when popup is open + const elevation = useElevation({ survey, point: pointLatLong, active: open }) const recordOwnerName = useUserName({ userUuid: recordOwnerUuid, active: open }) const onRemove = useCallback(() => { @@ -135,8 +135,8 @@ export const CoordinateAttributePopUp = (props) => { }, [generateGeoJsonWithName]) const content = useMemo( - () => generateContent({ i18n, recordOwnerName, point, path, altitude }), - [altitude, i18n, path, point, recordOwnerName] + () => generateContent({ i18n, recordOwnerName, point, path, elevation }), + [elevation, i18n, path, point, recordOwnerName] ) return ( diff --git a/webapp/views/App/views/Data/MapView/SamplingPointDataLayer/SamplingPointDataItemPopup.js b/webapp/views/App/views/Data/MapView/SamplingPointDataLayer/SamplingPointDataItemPopup.js index 195f3afe0d..b6f9c70c49 100644 --- a/webapp/views/App/views/Data/MapView/SamplingPointDataLayer/SamplingPointDataItemPopup.js +++ b/webapp/views/App/views/Data/MapView/SamplingPointDataLayer/SamplingPointDataItemPopup.js @@ -12,7 +12,7 @@ import { ButtonNext } from '@webapp/components/buttons/ButtonNext' import { ButtonPrevious } from '@webapp/components/buttons/ButtonPrevious' import { useAuthCanCreateRecord } from '@webapp/store/user' import { useSurvey } from '@webapp/store/survey' -import { useAltitude } from '../common/useAltitude' +import { useElevation } from '../common/useElevation' export const SamplingPointDataItemPopup = (props) => { const { createRecordFromSamplingPointDataItem, flyToNextPoint, flyToPreviousPoint, onRecordEditClick, pointFeature } = @@ -36,7 +36,7 @@ export const SamplingPointDataItemPopup = (props) => { const [open, setOpen] = useState(false) - const altitude = useAltitude({ survey, point, active: open }) + const elevation = useElevation({ survey, point, active: open }) const content = `**${i18n.t('mapView.samplingPointItemPopup.title')}** ${itemCodes @@ -46,7 +46,7 @@ ${itemCodes * **X**: ${point.x} * **Y**: ${point.y} * **SRS**: ${point.srs} -* **${i18n.t('mapView.altitude')}**: ${altitude}` +* **${i18n.t('mapView.elevation')}**: ${elevation}` const onClickNext = () => { flyToNextPoint(pointFeature) diff --git a/webapp/views/App/views/Data/MapView/common/useAltitude.js b/webapp/views/App/views/Data/MapView/common/useElevation.js similarity index 60% rename from webapp/views/App/views/Data/MapView/common/useAltitude.js rename to webapp/views/App/views/Data/MapView/common/useElevation.js index ada76be179..0370f795ae 100644 --- a/webapp/views/App/views/Data/MapView/common/useAltitude.js +++ b/webapp/views/App/views/Data/MapView/common/useElevation.js @@ -6,7 +6,7 @@ import * as Survey from '@core/survey/survey' import * as API from '@webapp/service/api' -export const useAltitude = ({ survey, point, active = true }) => { +export const useElevation = ({ survey, point, active = true }) => { const surveyId = Survey.getId(survey) const surveyInfo = Survey.getSurveyInfo(survey) const srsIndex = Survey.getSRSIndex(surveyInfo) @@ -14,17 +14,17 @@ export const useAltitude = ({ survey, point, active = true }) => { const pointLatLng = Points.toLatLong(point, srsIndex) const { y: lat, x: lng } = pointLatLng - const [altitude, setAltitude] = useState('...') + const [elevation, setElevation] = useState('...') useEffect(() => { - const fetchAltitude = async () => { - const _altitude = await API.fetchAltitude({ surveyId, lat, lng }) - setAltitude(_altitude === null ? 'error' : _altitude) + const fetchElevation = async () => { + const _elevation = await API.fetchElevation({ surveyId, lat, lng }) + setElevation(_elevation === null ? 'error' : _elevation) } if (active) { - fetchAltitude() + fetchElevation() } }, [active, lat, lng, surveyId]) - return altitude + return elevation } From dc66858a043c7ccb9d1c1c263a6f1ff03d68818b Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Fri, 27 Sep 2024 13:43:47 +0200 Subject: [PATCH 2/8] survey files: get max space from DB --- core/survey/_survey/surveyConfig.js | 3 +++ core/survey/survey.js | 2 ++ package.json | 2 +- server/modules/record/service/fileService.js | 7 +++++-- .../modules/survey/manager/surveyManager.js | 1 + .../survey/repository/surveyRepository.js | 9 +++++++++ yarn.lock | 20 +++++++++++++++++++ 7 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 core/survey/_survey/surveyConfig.js diff --git a/core/survey/_survey/surveyConfig.js b/core/survey/_survey/surveyConfig.js new file mode 100644 index 0000000000..87dd69dbc8 --- /dev/null +++ b/core/survey/_survey/surveyConfig.js @@ -0,0 +1,3 @@ +export const configKeys = { + filesTotalSpace: 'filesTotalSpace', // total space to store files (in MB) +} diff --git a/core/survey/survey.js b/core/survey/survey.js index 87a7033cfb..259200a998 100644 --- a/core/survey/survey.js +++ b/core/survey/survey.js @@ -8,6 +8,7 @@ import * as NodeDef from './nodeDef' import * as SurveySortKeys from './_survey/surveySortKeys' +import * as SurveyConfig from './_survey/surveyConfig' import * as SurveyInfo from './_survey/surveyInfo' import * as SurveyCycle from './surveyCycle' import * as SurveyNodeDefs from './_survey/surveyNodeDefs' @@ -60,6 +61,7 @@ export const newSurvey = ({ [SurveyInfo.keys.template]: template, }) +export const { configKeys } = SurveyConfig export const { keys: infoKeys, status } = SurveyInfo export const { dependencyTypes } = SurveyDependencies export const { collectReportKeys, cycleOneKey, samplingPointDataCategoryName } = SurveyInfo diff --git a/package.json b/package.json index a3721b7386..5ce33efc63 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@mui/x-date-pickers": "^7.14.0", "@mui/x-tree-view": "^7.7.1", "@openforis/arena-core": "^0.0.204", - "@openforis/arena-server": "^0.1.34", + "@openforis/arena-server": "../arena-server", "@reduxjs/toolkit": "^2.2.5", "@sendgrid/mail": "^8.1.3", "@shopify/draggable": "^1.1.3", diff --git a/server/modules/record/service/fileService.js b/server/modules/record/service/fileService.js index 142586a837..cf57e9199e 100644 --- a/server/modules/record/service/fileService.js +++ b/server/modules/record/service/fileService.js @@ -6,6 +6,8 @@ import * as FileManager from '../manager/fileManager' const logger = Log.getLogger('FileService') +const defaultSurveyFilesTotalSpaceMB = 10 * 1024 // in MB (=10 GB) + export const checkFilesStorage = async () => { const storageType = FileManager.getFileContentStorageType() @@ -40,9 +42,10 @@ export const checkFilesStorage = async () => { } } -// eslint-disable-next-line no-unused-vars const getSurveyFilesTotalSpace = async ({ surveyId }) => { - return 10 * Math.pow(1024, 3) // TODO make it configurable, fixed to 10 GB per survey now + const surveyTotalSpaceMB = await SurveyManager.fetchFilesTotalSpace(surveyId) + const totalSpaceMB = surveyTotalSpaceMB ?? defaultSurveyFilesTotalSpaceMB + return totalSpaceMB * 1024 * 1024 // from MB to bytes } export const fetchFilesStatistics = async ({ surveyId }) => { diff --git a/server/modules/survey/manager/surveyManager.js b/server/modules/survey/manager/surveyManager.js index 216b3c2218..b9101bf284 100644 --- a/server/modules/survey/manager/surveyManager.js +++ b/server/modules/survey/manager/surveyManager.js @@ -196,6 +196,7 @@ export const { fetchSurveysByName, fetchSurveyIdsAndNames, fetchDependencies, + fetchFilesTotalSpace, } = SurveyRepository export const fetchSurveyById = async ({ surveyId, draft = false, validate = false, backup = false }, client = db) => { diff --git a/server/modules/survey/repository/surveyRepository.js b/server/modules/survey/repository/surveyRepository.js index 184e9fe9d7..41d6abecf5 100644 --- a/server/modules/survey/repository/surveyRepository.js +++ b/server/modules/survey/repository/surveyRepository.js @@ -265,6 +265,15 @@ export const fetchDependencies = async (surveyId, client = db) => R.prop('dependencies') ) +export const fetchFilesTotalSpace = async (surveyId, client = db) => + client.oneOrNone( + `SELECT config#>'{${Survey.configKeys.filesTotalSpace}}' AS value + FROM survey + WHERE id = $1`, + [surveyId], + R.prop('value') + ) + export const fetchTemporarySurveyIds = async ({ olderThan24Hours = false } = {}, client = db) => client.map( `SELECT id diff --git a/yarn.lock b/yarn.lock index 03a44bdec1..8c947c18eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2671,6 +2671,26 @@ proj4 "^2.11.0" uuid "^10.0.0" +"@openforis/arena-server@../arena-server": + version "0.1.34" + dependencies: + "@godaddy/terminus" "^4.12.1" + "@openforis/arena-core" "^0.0.188" + bcryptjs "^2.4.3" + compression "^1.7.4" + connect-pg-simple "^9.0.0" + db-migrate "^0.11.14" + db-migrate-pg "^1.5.2" + express "^4.18.2" + express-fileupload "^1.4.0" + express-session "^1.17.2" + lodash.throttle "^4.1.1" + log4js "^6.9.1" + passport "^0.6.0" + passport-local "^1.0.0" + pg-promise "^11.5.4" + socket.io "^4.7.2" + "@openforis/arena-server@^0.1.34": version "0.1.34" resolved "https://npm.pkg.github.com/download/@openforis/arena-server/0.1.34/2438e3584eca5cd8ed5a69d44e94a92384433e6c#2438e3584eca5cd8ed5a69d44e94a92384433e6c" From bac17fb5d3ef24b241565905c4bdb4b2ff3a3d82 Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Fri, 27 Sep 2024 14:53:26 +0200 Subject: [PATCH 3/8] prepare editing --- core/i18n/resources/en/common.js | 4 ++ core/numberConversionUtils.js | 69 +++++++++++++++++++ core/numberUtils.js | 39 ----------- webapp/components/ItemEditButtonBar.js | 34 +++++++++ webapp/components/geo/GeoPolygonInfo.js | 12 +--- .../ExtraPropDefsEditor/ExtraPropDefEditor.js | 29 +++----- .../useExtraPropDefEditor.js | 25 ++++--- .../SurveyInfo/SurveyConfigurationEditor.js | 33 +++++++++ .../App/views/Home/SurveyInfo/SurveyInfo.js | 16 +++-- 9 files changed, 180 insertions(+), 81 deletions(-) create mode 100644 core/numberConversionUtils.js create mode 100644 webapp/components/ItemEditButtonBar.js create mode 100644 webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js diff --git a/core/i18n/resources/en/common.js b/core/i18n/resources/en/common.js index 0404a3d223..2bd722a397 100644 --- a/core/i18n/resources/en/common.js +++ b/core/i18n/resources/en/common.js @@ -437,6 +437,10 @@ Thank you and enjoy **$t(common.appNameFull)**!`, }, surveyDeleted: 'Survey {{surveyName}} has been deleted', surveyInfo: { + configuration: { + title: 'Configuration', + filesTotalSpace: 'Files total space (GB)', + }, confirmDeleteCycleHeader: 'Delete this cycle?', confirmDeleteCycle: `Are you sure you want to delete the cycle {{cycle}}?\n\n$t(common.cantUndoWarning)\n\n If there are records associated to this cycle, they will be deleted.`, diff --git a/core/numberConversionUtils.js b/core/numberConversionUtils.js new file mode 100644 index 0000000000..adf919ec77 --- /dev/null +++ b/core/numberConversionUtils.js @@ -0,0 +1,69 @@ +import { Objects } from '@openforis/arena-core' + +const areaUnits = { + squareMeter: 'squareMeter', + squareFoot: 'squareFoot', + acre: 'acre', + hectare: 'hectare', +} + +const areaUnitToSquareMetersConversionFactor = { + [areaUnits.acre]: 4046.85642199999983859016, + [areaUnits.hectare]: 10000, + [areaUnits.squareMeter]: 1, + [areaUnits.squareFoot]: 0.09290304, +} + +const lengthUnits = { + meter: 'meter', + foot: 'foot', +} + +const lengthUnitToMetersConversionFactor = { + [lengthUnits.meter]: 1, + [lengthUnits.foot]: 0.3048, +} + +const abbreviationByUnit = { + [areaUnits.squareMeter]: 'm²', + [areaUnits.squareFoot]: 'ft²', + [areaUnits.acre]: 'ac', + [areaUnits.hectare]: 'ha', + [lengthUnits.meter]: 'm', + [lengthUnits.foot]: 'ft', +} + +const dataStorageUnits = { + byte: 'byte', + MB: 'MB', + GB: 'GB', +} + +const dataStorageUnitToBytesConversionFactor = { + [dataStorageUnits.byte]: 1, + [dataStorageUnits.MB]: Math.pow(1024, 2), + [dataStorageUnits.GB]: Math.pow(1024, 3), +} + +const squareMetersToUnit = (unit) => (value) => + Objects.isNil(value) ? NaN : value / areaUnitToSquareMetersConversionFactor[unit] + +const metersToUnit = (unit) => (value) => + Objects.isNil(value) ? NaN : value / lengthUnitToMetersConversionFactor[unit] + +const dataStorageBytesToUnit = (unit) => (bytes) => + Objects.isNil(bytes) ? NaN : bytes / dataStorageUnitToBytesConversionFactor[unit] + +const dataStorageValueToBytes = (unit) => (value) => + Objects.isNil(value) ? NaN : value * dataStorageUnitToBytesConversionFactor[unit] + +export const NumberConversionUtils = { + areaUnits, + lengthUnits, + abbreviationByUnit, + metersToUnit, + squareMetersToUnit, + dataStorageUnits, + dataStorageBytesToUnit, + dataStorageValueToBytes, +} diff --git a/core/numberUtils.js b/core/numberUtils.js index 2e467d3b64..dc6918a51c 100644 --- a/core/numberUtils.js +++ b/core/numberUtils.js @@ -12,39 +12,6 @@ BigNumber.config({ }, }) -export const areaUnits = { - squareMeter: 'squareMeter', - squareFoot: 'squareFoot', - acre: 'acre', - hectare: 'hectare', -} - -const areaUnitToSquareMetersConversionFactor = { - [areaUnits.acre]: 4046.85642199999983859016, - [areaUnits.hectare]: 10000, - [areaUnits.squareMeter]: 1, - [areaUnits.squareFoot]: 0.09290304, -} - -export const lengthUnits = { - meter: 'meter', - foot: 'foot', -} - -const lengthUnitToMetersConversionFactor = { - [lengthUnits.meter]: 1, - [lengthUnits.foot]: 0.3048, -} - -export const abbreviationByUnit = { - [areaUnits.squareMeter]: 'm²', - [areaUnits.squareFoot]: 'ft²', - [areaUnits.acre]: 'ac', - [areaUnits.hectare]: 'ha', - [lengthUnits.meter]: 'm', - [lengthUnits.foot]: 'ft', -} - export const toNumber = (num) => (Objects.isEmpty(num) ? NaN : Number(num)) export const isInteger = A.pipe(toNumber, Number.isInteger) @@ -91,9 +58,3 @@ export const formatInteger = (value) => formatDecimal(value, 0) * @returns {number} - The result of the modulus (always positive or 0). */ export const mod = (modulus) => (value) => ((value % modulus) + modulus) % modulus - -export const squareMetersToUnit = (unit) => (value) => - Objects.isNil(value) ? NaN : value / areaUnitToSquareMetersConversionFactor[unit] - -export const metersToUnit = (unit) => (value) => - Objects.isNil(value) ? NaN : value / lengthUnitToMetersConversionFactor[unit] diff --git a/webapp/components/ItemEditButtonBar.js b/webapp/components/ItemEditButtonBar.js new file mode 100644 index 0000000000..0c0e972b03 --- /dev/null +++ b/webapp/components/ItemEditButtonBar.js @@ -0,0 +1,34 @@ +import React from 'react' + +import * as Validation from '@core/validation/validation' +import { ButtonCancel, ButtonDelete, ButtonIconEdit, ButtonSave } from './buttons' + +export const ItemEditButtonBar = (props) => { + const { + dirty = false, + editing = false, + onCancel, + onDelete = null, + onEdit, + onSave, + readOnly = false, + validation = null, + } = props + + return ( +
+ {!editing && !readOnly && ( + <> + + {onDelete && } + + )} + {editing && ( + <> + + + + )} +
+ ) +} diff --git a/webapp/components/geo/GeoPolygonInfo.js b/webapp/components/geo/GeoPolygonInfo.js index c83516ac2b..02ad3a93e4 100644 --- a/webapp/components/geo/GeoPolygonInfo.js +++ b/webapp/components/geo/GeoPolygonInfo.js @@ -4,6 +4,7 @@ import React, { useCallback, useMemo } from 'react' import PropTypes from 'prop-types' import * as NumberUtils from '@core/numberUtils' +import { NumberConversionUtils } from '@core/numberConversionUtils' import { useI18n } from '@webapp/store/system' import { GeoJsonUtils } from '@webapp/utils/geoJsonUtils' @@ -11,15 +12,8 @@ import { GeoJsonUtils } from '@webapp/utils/geoJsonUtils' import { FormItem } from '../form/Input' import { ButtonIconInfo } from '../buttons' -const { - abbreviationByUnit, - areaUnits, - formatDecimal, - lengthUnits, - metersToUnit, - roundToPrecision, - squareMetersToUnit, -} = NumberUtils +const { formatDecimal, roundToPrecision } = NumberUtils +const { areaUnits, lengthUnits, abbreviationByUnit, metersToUnit, squareMetersToUnit } = NumberConversionUtils const formatNumber = (value) => formatDecimal(roundToPrecision(value, 2)) diff --git a/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js b/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js index b2efcc425a..4be8537f92 100644 --- a/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js +++ b/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js @@ -8,9 +8,9 @@ import * as Validation from '@core/validation/validation' import { FormItem, Input } from '@webapp/components/form/Input' import { Dropdown } from '@webapp/components/form' -import { ButtonCancel, ButtonDelete, ButtonIconEdit, ButtonSave } from '@webapp/components' import { useExtraPropDefEditor } from './useExtraPropDefEditor' +import { ItemEditButtonBar } from '@webapp/components/ItemEditButtonBar' export const ExtraPropDefEditor = (props) => { const { index, readOnly = false, onItemDelete } = props @@ -53,23 +53,16 @@ export const ExtraPropDefEditor = (props) => { } validation={Validation.getFieldValidation(ExtraPropDef.keys.dataType)(validation)} /> - {!editing && !readOnly && ( - <> - - onItemDelete({ index })} /> - - )} - {editing && ( - <> - - - - )} + onItemDelete({ index })} + onEdit={onEditClick} + onSave={onSaveClick} + readOnly={readOnly} + validation={validation} + /> ) } diff --git a/webapp/components/survey/ExtraPropDefsEditor/useExtraPropDefEditor.js b/webapp/components/survey/ExtraPropDefsEditor/useExtraPropDefEditor.js index f19ce8c5aa..392b18dde5 100644 --- a/webapp/components/survey/ExtraPropDefsEditor/useExtraPropDefEditor.js +++ b/webapp/components/survey/ExtraPropDefsEditor/useExtraPropDefEditor.js @@ -28,16 +28,19 @@ export const useExtraPropDefEditor = (props) => { setState(initialState) }, [initialState]) - const updateExtraPropDef = useCallback(async ({ extraPropDefUpdated }) => { - const validation = await validateExtraPropDef({ - extraPropDef: extraPropDefUpdated, - extraPropDefsArray: extraPropDefs, - }) - setState((statePrev) => ({ - ...statePrev, - extraPropDef: Validation.assocValidation(validation)(extraPropDefUpdated), - })) - }, []) + const updateExtraPropDef = useCallback( + async ({ extraPropDefUpdated }) => { + const validation = await validateExtraPropDef({ + extraPropDef: extraPropDefUpdated, + extraPropDefsArray: extraPropDefs, + }) + setState((statePrev) => ({ + ...statePrev, + extraPropDef: Validation.assocValidation(validation)(extraPropDefUpdated), + })) + }, + [extraPropDefs] + ) const onEditClick = () => setState((statePrev) => ({ ...statePrev, editing: true })) @@ -80,7 +83,7 @@ export const useExtraPropDefEditor = (props) => { await doSave() } } - }, [extraPropDef, extraPropDefProp, index, onItemUpdate]) + }, [confirm, extraPropDef, extraPropDefProp, i18n, index, onItemUpdate]) const onCancelClick = () => { setState((statePrev) => ({ ...statePrev, extraPropDef: extraPropDefProp, editing: false })) diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js new file mode 100644 index 0000000000..9a8f30fe45 --- /dev/null +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js @@ -0,0 +1,33 @@ +import React from 'react' + +import { NumberConversionUtils } from '@core/numberConversionUtils' +import * as Survey from '@core/survey/survey' + +import { FormItem, Input, NumberFormats } from '@webapp/components/form/Input' +import { useI18n } from '@webapp/store/system' +import { useSurveyInfo } from '@webapp/store/survey' + +const { dataStorageUnits, dataStorageValueToBytes, dataStorageBytesToUnit } = NumberConversionUtils + +const defaultTotalSpaceGB = 10 +const defaultTotalSpace = dataStorageValueToBytes(dataStorageUnits.GB)(defaultTotalSpaceGB) + +export const SurveyConfigurationEditor = () => { + const i18n = useI18n() + const surveyInfo = useSurveyInfo() + const filesStatistics = Survey.getFilesStatistics(surveyInfo) + + const { usedSpace, totalSpace } = filesStatistics + const minTotalSpace = Math.max(usedSpace, defaultTotalSpace) + const totalSpaceGB = dataStorageBytesToUnit(dataStorageUnits.GB)(totalSpace) + + return ( +
+ + val} value={totalSpaceGB} /> + + + +
+ ) +} diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js b/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js index 1acb2082b3..b9c589dbf9 100644 --- a/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js @@ -6,26 +6,28 @@ import * as Survey from '@core/survey/survey' import { useI18n } from '@webapp/store/system' import { useSurveyInfo } from '@webapp/store/survey' -import { useAuthCanEditSurvey } from '@webapp/store/user' +import { useAuthCanEditSurvey, useUserIsSystemAdmin } from '@webapp/store/user' import { TestId } from '@webapp/utils/testId' +import { ButtonSave, ExpansionPanel } from '@webapp/components' import { Checkbox } from '@webapp/components/form' import { FormItem, Input } from '@webapp/components/form/Input' +import CycleSelector from '@webapp/components/survey/CycleSelector' import LabelsEditor from '@webapp/components/survey/LabelsEditor' -import { ButtonSave } from '@webapp/components' import CyclesEditor from './CyclesEditor' import SrsEditor from './SrsEditor' import LanguagesEditor from './LanguagesEditor' +import SamplingPolygonEditor from './SamplingPolygonEditor' +import { SurveyConfigurationEditor } from './SurveyConfigurationEditor' import { useSurveyInfoForm } from './store' -import SamplingPolygonEditor from './SamplingPolygonEditor' -import CycleSelector from '@webapp/components/survey/CycleSelector' const SurveyInfo = () => { const surveyInfo = useSurveyInfo() const readOnly = !useAuthCanEditSurvey() const i18n = useI18n() + const isSystemAdmin = useUserIsSystemAdmin() const { cycleKeys, @@ -146,6 +148,12 @@ const SurveyInfo = () => { + {isSystemAdmin && ( + + + + )} + {!readOnly && } From 2bfad5b5f152ddb2c4d15f57c58ecfc429963377 Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Fri, 27 Sep 2024 15:02:34 +0200 Subject: [PATCH 4/8] added item edit button bar --- webapp/components/ItemEditButtonBar.js | 6 ++++-- webapp/components/ItemEditButtonBar.scss | 4 ++++ .../survey/ExtraPropDefsEditor/ExtraPropDefEditor.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 webapp/components/ItemEditButtonBar.scss diff --git a/webapp/components/ItemEditButtonBar.js b/webapp/components/ItemEditButtonBar.js index 0c0e972b03..759e3a614c 100644 --- a/webapp/components/ItemEditButtonBar.js +++ b/webapp/components/ItemEditButtonBar.js @@ -1,3 +1,5 @@ +import './ItemEditButtonBar.scss' + import React from 'react' import * as Validation from '@core/validation/validation' @@ -16,7 +18,7 @@ export const ItemEditButtonBar = (props) => { } = props return ( -
+
{!editing && !readOnly && ( <> @@ -25,7 +27,7 @@ export const ItemEditButtonBar = (props) => { )} {editing && ( <> - + )} diff --git a/webapp/components/ItemEditButtonBar.scss b/webapp/components/ItemEditButtonBar.scss new file mode 100644 index 0000000000..90500da50d --- /dev/null +++ b/webapp/components/ItemEditButtonBar.scss @@ -0,0 +1,4 @@ +.item-edit-button-bar { + display: flex; + gap: 0.6rem; +} diff --git a/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js b/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js index 4be8537f92..99806445cd 100644 --- a/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js +++ b/webapp/components/survey/ExtraPropDefsEditor/ExtraPropDefEditor.js @@ -8,9 +8,9 @@ import * as Validation from '@core/validation/validation' import { FormItem, Input } from '@webapp/components/form/Input' import { Dropdown } from '@webapp/components/form' +import { ItemEditButtonBar } from '@webapp/components/ItemEditButtonBar' import { useExtraPropDefEditor } from './useExtraPropDefEditor' -import { ItemEditButtonBar } from '@webapp/components/ItemEditButtonBar' export const ExtraPropDefEditor = (props) => { const { index, readOnly = false, onItemDelete } = props From 67e816e31c50df4de51c65e3d66d2edf8535b526 Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Mon, 30 Sep 2024 14:46:58 +0200 Subject: [PATCH 5/8] max files size editor --- core/auth/authorizer.js | 1 + core/numberConversionUtils.js | 14 +++-- core/numberUtils.js | 9 +++ server/modules/auth/authApiMiddleware.js | 1 + server/modules/record/manager/fileManager.js | 12 ++++ server/modules/record/service/fileService.js | 17 ++---- server/modules/survey/api/surveyApi.js | 15 ++++- .../modules/survey/manager/surveyManager.js | 19 ++++++- .../survey/repository/surveyRepository.js | 11 ++++ .../modules/survey/service/surveyService.js | 1 + webapp/service/api/index.js | 1 + webapp/service/api/survey/index.js | 7 ++- .../SurveyInfo/SurveyConfigurationEditor.js | 33 ----------- .../ConfigurationNumericItemEditor.js | 56 +++++++++++++++++++ .../SurveyConfigurationEditor.js | 54 ++++++++++++++++++ .../SurveyConfigurationEditor/index.js | 1 + .../App/views/Home/SurveyInfo/SurveyInfo.js | 13 +++-- .../App/views/Home/SurveyInfo/SurveyInfo.scss | 11 +++- 18 files changed, 217 insertions(+), 59 deletions(-) delete mode 100644 webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js create mode 100644 webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/ConfigurationNumericItemEditor.js create mode 100644 webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/SurveyConfigurationEditor.js create mode 100644 webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/index.js diff --git a/core/auth/authorizer.js b/core/auth/authorizer.js index d54e886823..291d0dd8f7 100644 --- a/core/auth/authorizer.js +++ b/core/auth/authorizer.js @@ -53,6 +53,7 @@ export const canViewTemplates = (user) => User.isSystemAdmin(user) // UPDATE export const canEditSurvey = _hasSurveyPermission(permissions.surveyEdit) +export const canEditSurveyConfig = (user) => User.isSystemAdmin(user) export const canEditTemplates = (user) => User.isSystemAdmin(user) export const canRefreshAllSurveyRdbs = (user) => User.isSystemAdmin(user) diff --git a/core/numberConversionUtils.js b/core/numberConversionUtils.js index adf919ec77..c279c41f46 100644 --- a/core/numberConversionUtils.js +++ b/core/numberConversionUtils.js @@ -46,16 +46,21 @@ const dataStorageUnitToBytesConversionFactor = { } const squareMetersToUnit = (unit) => (value) => - Objects.isNil(value) ? NaN : value / areaUnitToSquareMetersConversionFactor[unit] + Objects.isNil(value) ? NaN : Number(value) / areaUnitToSquareMetersConversionFactor[unit] const metersToUnit = (unit) => (value) => - Objects.isNil(value) ? NaN : value / lengthUnitToMetersConversionFactor[unit] + Objects.isNil(value) ? NaN : Number(value) / lengthUnitToMetersConversionFactor[unit] const dataStorageBytesToUnit = (unit) => (bytes) => - Objects.isNil(bytes) ? NaN : bytes / dataStorageUnitToBytesConversionFactor[unit] + Objects.isNil(bytes) ? NaN : Number(bytes) / dataStorageUnitToBytesConversionFactor[unit] const dataStorageValueToBytes = (unit) => (value) => - Objects.isNil(value) ? NaN : value * dataStorageUnitToBytesConversionFactor[unit] + Objects.isNil(value) ? NaN : Number(value) * dataStorageUnitToBytesConversionFactor[unit] + +const dataStorageValueToUnit = (unitFrom, unitTo) => (value) => { + const bytes = dataStorageValueToBytes(unitFrom)(value) + return dataStorageBytesToUnit(unitTo)(bytes) +} export const NumberConversionUtils = { areaUnits, @@ -66,4 +71,5 @@ export const NumberConversionUtils = { dataStorageUnits, dataStorageBytesToUnit, dataStorageValueToBytes, + dataStorageValueToUnit, } diff --git a/core/numberUtils.js b/core/numberUtils.js index dc6918a51c..b183031fbf 100644 --- a/core/numberUtils.js +++ b/core/numberUtils.js @@ -58,3 +58,12 @@ export const formatInteger = (value) => formatDecimal(value, 0) * @returns {number} - The result of the modulus (always positive or 0). */ export const mod = (modulus) => (value) => ((value % modulus) + modulus) % modulus + +export const limit = + ({ minValue = null, maxValue = null }) => + (value) => { + let result = Number(value) + if (minValue) result = Math.max(minValue, result) + if (maxValue) result = Math.min(maxValue, result) + return result + } diff --git a/server/modules/auth/authApiMiddleware.js b/server/modules/auth/authApiMiddleware.js index 987c63cce8..0c5c1d7915 100644 --- a/server/modules/auth/authApiMiddleware.js +++ b/server/modules/auth/authApiMiddleware.js @@ -101,6 +101,7 @@ export const requireSurveyCreatePermission = async (req, _res, next) => { } export const requireSurveyViewPermission = requireSurveyPermission(Authorizer.canViewSurvey) export const requireSurveyEditPermission = requireSurveyPermission(Authorizer.canEditSurvey) +export const requireSurveyConfigEditPermission = requireSurveyPermission(Authorizer.canEditSurveyConfig) export const requireRecordCleansePermission = requireSurveyPermission(Authorizer.canCleanseRecords) export const requireSurveyRdbRefreshPermission = requirePermission(Authorizer.canRefreshAllSurveyRdbs) export const requireCanExportSurveysList = requirePermission(Authorizer.canExportSurveysList) diff --git a/server/modules/record/manager/fileManager.js b/server/modules/record/manager/fileManager.js index 5c6ee10103..1360eefa91 100644 --- a/server/modules/record/manager/fileManager.js +++ b/server/modules/record/manager/fileManager.js @@ -5,12 +5,18 @@ import * as RecordFile from '@core/record/recordFile' import { db } from '@server/db/db' import * as Log from '@server/log/log' +import * as SurveyRepository from '@server/modules/survey/repository/surveyRepository' + import * as FileRepository from '../repository/fileRepository' import * as FileRepositoryFileSystem from '../repository/fileRepositoryFileSystem' import * as FileRepositoryS3Bucket from '../repository/fileRepositoryS3Bucket' +import { NumberConversionUtils } from '@core/numberConversionUtils' const logger = Log.getLogger('FileManager') +export const defaultSurveyFilesTotalSpaceMB = 10 * 1024 // in MB (=10 GB) +export const maxSurveyFilesTotalSpaceMB = 100 * 1024 // in MB (=100 GB) + export const fileContentStorageTypes = { db: 'db', fileSystem: 'fileSystem', @@ -74,6 +80,12 @@ export const fetchFileContentAsBuffer = async ({ surveyId, fileUuid }, client = return streamToBuffer(contentStream) } +export const fetchSurveyFilesTotalSpace = async ({ surveyId }) => { + const surveyTotalSpaceMB = await SurveyRepository.fetchFilesTotalSpace(surveyId) + const totalSpaceMB = surveyTotalSpaceMB ?? defaultSurveyFilesTotalSpaceMB + return NumberConversionUtils.dataStorageValueToBytes(NumberConversionUtils.dataStorageUnits.MB)(totalSpaceMB) +} + export const insertFile = async (surveyId, file, client = db) => { const storageType = getFileContentStorageType() const contentStoreFunction = contentStoreFunctionByStorageType[storageType] diff --git a/server/modules/record/service/fileService.js b/server/modules/record/service/fileService.js index cf57e9199e..95e29c3ae4 100644 --- a/server/modules/record/service/fileService.js +++ b/server/modules/record/service/fileService.js @@ -1,13 +1,12 @@ import { Promises } from '@openforis/arena-core' import * as Log from '@server/log/log' -import * as SurveyManager from '@server/modules/survey/manager/surveyManager' +import * as SurveyRepository from '@server/modules/survey/repository/surveyRepository' + import * as FileManager from '../manager/fileManager' const logger = Log.getLogger('FileService') -const defaultSurveyFilesTotalSpaceMB = 10 * 1024 // in MB (=10 GB) - export const checkFilesStorage = async () => { const storageType = FileManager.getFileContentStorageType() @@ -21,7 +20,7 @@ export const checkFilesStorage = async () => { return } logger.debug(`Moving survey files to new storage (if necessary)`) - const surveyIds = await SurveyManager.fetchAllSurveyIds() + const surveyIds = await SurveyRepository.fetchAllSurveyIds() let allSurveysFilesMoved = false let errorsFound = false await Promises.each(surveyIds, async (surveyId) => { @@ -42,14 +41,8 @@ export const checkFilesStorage = async () => { } } -const getSurveyFilesTotalSpace = async ({ surveyId }) => { - const surveyTotalSpaceMB = await SurveyManager.fetchFilesTotalSpace(surveyId) - const totalSpaceMB = surveyTotalSpaceMB ?? defaultSurveyFilesTotalSpaceMB - return totalSpaceMB * 1024 * 1024 // from MB to bytes -} - export const fetchFilesStatistics = async ({ surveyId }) => { - const totalSpace = await getSurveyFilesTotalSpace({ surveyId }) + const totalSpace = await FileManager.fetchSurveyFilesTotalSpace({ surveyId }) const { total: usedSpace } = await FileManager.fetchCountAndTotalFilesSize({ surveyId }) const availableSpace = Math.max(0, totalSpace - usedSpace) @@ -61,7 +54,7 @@ export const fetchFilesStatistics = async ({ surveyId }) => { } export const cleanupAllSurveysFilesProps = async () => { - const surveyIds = await SurveyManager.fetchAllSurveyIds() + const surveyIds = await SurveyRepository.fetchAllSurveyIds() let count = 0 for await (const surveyId of surveyIds) { const cleanedFiles = await FileManager.cleanupSurveyFilesProps({ surveyId }) diff --git a/server/modules/survey/api/surveyApi.js b/server/modules/survey/api/surveyApi.js index f0ef349338..747f827cb9 100644 --- a/server/modules/survey/api/surveyApi.js +++ b/server/modules/survey/api/surveyApi.js @@ -146,7 +146,8 @@ export const init = (app) => { let surveyUpdated = survey if (Authorizer.canEditSurvey(user, Survey.getSurveyInfo(survey))) { const surveyId = Survey.getId(survey) - surveyUpdated = Survey.assocFilesStatistics(await FileService.fetchFilesStatistics({ surveyId }))(survey) + const filesStatistics = await FileService.fetchFilesStatistics({ surveyId }) + surveyUpdated = Survey.assocFilesStatistics(filesStatistics)(survey) } res.json({ survey: surveyUpdated }) } @@ -295,6 +296,18 @@ export const init = (app) => { } }) + app.put('/survey/:surveyId/config', AuthMiddleware.requireSurveyConfigEditPermission, async (req, res, next) => { + try { + const user = Request.getUser(req) + const { key, value } = Request.getBody(req) + const { surveyId } = Request.getParams(req) + const survey = await SurveyService.updateSurveyConfigurationProp({ surveyId, key, value }) + await _sendSurvey({ survey, user, res }) + } catch (error) { + next(error) + } + }) + // ==== DELETE app.delete('/survey/:surveyId', AuthMiddleware.requireSurveyEditPermission, async (req, res, next) => { diff --git a/server/modules/survey/manager/surveyManager.js b/server/modules/survey/manager/surveyManager.js index b9101bf284..838e554fc3 100644 --- a/server/modules/survey/manager/surveyManager.js +++ b/server/modules/survey/manager/surveyManager.js @@ -10,9 +10,10 @@ import * as SurveyValidator from '@core/survey/surveyValidator' import * as NodeDef from '@core/survey/nodeDef' import * as NodeDefLayout from '@core/survey/nodeDefLayout' import * as User from '@core/user/user' +import * as NumberUtils from '@core/numberUtils' import * as ObjectUtils from '@core/objectUtils' -import * as Validation from '@core/validation/validation' import * as PromiseUtils from '@core/promiseUtils' +import * as Validation from '@core/validation/validation' import SystemError from '@core/systemError' import { db } from '@server/db/db' @@ -418,6 +419,22 @@ export const publishSurveyProps = async (surveyId, langsDeleted, client = db) => export const unpublishSurveyProps = async (surveyId, client = db) => SurveyRepository.unpublishSurveyProps(surveyId, client) +export const updateSurveyConfigurationProp = async ({ surveyId, key, value }, client = db) => { + if (key !== Survey.configKeys.filesTotalSpace) { + throw new Error(`Configuration key update not supported: ${key}`) + } + const valueLimited = NumberUtils.limit({ + minValue: FileManager.defaultSurveyFilesTotalSpaceMB, + maxValue: FileManager.maxSurveyFilesTotalSpaceMB, + })(value) + if (valueLimited === FileManager.defaultSurveyFilesTotalSpaceMB) { + await SurveyRepository.clearSurveyConfiguration({ surveyId }, client) + } else { + await SurveyRepository.updateSurveyConfigurationProp({ surveyId, key, value: String(valueLimited) }, client) + } + return fetchSurveyById({ surveyId, draft: true, validate: true }, client) +} + export const { removeSurveyTemporaryFlag, updateSurveyDependencyGraphs } = SurveyRepository // ====== DELETE diff --git a/server/modules/survey/repository/surveyRepository.js b/server/modules/survey/repository/surveyRepository.js index 41d6abecf5..186cff4f0e 100644 --- a/server/modules/survey/repository/surveyRepository.js +++ b/server/modules/survey/repository/surveyRepository.js @@ -363,6 +363,17 @@ export const removeSurveyTemporaryFlag = async ({ surveyId }, client = db) => [surveyId] ) +export const updateSurveyConfigurationProp = async ({ surveyId, key, value }, client = db) => + client.none( + `UPDATE survey + SET config = jsonb_set(coalesce(config, '{}'), '{${key}}', $/value/) + WHERE id = $/surveyId/`, + { surveyId, value } + ) + +export const clearSurveyConfiguration = async ({ surveyId }, client = db) => + client.none(`UPDATE survey SET config = null WHERE id = $1`, [surveyId]) + // ============== DELETE export const deleteSurvey = async (id, client = db) => client.one('DELETE FROM survey WHERE id = $1 RETURNING id', [id]) diff --git a/server/modules/survey/service/surveyService.js b/server/modules/survey/service/surveyService.js index 0825269ea8..db9938196f 100644 --- a/server/modules/survey/service/surveyService.js +++ b/server/modules/survey/service/surveyService.js @@ -137,6 +137,7 @@ export const { // UPDATE updateSurveyDependencyGraphs, updateSurveyProps, + updateSurveyConfigurationProp, // DELETE deleteTemporarySurveys, // UTILS diff --git a/webapp/service/api/index.js b/webapp/service/api/index.js index 2f43707b9c..5a0a3cfb30 100644 --- a/webapp/service/api/index.js +++ b/webapp/service/api/index.js @@ -62,6 +62,7 @@ export { fetchSurveyTemplatesPublished, insertSurvey, startImportLabelsJob, + updateSurveyConfigurationProp, } from './survey' export { convertNodeDef, diff --git a/webapp/service/api/survey/index.js b/webapp/service/api/survey/index.js index a81431dc2f..07e12907ea 100644 --- a/webapp/service/api/survey/index.js +++ b/webapp/service/api/survey/index.js @@ -42,7 +42,12 @@ export const fetchSurveyTemplatesPublished = async () => { // ==== UPDATE export const startImportLabelsJob = async ({ surveyId, file }) => { const formData = objectToFormData({ file }) - const { data: job } = await axios.put(`/api/survey/${surveyId}/labels`, formData) return job } + +export const updateSurveyConfigurationProp = async ({ surveyId, key, value }) => { + const formData = objectToFormData({ key, value }) + const { data } = await axios.put(`/api/survey/${surveyId}/config`, formData) + return data +} diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js deleted file mode 100644 index 9a8f30fe45..0000000000 --- a/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' - -import { NumberConversionUtils } from '@core/numberConversionUtils' -import * as Survey from '@core/survey/survey' - -import { FormItem, Input, NumberFormats } from '@webapp/components/form/Input' -import { useI18n } from '@webapp/store/system' -import { useSurveyInfo } from '@webapp/store/survey' - -const { dataStorageUnits, dataStorageValueToBytes, dataStorageBytesToUnit } = NumberConversionUtils - -const defaultTotalSpaceGB = 10 -const defaultTotalSpace = dataStorageValueToBytes(dataStorageUnits.GB)(defaultTotalSpaceGB) - -export const SurveyConfigurationEditor = () => { - const i18n = useI18n() - const surveyInfo = useSurveyInfo() - const filesStatistics = Survey.getFilesStatistics(surveyInfo) - - const { usedSpace, totalSpace } = filesStatistics - const minTotalSpace = Math.max(usedSpace, defaultTotalSpace) - const totalSpaceGB = dataStorageBytesToUnit(dataStorageUnits.GB)(totalSpace) - - return ( -
- - val} value={totalSpaceGB} /> - - - -
- ) -} diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/ConfigurationNumericItemEditor.js b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/ConfigurationNumericItemEditor.js new file mode 100644 index 0000000000..b69fe5315b --- /dev/null +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/ConfigurationNumericItemEditor.js @@ -0,0 +1,56 @@ +import React, { useCallback, useState } from 'react' +import PropTypes from 'prop-types' + +import * as NumberUtils from '@core/numberUtils' +import { FormItem, Input, NumberFormats } from '@webapp/components/form/Input' +import { ItemEditButtonBar } from '@webapp/components/ItemEditButtonBar' +import { useI18n } from '@webapp/store/system' + +export const ConfigurationNumericItemEditor = (props) => { + const { labelKey, maxValue = undefined, minValue = undefined, onSave: onSaveProp, value: valueProp } = props + + const i18n = useI18n() + const [state, setState] = useState({ + dirty: false, + editing: false, + value: valueProp, + }) + const { dirty, editing, value } = state + + const onEdit = useCallback(() => setState((statePrev) => ({ ...statePrev, editing: true })), []) + + const onChange = useCallback( + (val) => { + const valueNext = Number(val) + setState((statePrev) => ({ ...statePrev, dirty: Number(valueProp) !== valueNext, value: valueNext })) + }, + [valueProp] + ) + + const onSave = useCallback(() => { + const valueNext = NumberUtils.limit({ minValue, maxValue })(value) + setState((statePrev) => ({ ...statePrev, dirty: false, editing: false, value: valueNext })) + onSaveProp(valueNext) + }, [maxValue, minValue, onSaveProp, value]) + + const onCancel = useCallback(() => { + setState((statePrev) => ({ ...statePrev, editing: false, value: valueProp })) + }, [valueProp]) + + return ( + +
+ + +
+
+ ) +} + +ConfigurationNumericItemEditor.propTypes = { + labelKey: PropTypes.string.isRequired, + maxValue: PropTypes.number, + minValue: PropTypes.number, + onSave: PropTypes.func.isRequired, + value: PropTypes.number.isRequired, +} diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/SurveyConfigurationEditor.js b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/SurveyConfigurationEditor.js new file mode 100644 index 0000000000..66233d7a52 --- /dev/null +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/SurveyConfigurationEditor.js @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react' +import { useDispatch } from 'react-redux' + +import { NumberConversionUtils } from '@core/numberConversionUtils' +import * as Survey from '@core/survey/survey' + +import * as API from '@webapp/service/api' +import { useSurveyInfo } from '@webapp/store/survey' +import { surveyInfoUpdate } from '@webapp/store/survey/surveyInfo/actions' +import { ConfigurationNumericItemEditor } from './ConfigurationNumericItemEditor' + +const { dataStorageUnits, dataStorageValueToBytes, dataStorageValueToUnit, dataStorageBytesToUnit } = + NumberConversionUtils + +const defaultTotalSpaceGB = 10 +const maxTotalSpaceGB = 100 +const defaultTotalSpace = dataStorageValueToBytes(dataStorageUnits.GB)(defaultTotalSpaceGB) + +export const SurveyConfigurationEditor = () => { + const dispatch = useDispatch() + const surveyInfo = useSurveyInfo() + const surveyId = Survey.getId(surveyInfo) + const filesStatistics = Survey.getFilesStatistics(surveyInfo) + + const { usedSpace, totalSpace } = filesStatistics + const minTotalSpace = Math.max(usedSpace, defaultTotalSpace) + const minTotalSpaceGB = dataStorageBytesToUnit(dataStorageUnits.GB)(minTotalSpace) + const totalSpaceGB = dataStorageBytesToUnit(dataStorageUnits.GB)(totalSpace) + + const onTotalSpaceSave = useCallback( + async (value) => { + const valueToStore = dataStorageValueToUnit(dataStorageUnits.GB, dataStorageUnits.MB)(value) + const { survey: surveyInfoUpdated } = await API.updateSurveyConfigurationProp({ + surveyId, + key: Survey.configKeys.filesTotalSpace, + value: valueToStore, + }) + dispatch({ type: surveyInfoUpdate, surveyInfo: surveyInfoUpdated }) + }, + [dispatch, surveyId] + ) + + return ( +
+ +
+ ) +} diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/index.js b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/index.js new file mode 100644 index 0000000000..2e606659fc --- /dev/null +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyConfigurationEditor/index.js @@ -0,0 +1 @@ +export { SurveyConfigurationEditor } from './SurveyConfigurationEditor' diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js b/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js index b9c589dbf9..228e8c4ca4 100644 --- a/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.js @@ -58,10 +58,7 @@ const SurveyInfo = () => { return (
-
- + { onChange={setName} readOnly={readOnly} /> -
+ { {isSystemAdmin && ( - + )} diff --git a/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.scss b/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.scss index de0ee49da9..bae5bb9413 100644 --- a/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.scss +++ b/webapp/views/App/views/Home/SurveyInfo/SurveyInfo.scss @@ -9,7 +9,7 @@ } .form-item { - grid-template-columns: 0.2fr 0.7fr; + grid-template-columns: 0.2fr 0.65fr; } .lanuages_editor__input_chips { @@ -26,6 +26,15 @@ align-items: baseline; } + .survey-info-configuration { + width: 25rem; + justify-self: center; + + .form-item { + grid-template-columns: 0.5fr 0.5fr; + } + } + .btn-save { justify-self: center; } From 052d903aa952f8dd35f7ef80ddb0dbcea3bda0ae Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Mon, 30 Sep 2024 14:52:59 +0200 Subject: [PATCH 6/8] files size formatting: use power of 1024 by default --- webapp/utils/fileUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/utils/fileUtils.js b/webapp/utils/fileUtils.js index f7326c2a78..0093cb1292 100644 --- a/webapp/utils/fileUtils.js +++ b/webapp/utils/fileUtils.js @@ -14,7 +14,7 @@ const getExtension = (file) => { * * @returns {string} - Formatted string. */ -const toHumanReadableFileSize = (bytes, { si = true, decimalPlaces = 1 } = {}) => { +const toHumanReadableFileSize = (bytes, { si = false, decimalPlaces = 1 } = {}) => { const threshold = si ? 1000 : 1024 if (Math.abs(bytes) < threshold) { From bfcb1c14fd33f5b1cdb622ee10bad31ed613d951 Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Tue, 1 Oct 2024 09:30:01 +0200 Subject: [PATCH 7/8] updated arena-server version --- package.json | 2 +- yarn.lock | 199 ++++++++++++--------------------------------------- 2 files changed, 47 insertions(+), 154 deletions(-) diff --git a/package.json b/package.json index 5ce33efc63..cdf7a798c0 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@mui/x-date-pickers": "^7.14.0", "@mui/x-tree-view": "^7.7.1", "@openforis/arena-core": "^0.0.204", - "@openforis/arena-server": "../arena-server", + "@openforis/arena-server": "^0.1.35", "@reduxjs/toolkit": "^2.2.5", "@sendgrid/mail": "^8.1.3", "@shopify/draggable": "^1.1.3", diff --git a/yarn.lock b/yarn.lock index 8c947c18eb..a50d56583a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1791,7 +1791,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.0", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.12.0", "@babel/runtime@^7.8.4": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== @@ -2639,26 +2639,26 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@openforis/arena-core@^0.0.188": - version "0.0.188" - resolved "https://npm.pkg.github.com/download/@openforis/arena-core/0.0.188/ff79e8e185b69aa124595de65c56b16161878f49#ff79e8e185b69aa124595de65c56b16161878f49" - integrity sha512-BiYLoBpaEIbw5R6XJpxiYMjBl5biGLuj15lHyFj0egxWPpXaGuj0rJl1EHpk9DWwPDBKo21vGpuoxZQAjOHKJQ== +"@openforis/arena-core@^0.0.204": + version "0.0.204" + resolved "https://npm.pkg.github.com/download/@openforis/arena-core/0.0.204/aa4aec1004bbd93b778ea3d720f165501f0b4f26#aa4aec1004bbd93b778ea3d720f165501f0b4f26" + integrity sha512-RwcJZD2IU/CBKhktLZZxMb0DzA8afHJSXg6GEzpnJI+VDU6NPhqNyQnNuzWZRotDh8XiU2gctbiLN6Vg0BoIJA== dependencies: "@jsep-plugin/regex" "^1.0.3" bignumber.js "^9.1.2" - date-fns "^2.30.0" jsep "^1.3.8" lodash.differencewith "^4.5.0" lodash.frompairs "^4.0.1" lodash.isequal "^4.5.0" lodash.topairs "^4.3.0" + moment "^2.30.1" proj4 "^2.11.0" - uuid "^9.0.1" + uuid "^10.0.0" -"@openforis/arena-core@^0.0.204": - version "0.0.204" - resolved "https://npm.pkg.github.com/download/@openforis/arena-core/0.0.204/aa4aec1004bbd93b778ea3d720f165501f0b4f26#aa4aec1004bbd93b778ea3d720f165501f0b4f26" - integrity sha512-RwcJZD2IU/CBKhktLZZxMb0DzA8afHJSXg6GEzpnJI+VDU6NPhqNyQnNuzWZRotDh8XiU2gctbiLN6Vg0BoIJA== +"@openforis/arena-core@^0.0.205": + version "0.0.205" + resolved "https://npm.pkg.github.com/download/@openforis/arena-core/0.0.205/7a4a1074ac76b00880789904fc5fd5dc7d1d6382#7a4a1074ac76b00880789904fc5fd5dc7d1d6382" + integrity sha512-JISauhuzUbN/UReULxRRe50P7IhCcowkayt6pXAy8gF4m8VypKU1AfJgXLXZ3eSGnvXwyuk2TXzwrxIJ7epw/Q== dependencies: "@jsep-plugin/regex" "^1.0.3" bignumber.js "^9.1.2" @@ -2671,14 +2671,16 @@ proj4 "^2.11.0" uuid "^10.0.0" -"@openforis/arena-server@../arena-server": - version "0.1.34" +"@openforis/arena-server@^0.1.35": + version "0.1.35" + resolved "https://npm.pkg.github.com/download/@openforis/arena-server/0.1.35/248bbdc007913fc5ad7d914ca6b9a0a11f75ea9e#248bbdc007913fc5ad7d914ca6b9a0a11f75ea9e" + integrity sha512-goNOQ735SbgYNrfxYZPzB43tTn/WHmPcqUKkbWenc5yXeCvPHdxuh76/xOftjqVF/a2jnalTKdtqEhVdJRYrwA== dependencies: "@godaddy/terminus" "^4.12.1" - "@openforis/arena-core" "^0.0.188" + "@openforis/arena-core" "^0.0.205" bcryptjs "^2.4.3" compression "^1.7.4" - connect-pg-simple "^9.0.0" + connect-pg-simple "^10.0.0" db-migrate "^0.11.14" db-migrate-pg "^1.5.2" express "^4.18.2" @@ -2688,30 +2690,8 @@ log4js "^6.9.1" passport "^0.6.0" passport-local "^1.0.0" - pg-promise "^11.5.4" - socket.io "^4.7.2" - -"@openforis/arena-server@^0.1.34": - version "0.1.34" - resolved "https://npm.pkg.github.com/download/@openforis/arena-server/0.1.34/2438e3584eca5cd8ed5a69d44e94a92384433e6c#2438e3584eca5cd8ed5a69d44e94a92384433e6c" - integrity sha512-GQ+vY/TMThhi8xGgAWqukcLz+IsQ+D2lNC4ZyiJxIgSZQps3BtJltADz4Tvfo4dVQ+VRoT7HXQ3EztTKok3kUA== - dependencies: - "@godaddy/terminus" "^4.12.1" - "@openforis/arena-core" "^0.0.188" - bcryptjs "^2.4.3" - compression "^1.7.4" - connect-pg-simple "^9.0.0" - db-migrate "^0.11.14" - db-migrate-pg "^1.5.2" - express "^4.18.2" - express-fileupload "^1.4.0" - express-session "^1.17.2" - lodash.throttle "^4.1.1" - log4js "^6.9.1" - passport "^0.6.0" - passport-local "^1.0.0" - pg-promise "^11.5.4" - socket.io "^4.7.2" + pg-promise "^11.9.1" + socket.io "^4.8.0" "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -3688,15 +3668,6 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/pg@^8.10.2": - version "8.10.2" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.10.2.tgz#7814d1ca02c8071f4d0864c1b17c589b061dba43" - integrity sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw== - dependencies: - "@types/node" "*" - pg-protocol "*" - pg-types "^4.0.1" - "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -5562,13 +5533,12 @@ connect-history-api-fallback@^2.0.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== -connect-pg-simple@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/connect-pg-simple/-/connect-pg-simple-9.0.0.tgz#b09389c905deca5a7ef6220ab8cd4820cc08b6fb" - integrity sha512-tPHR01VQU/qnOl8bMsORD1WyrNkbVkyNmnXfXqaH4NZfxWb3zQkCTPbVHjkhjFpKCQpHA+Ry9Y7medndwyvq6A== +connect-pg-simple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz#972b08d9fc6a1861c523a6c9166240a24b4bc3ca" + integrity sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A== dependencies: - "@types/pg" "^8.10.2" - pg "^8.8.0" + pg "^8.12.0" console-control-strings@^1.1.0: version "1.1.0" @@ -6223,13 +6193,6 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -date-fns@^2.30.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" - integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== - dependencies: - "@babel/runtime" "^7.21.0" - date-format@^4.0.14: version "4.0.14" resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" @@ -6720,10 +6683,10 @@ engine.io-parser@~5.2.1: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== -engine.io@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.2.tgz#769348ced9d56bd47bd83d308ec1c3375e85937c" - integrity sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA== +engine.io@~6.6.0: + version "6.6.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.1.tgz#a82b1e5511239a0e95fac14516870ee9138febc8" + integrity sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -6734,7 +6697,7 @@ engine.io@~6.5.2: cors "~2.8.5" debug "~4.3.1" engine.io-parser "~5.2.1" - ws "~8.11.0" + ws "~8.17.1" enhanced-resolve@^0.9.1: version "0.9.1" @@ -11138,7 +11101,7 @@ object.values@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" -obuf@^1.0.0, obuf@^1.1.2, obuf@~1.1.2: +obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== @@ -11551,21 +11514,11 @@ pg-int8@1.0.1: resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-minify@1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/pg-minify/-/pg-minify-1.6.3.tgz#3def4c876a2d258da20cfdb0e387373d41c7a4dc" - integrity sha512-NoSsPqXxbkD8RIe+peQCqiea4QzXgosdTKY8p7PsbbGsh2F8TifDj/vJxfuR8qJwNYrijdSs7uf0tAe6WOyCsQ== - pg-minify@1.6.5: version "1.6.5" resolved "https://registry.yarnpkg.com/pg-minify/-/pg-minify-1.6.5.tgz#c6a2e23489f28c2944119f559fb4081dfdd347a9" integrity sha512-u0UE8veaCnMfJmoklqneeBBopOAPG3/6DHqGVHYAhz8DkJXh9dnjPlz25fRxn4e+6XVzdOp7kau63Rp52fZ3WQ== -pg-numeric@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" - integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== - pg-pool@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7" @@ -11576,17 +11529,7 @@ pg-pool@^3.6.2: resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== -pg-promise@^11.5.4: - version "11.5.4" - resolved "https://registry.yarnpkg.com/pg-promise/-/pg-promise-11.5.4.tgz#2436aa4697a56511ec7b017f7db9a06a990f8f6a" - integrity sha512-esYSkDt2h6NQOkfotGAm1Ld5OjoITJLpB88Z1PIlcAU/RQ0XQE2PxW0bLJEOMHPGV5iaRnj1Y7ARznXbgN4FNw== - dependencies: - assert-options "0.8.1" - pg "8.11.3" - pg-minify "1.6.3" - spex "3.3.0" - -pg-promise@^11.8.0: +pg-promise@^11.8.0, pg-promise@^11.9.1: version "11.9.1" resolved "https://registry.yarnpkg.com/pg-promise/-/pg-promise-11.9.1.tgz#e203d4cc647b825490b0ed990d7e8fc8f2decd8b" integrity sha512-qvMmyDvWd64X0a25hCuWV40GLMbgeYf4z7ZmzxQqGHtUIlzMtxcMtaBHAMr7XVOL62wFv2ZVKW5pFruD/4ZAOg== @@ -11596,7 +11539,7 @@ pg-promise@^11.8.0: pg-minify "1.6.5" spex "3.3.0" -pg-protocol@*, pg-protocol@^1.6.0: +pg-protocol@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== @@ -11624,34 +11567,6 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg-types@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.1.tgz#31857e89d00a6c66b06a14e907c3deec03889542" - integrity sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g== - dependencies: - pg-int8 "1.0.1" - pg-numeric "1.0.2" - postgres-array "~3.0.1" - postgres-bytea "~3.0.0" - postgres-date "~2.0.1" - postgres-interval "^3.0.0" - postgres-range "^1.1.1" - -pg@8.11.3, pg@^8.11.2: - version "8.11.3" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb" - integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.6.2" - pg-pool "^3.6.1" - pg-protocol "^1.6.0" - pg-types "^2.1.0" - pgpass "1.x" - optionalDependencies: - pg-cloudflare "^1.1.1" - pg@8.12.0, pg@^8.12.0: version "8.12.0" resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" @@ -11665,10 +11580,10 @@ pg@8.12.0, pg@^8.12.0: optionalDependencies: pg-cloudflare "^1.1.1" -pg@^8.8.0: - version "8.11.2" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.2.tgz#1a23f6de7bfb65ba56e4dd15df96668d319900c4" - integrity sha512-l4rmVeV8qTIrrPrIR3kZQqBgSN93331s9i6wiUiLOSk0Q7PmUxZD/m1rQI622l3NfqBby9Ar5PABfS/SulfieQ== +pg@^8.11.2: + version "8.11.3" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.11.3.tgz#d7db6e3fe268fcedd65b8e4599cda0b8b4bf76cb" + integrity sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g== dependencies: buffer-writer "2.0.0" packet-reader "1.0.0" @@ -12075,33 +11990,16 @@ postgres-array@~2.0.0: resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== -postgres-array@~3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" - integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== - postgres-bytea@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== -postgres-bytea@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" - integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== - dependencies: - obuf "~1.1.2" - postgres-date@~1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== -postgres-date@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.0.1.tgz#638b62e5c33764c292d37b08f5257ecb09231457" - integrity sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw== - postgres-interval@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" @@ -12109,16 +12007,6 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" -postgres-interval@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" - integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== - -postgres-range@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76" - integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g== - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -13529,16 +13417,16 @@ socket.io-parser@~4.2.4: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" - integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== +socket.io@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.0.tgz#33d05ae0915fad1670bd0c4efcc07ccfabebe3b1" + integrity sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA== dependencies: accepts "~1.3.4" base64id "~2.0.0" cors "~2.8.5" debug "~4.3.2" - engine.io "~6.5.2" + engine.io "~6.6.0" socket.io-adapter "~2.5.2" socket.io-parser "~4.2.4" @@ -15260,6 +15148,11 @@ ws@~8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + xml-js@^1.6.11: version "1.6.11" resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" From b2fa1eef7728e9d238490e9815d8b31f88142cd1 Mon Sep 17 00:00:00 2001 From: Stefano Ricci Date: Tue, 1 Oct 2024 09:30:08 +0200 Subject: [PATCH 8/8] code cleanup --- webapp/components/ItemEditButtonBar.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/webapp/components/ItemEditButtonBar.js b/webapp/components/ItemEditButtonBar.js index 759e3a614c..4f754172a1 100644 --- a/webapp/components/ItemEditButtonBar.js +++ b/webapp/components/ItemEditButtonBar.js @@ -1,6 +1,7 @@ import './ItemEditButtonBar.scss' import React from 'react' +import PropTypes from 'prop-types' import * as Validation from '@core/validation/validation' import { ButtonCancel, ButtonDelete, ButtonIconEdit, ButtonSave } from './buttons' @@ -34,3 +35,14 @@ export const ItemEditButtonBar = (props) => {
) } + +ItemEditButtonBar.propTypes = { + dirty: PropTypes.bool, + editing: PropTypes.bool, + onCancel: PropTypes.func.isRequired, + onDelete: PropTypes.func, + onEdit: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + readOnly: PropTypes.bool, + validation: PropTypes.object, +}