diff --git a/src/base-interfaces.ts b/src/base-interfaces.ts index 1dd0124298..bf9eacd505 100644 --- a/src/base-interfaces.ts +++ b/src/base-interfaces.ts @@ -391,4 +391,8 @@ export interface IPlainDataOptions { } export interface ILoadFromJSONOptions { validatePropertyValues?: boolean; +} +export interface ISaveToJSONOptions { + storeDefaults?: boolean; + version?: string; } \ No newline at end of file diff --git a/src/base.ts b/src/base.ts index b3ce6027c4..436a5da2cc 100644 --- a/src/base.ts +++ b/src/base.ts @@ -9,7 +9,7 @@ import { } from "./jsonobject"; import { settings } from "./settings"; import { ItemValue } from "./itemvalue"; -import { IElement, IFindElement, IProgressInfo, ISurvey, ILoadFromJSONOptions } from "./base-interfaces"; +import { IElement, IFindElement, IProgressInfo, ISurvey, ILoadFromJSONOptions, ISaveToJSONOptions } from "./base-interfaces"; import { ExpressionRunner } from "./conditions"; import { surveyLocalization } from "./surveyStrings"; import { ConsoleWarnings } from "./console-warnings"; @@ -402,8 +402,8 @@ export class Base { * Returns a JSON object that corresponds to the current SurveyJS object. * @see fromJSON */ - public toJSON(): any { - return new JsonObject().toJsonObject(this); + public toJSON(options?: ISaveToJSONOptions): any { + return new JsonObject().toJsonObject(this, options); } /** * Assigns a new configuration to the current SurveyJS object. This configuration is taken from a passed JSON object. diff --git a/src/helpers.ts b/src/helpers.ts index dc6f86aa39..cf0b6e0c9f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -398,6 +398,25 @@ export class Helpers { } return val; } + public static compareVerions(ver1: string, ver2: string): number { + if(!ver1 && !ver2) return 0; + const ver1Ar = ver1.split("."); + const ver2Ar = ver2.split("."); + const len1 = ver1Ar.length; + const len2 = ver2Ar.length; + for(let i = 0; i < len1 && i < len2; i ++) { + const str1 = ver1Ar[i]; + const str2 = ver2Ar[i]; + if(str1.length === str2.length) { + if(str1 !== str2) { + return str1 < str2 ? -1 : 1; + } + } else { + return str1.length < str2.length ? -1 : 1; + } + } + return len1 === len2 ? 0 : (len1 < len2 ? -1 : 1); + } } if (!(String.prototype)["format"]) { (String.prototype)["format"] = function() { diff --git a/src/jsonobject.ts b/src/jsonobject.ts index 4e73a9d563..292e506047 100644 --- a/src/jsonobject.ts +++ b/src/jsonobject.ts @@ -1,7 +1,7 @@ import { surveyLocalization } from "./surveyStrings"; import { Base, ComputedUpdater } from "./base"; import { Helpers, HashTable } from "./helpers"; -import { ILoadFromJSONOptions } from "./base-interfaces"; +import { ILoadFromJSONOptions, ISaveToJSONOptions } from "./base-interfaces"; export interface IPropertyDecoratorOptions { defaultValue?: T; @@ -202,6 +202,7 @@ export class JsonObjectProperty implements IObject { "className", "alternativeName", "layout", + "version", "classNamePart", "baseClassName", "defaultValue", @@ -263,6 +264,7 @@ export class JsonObjectProperty implements IObject { public minValue: any; private dataListValue: Array; public layout: string; + public version: string; public onSerializeValue: (obj: any) => any; public onGetValue: (obj: any) => any; public onSettingValue: (obj: any, value: any) => any; @@ -455,6 +457,18 @@ export class JsonObjectProperty implements IObject { public set visible(val: boolean) { this.visibleValue = val; } + public isAvailableInVersion(ver: string): boolean { + if(!!this.alternativeName) return true; + return this.isAvailableInVersionCore(ver); + } + public getSerializedName(ver: string): string { + if(!this.alternativeName) return this.name; + return this.isAvailableInVersionCore(ver) ? this.name : this.alternativeName; + } + private isAvailableInVersionCore(ver: string): boolean { + if(!ver || !this.version) return true; + return Helpers.compareVerions(this.version, ver) <= 0; + } public get isLocalizable(): boolean { return this.isLocalizableValue != null ? this.isLocalizableValue : false; } @@ -914,6 +928,9 @@ export class JsonMetadataClass { if (propInfo.layout) { prop.layout = propInfo.layout; } + if (propInfo.version) { + prop.version = propInfo.version; + } if (propInfo.dependsOn) { this.addDependsOnProperties(prop, propInfo.dependsOn); } @@ -1566,8 +1583,8 @@ export class JsonObject { public errors = new Array(); public lightSerializing: boolean = false; public options: ILoadFromJSONOptions; - public toJsonObject(obj: any, storeDefaults = false): any { - return this.toJsonObjectCore(obj, null, storeDefaults); + public toJsonObject(obj: any, options?: ISaveToJSONOptions | boolean): any { + return this.toJsonObjectCore(obj, null, options); } public toObject(jsonObj: any, obj: any, options?: ILoadFromJSONOptions): void { this.toObjectCore(jsonObj, obj, options); @@ -1616,7 +1633,7 @@ export class JsonObject { public toJsonObjectCore( obj: any, property: JsonObjectProperty, - storeDefaults = false + options?: ISaveToJSONOptions | boolean ): any { if (!obj || !obj.getType) return obj; if (typeof obj.getData === "function") return obj.getData(); @@ -1626,17 +1643,24 @@ export class JsonObject { obj.getType() ); } + const storeDefaults = options === true; + if(!options || options === true) { + options = { }; + } + if(storeDefaults) { + options.storeDefaults = storeDefaults; + } this.propertiesToJson( obj, Serializer.getProperties(obj.getType()), result, - storeDefaults + options ); this.propertiesToJson( obj, this.getDynamicProperties(obj), result, - storeDefaults + options ); return result; } @@ -1663,40 +1687,35 @@ export class JsonObject { obj: any, properties: Array, json: any, - storeDefaults = false + options: ISaveToJSONOptions ) { for (var i: number = 0; i < properties.length; i++) { - this.valueToJson(obj, json, properties[i], storeDefaults); + this.valueToJson(obj, json, properties[i], options); } } - public valueToJson( - obj: any, - result: any, - property: JsonObjectProperty, - storeDefaults = false - ): void { - if ( - property.isSerializable === false || - (property.isLightSerializable === false && this.lightSerializing) - ) - return; - var value = property.getSerializableValue(obj); - if (!storeDefaults && property.isDefaultValueByObj(obj, value)) return; + public valueToJson(obj: any, result: any, prop: JsonObjectProperty, options?: ISaveToJSONOptions): void { + if(!options) options = {}; + if (prop.isSerializable === false || (prop.isLightSerializable === false && this.lightSerializing)) return; + if(options.version && !prop.isAvailableInVersion(options.version)) return; + var value = prop.getSerializableValue(obj); + if (!options.storeDefaults && prop.isDefaultValueByObj(obj, value)) return; if (this.isValueArray(value)) { var arrValue = []; for (var i = 0; i < value.length; i++) { - arrValue.push(this.toJsonObjectCore(value[i], property, storeDefaults)); + arrValue.push(this.toJsonObjectCore(value[i], prop, options)); } value = arrValue.length > 0 ? arrValue : null; } else { - value = this.toJsonObjectCore(value, property, storeDefaults); + value = this.toJsonObjectCore(value, prop, options); } + if(value === undefined || value === null) return; + const name = prop.getSerializedName(options.version); var hasValue = typeof obj["getPropertyValue"] === "function" && - obj["getPropertyValue"](property.name, null) !== null; - if ((storeDefaults && hasValue) || !property.isDefaultValueByObj(obj, value)) { - if (!Serializer.onSerializingProperty || !Serializer.onSerializingProperty(obj, property, value, result)) { - result[property.name] = this.removePosOnValueToJson(property, value); + obj["getPropertyValue"](name, null) !== null; + if ((options.storeDefaults && hasValue) || !prop.isDefaultValueByObj(obj, value)) { + if (!Serializer.onSerializingProperty || !Serializer.onSerializingProperty(obj, prop, value, result)) { + result[name] = this.removePosOnValueToJson(prop, value); } } } diff --git a/tests/helperstests.ts b/tests/helperstests.ts index 8c8d458d80..9b8239fce9 100644 --- a/tests/helperstests.ts +++ b/tests/helperstests.ts @@ -527,4 +527,17 @@ QUnit.test("base.equals", function(assert) { assert.equal(Helpers.isTwoValueEquals(q1, q3), true, "#2"); assert.equal(Helpers.isTwoValueEquals(q1, q4), false, "#3"); assert.equal(Helpers.isTwoValueEquals(q1, q5), false, "#4"); -}); \ No newline at end of file +}); +QUnit.test("compareVersions", function(assert) { + assert.equal(Helpers.compareVerions("", ""), 0, "#1"); + assert.equal(Helpers.compareVerions("1", ""), 1, "#2"); + assert.equal(Helpers.compareVerions("", "1"), -1, "#3"); + assert.equal(Helpers.compareVerions("1.2.3", "1.2.3"), 0, "#4"); + assert.equal(Helpers.compareVerions("1.201.31", "1.201.31"), 0, "#5"); + assert.equal(Helpers.compareVerions("1.201.31", "1.90.31"), 1, "#6"); + assert.equal(Helpers.compareVerions("1.90.31", "1.201.31"), -1, "#7"); + assert.equal(Helpers.compareVerions("1", "1.2.3"), -1, "#8"); + assert.equal(Helpers.compareVerions("1.2.3", "1"), 1, "#9"); + assert.equal(Helpers.compareVerions("1.2", "1.2.3"), -1, "#10"); + assert.equal(Helpers.compareVerions("1.2.3", "1.2"), 1, "#11"); +}); diff --git a/tests/jsonobjecttests.ts b/tests/jsonobjecttests.ts index 4b0eb875df..7759434b05 100644 --- a/tests/jsonobjecttests.ts +++ b/tests/jsonobjecttests.ts @@ -1846,8 +1846,10 @@ QUnit.test( assert.equal( json["readOnly"], false, - "default value for readOnly proeprty has been serialzied successfully" + "default value for readOnly property has been serialzied successfully" ); + const jsonKeys = Object.keys(json); + assert.equal(jsonKeys.indexOf("validators"), -1, "no validators"); q2.readOnly = true; new JsonObject().toObject(json, q2); @@ -3148,3 +3150,44 @@ QUnit.test("Check existing pos", function (assert) { assert.deepEqual(json, { name: "question1", testProperty: { someProperty: "bbb" } }, "no pos in json"); Serializer.removeProperty("question", "testProperty"); }); +QUnit.test("Versions in property", function (assert) { + const prop = Serializer.addProperty("question", { name: "testProperty", version: "1.9.127" }); + assert.equal(prop.version, "1.9.127", "version is set correclty"); + assert.equal(prop.isAvailableInVersion("1.9.5"), false, "#1"); + assert.equal(prop.isAvailableInVersion("1.9.200"), true, "#2"); + assert.equal(prop.isAvailableInVersion("2"), true, "#3"); + assert.equal(prop.isAvailableInVersion("1"), false, "#4"); + assert.equal(prop.isAvailableInVersion("1.9.127"), true, "#5"); + assert.equal(prop.isAvailableInVersion("1.9.126"), false, "#6"); + assert.equal(prop.isAvailableInVersion("1.9.128"), true, "#7"); + assert.equal(prop.isAvailableInVersion(""), true, "#8"); + Serializer.removeProperty("question", "testProperty"); +}); +QUnit.test("Versions in new property serialization", function (assert) { + Serializer.addProperty("question", { name: "testProperty", version: "1.9.127" }); + const question = new QuestionTextModel("q1"); + question.testProperty = "abc"; + assert.deepEqual(question.toJSON(), { name: "q1", testProperty: "abc" }, "#1"); + assert.deepEqual(question.toJSON({ version: "1.9.127" }), { name: "q1", testProperty: "abc" }, "#2"); + assert.deepEqual(question.toJSON({ version: "1.9.126" }), { name: "q1" }, "#3"); + Serializer.removeProperty("question", "testProperty"); +}); +QUnit.test("Versions & alternative name", function (assert) { + const prop = Serializer.addProperty("question", { name: "testProperty", version: "1.9.127", alternativeName: "testProp" }); + + assert.equal(prop.isAvailableInVersion("1.9.5"), true, "isAvailableInVersion: #1"); + assert.equal(prop.isAvailableInVersion("1.9.200"), true, "isAvailableInVersion: #2"); + assert.equal(prop.isAvailableInVersion(""), true, "isAvailableInVersion: #3"); + assert.equal(prop.getSerializedName("1.9.5"), "testProp", "getSerializedName: #1"); + assert.equal(prop.getSerializedName("1.9.200"), "testProperty", "getSerializedName: #2"); + assert.equal(prop.getSerializedName(""), "testProperty", "getSerializedName: #3"); + + const question = new QuestionTextModel("q1"); + question.testProperty = "abc"; + assert.deepEqual(question.toJSON(), { name: "q1", testProperty: "abc" }, "#1"); + assert.deepEqual(question.toJSON({ version: "1.9.127" }), { name: "q1", testProperty: "abc" }, "#2"); + assert.deepEqual(question.toJSON({ version: "1.9.128" }), { name: "q1", testProperty: "abc" }, "#3"); + assert.deepEqual(question.toJSON({ version: "1.9.126" }), { name: "q1", testProp: "abc" }, "#4"); + assert.deepEqual(question.toJSON({ version: "1" }), { name: "q1", testProp: "abc" }, "#5"); + Serializer.removeProperty("question", "testProperty"); +});