diff --git a/integration-tests/src/types.test.ts b/integration-tests/src/types.test.ts index 6c5f624a..8327d836 100644 --- a/integration-tests/src/types.test.ts +++ b/integration-tests/src/types.test.ts @@ -480,10 +480,211 @@ test("Delete type (existing lines)", async () => { }); }); -// Rename field, make sure markers are updated -// Rename field, make sure lines are updated -// Rename dropdown option, make sure markers are updated -// Rename dropdown option, make sure lines are updated +test("Rename field (marker type)", async () => { + const client = await openClient(); + + await createTemporaryPad(client, { createDefaultTypes: false }, async (createPadData, padData, result) => { + const type = await client.addType({ + name: "Test type", + type: "marker", + fields: [ + { name: "Field 1", type: "input" }, + { name: "Field 2", type: "input" } + ] + }); + + await client.updateBbox({ top: 1, right: 1, bottom: -1, left: -1, zoom: 0 }); // To have marker in bbox + + const marker = await client.addMarker({ + lat: 0, + lon: 0, + typeId: type.id, + data: { + "Field 1": "value 1", + "Field 2": "value 2" + } + }); + + const onMarker = vi.fn(); + client.on("marker", onMarker); + + await client.editType({ + id: type.id, + fields: [ + { oldName: "Field 1", name: "Field 1 new", type: "input" }, + { name: "Field 2", type: "input" } + ] + }); + + await retry(() => { + expect(onMarker).toBeCalledTimes(1); + }); + + expect(client.markers[marker.id].data).toEqual({ + "Field 1 new": "value 1", + "Field 2": "value 2" + }); + }); +}); + +test("Rename field (line type)", async () => { + const client = await openClient(); + + await createTemporaryPad(client, { createDefaultTypes: false }, async (createPadData, padData, result) => { + const type = await client.addType({ + name: "Test type", + type: "line", + fields: [ + { name: "Field 1", type: "input" }, + { name: "Field 2", type: "input" } + ] + }); + + const line = await client.addLine({ + routePoints: [ + { lat: 0, lon: 0 }, + { lat: 1, lon: 1 } + ], + typeId: type.id, + data: { + "Field 1": "value 1", + "Field 2": "value 2" + } + }); + + const onLine = vi.fn(); + client.on("line", onLine); + + await client.editType({ + id: type.id, + fields: [ + { oldName: "Field 1", name: "Field 1 new", type: "input" }, + { name: "Field 2", type: "input" } + ] + }); + + await retry(() => { + expect(onLine).toBeCalledTimes(1); + }); + + expect(client.lines[line.id].data).toEqual({ + "Field 1 new": "value 1", + "Field 2": "value 2" + }); + }); +}); + +test("Rename dropdown option (marker type)", async () => { + const client = await openClient(); + + await createTemporaryPad(client, { createDefaultTypes: false }, async (createPadData, padData, result) => { + const type = await client.addType({ + name: "Test type", + type: "marker", + fields: [ + { name: "Dropdown", type: "dropdown", options: [ { value: "Option 1" }, { value: "Option 2" } ] }, + ] + }); + + await client.updateBbox({ top: 1, right: 1, bottom: -1, left: -1, zoom: 0 }); // To have marker in bbox + + const marker1 = await client.addMarker({ + lat: 0, + lon: 0, + typeId: type.id, + data: { + "Dropdown": "Option 1" + } + }); + + const marker2 = await client.addMarker({ + lat: 0, + lon: 0, + typeId: type.id, + data: { + "Dropdown": "Option 2" + } + }); + + const onMarker = vi.fn(); + client.on("marker", onMarker); + + await client.editType({ + id: type.id, + fields: [ + { name: "Dropdown", type: "dropdown", options: [ { value: "Option 1" }, { oldValue: "Option 2", value: "Option 2 new" } ] } + ] + }); + + await retry(() => { + expect(onMarker).toBeCalledTimes(1); + }); + + expect(client.markers[marker1.id].data).toEqual({ + "Dropdown": "Option 1" + }); + expect(client.markers[marker2.id].data).toEqual({ + "Dropdown": "Option 2 new" + }); + }); +}); + +test("Rename dropdown option (line type)", async () => { + const client = await openClient(); + + await createTemporaryPad(client, { createDefaultTypes: false }, async (createPadData, padData, result) => { + const type = await client.addType({ + name: "Test type", + type: "line", + fields: [ + { name: "Dropdown", type: "dropdown", options: [ { value: "Option 1" }, { value: "Option 2" } ] }, + ] + }); + + const line1 = await client.addLine({ + routePoints: [ + { lat: 0, lon: 0 }, + { lat: 1, lon: 1 } + ], + typeId: type.id, + data: { + "Dropdown": "Option 1" + } + }); + + const line2 = await client.addLine({ + routePoints: [ + { lat: 0, lon: 0 }, + { lat: 1, lon: 1 } + ], + typeId: type.id, + data: { + "Dropdown": "Option 2" + } + }); + + const onLine = vi.fn(); + client.on("line", onLine); + + await client.editType({ + id: type.id, + fields: [ + { name: "Dropdown", type: "dropdown", options: [ { value: "Option 1" }, { oldValue: "Option 2", value: "Option 2 new" } ] } + ] + }); + + await retry(() => { + expect(onLine).toBeCalledTimes(1); + }); + + expect(client.lines[line1.id].data).toEqual({ + "Dropdown": "Option 1" + }); + expect(client.lines[line2.id].data).toEqual({ + "Dropdown": "Option 2 new" + }); + }); +}); // New marker (default values) is created with default settings // New line (default values) is created with default settings diff --git a/server/src/database/line.ts b/server/src/database/line.ts index 260ada5e..02b3a418 100644 --- a/server/src/database/line.ts +++ b/server/src/database/line.ts @@ -234,27 +234,27 @@ export default class DatabaseLines { throw new Error(`Cannot use ${newType.type} type for line.`); } - const update = { - ...resolveUpdateLine(originalLine, data, newType), - routePoints: data.routePoints || originalLine.routePoints, - mode: (data.mode ?? originalLine.mode) || "" - }; + const update = resolveUpdateLine(originalLine, data, newType); let routeInfo: RouteInfo | undefined; - if((update.mode == "track" && update.trackPoints) || !isEqual(update.routePoints, originalLine.routePoints) || update.mode != originalLine.mode) - routeInfo = await calculateRouteForLine(update, trackPointsFromRoute); + if((update.mode == "track" && update.trackPoints) || (update.routePoints && !isEqual(update.routePoints, originalLine.routePoints)) || (update.mode != null && update.mode != originalLine.mode)) + routeInfo = await calculateRouteForLine({ ...originalLine, ...update }, trackPointsFromRoute); Object.assign(update, mapValues(routeInfo, (val) => val == null ? null : val)); // Use null instead of undefined delete update.trackPoints; // They came if mode is track - const newLine = await this._db.helpers._updatePadObject("Line", originalLine.padId, originalLine.id, update, noHistory); + if (Object.keys(update).length > 0) { + const newLine = await this._db.helpers._updatePadObject("Line", originalLine.padId, originalLine.id, update, noHistory); - this._db.emit("line", originalLine.padId, newLine); + this._db.emit("line", originalLine.padId, newLine); - if(routeInfo) - await this._setLinePoints(originalLine.padId, originalLine.id, routeInfo.trackPoints); + if(routeInfo) + await this._setLinePoints(originalLine.padId, originalLine.id, routeInfo.trackPoints); - return newLine; + return newLine; + } else { + return originalLine; + } } async _setLinePoints(padId: PadId, lineId: ID, trackPoints: Point[], _noEvent?: boolean): Promise { diff --git a/server/src/database/marker.ts b/server/src/database/marker.ts index 5152c6e6..efaf55df 100644 --- a/server/src/database/marker.ts +++ b/server/src/database/marker.ts @@ -123,21 +123,25 @@ export default class DatabaseMarkers { const update = resolveUpdateMarker(originalMarker, data, newType); - const result = await this._db.helpers._updatePadObject("Marker", originalMarker.padId, originalMarker.id, update, noHistory); - - this._db.emit("marker", originalMarker.padId, result); + if (Object.keys(update).length > 0) { + const result = await this._db.helpers._updatePadObject("Marker", originalMarker.padId, originalMarker.id, update, noHistory); + + this._db.emit("marker", originalMarker.padId, result); + + if (update.lat != null && update.lon != null && update.ele === undefined) { + getElevationForPoint({ lat: update.lat, lon: update.lon }).then(async (ele) => { + if (ele != null) { + await this.updateMarker(originalMarker.padId, originalMarker.id, { ele }, true); + } + }).catch((err) => { + console.warn("Error updating marker elevation", err); + }); + } - if (update.lat != null && update.lon != null && update.ele === undefined) { - getElevationForPoint({ lat: update.lat, lon: update.lon }).then(async (ele) => { - if (ele != null) { - await this.updateMarker(originalMarker.padId, originalMarker.id, { ele }, true); - } - }).catch((err) => { - console.warn("Error updating marker elevation", err); - }); + return result; + } else { + return originalMarker; } - - return result; } async deleteMarker(padId: PadId, markerId: ID): Promise { diff --git a/server/src/database/type.ts b/server/src/database/type.ts index f907721d..88a73925 100644 --- a/server/src/database/type.ts +++ b/server/src/database/type.ts @@ -125,7 +125,30 @@ export default class DatabaseTypes { return createdType; } - async updateType(padId: PadId, typeId: ID, data: Omit, "id">, _doNotUpdateStyles?: boolean): Promise { + async updateType(padId: PadId, typeId: ID, data: Omit, "id">): Promise { + const rename: Record }> = {}; + for(const field of (data.fields || [])) { + if(field.oldName && field.oldName != field.name) + rename[field.oldName] = { name: field.name }; + + if(field.options) { + for(const option of field.options) { + if(option.oldValue && option.oldValue != option.value) { + if(!rename[field.oldName || field.name]) + rename[field.oldName || field.name] = { }; + if(!rename[field.oldName || field.name].values) + rename[field.oldName || field.name].values = { }; + + rename[field.oldName || field.name].values![option.oldValue] = option.value; + } + + delete option.oldValue; + } + } + + delete field.oldName; + } + if (data.idx != null) { await this._freeTypeIdx(padId, typeId, data.idx); } @@ -133,8 +156,10 @@ export default class DatabaseTypes { const result = await this._db.helpers._updatePadObject("Type", padId, typeId, data); this._db.emit("type", result.padId, result); - if(!_doNotUpdateStyles) - await this.recalculateObjectStylesForType(result.padId, typeId, result.type == "line"); + if(Object.keys(rename).length > 0) + await this._db.helpers.renameObjectDataField(padId, result.id, rename, result.type == "line"); + + await this.recalculateObjectStylesForType(result.padId, typeId, result.type == "line"); return result; } diff --git a/server/src/socket/socket-v2.ts b/server/src/socket/socket-v2.ts index 9c290602..9beacfd5 100644 --- a/server/src/socket/socket-v2.ts +++ b/server/src/socket/socket-v2.ts @@ -357,39 +357,7 @@ export class SocketConnectionV2 extends SocketConnection { if (!isPadId(this.padId)) throw new Error("No map opened."); - const rename: Record }> = {}; - for(const field of (data.fields || [])) { - if(field.oldName && field.oldName != field.name) - rename[field.oldName] = { name: field.name }; - - if(field.options) { - for(const option of field.options) { - if(option.oldValue && option.oldValue != option.value) { - if(!rename[field.oldName || field.name]) - rename[field.oldName || field.name] = { }; - if(!rename[field.oldName || field.name].values) - rename[field.oldName || field.name].values = { }; - - rename[field.oldName || field.name].values![option.oldValue] = option.value; - } - - delete option.oldValue; - } - } - - delete field.oldName; - } - - // We first update the type (without updating the styles). If that succeeds, we rename the data fields. - // Only then we update the object styles (as they often depend on the field values). - const newType = await this.database.types.updateType(this.padId, data.id, data, false) - - if(Object.keys(rename).length > 0) - await this.database.helpers.renameObjectDataField(this.padId, data.id, rename, newType.type == "line"); - - await this.database.types.recalculateObjectStylesForType(newType.padId, newType.id, newType.type == "line") - - return newType; + return await this.database.types.updateType(this.padId, data.id, data); }, deleteType: async (data) => {