diff --git a/services/static-webserver/client/source/class/osparc/dashboard/SortedByMenuButton.js b/services/static-webserver/client/source/class/osparc/dashboard/SortedByMenuButton.js index 24427e4995b..7bb0bcb8d4a 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/SortedByMenuButton.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/SortedByMenuButton.js @@ -65,6 +65,7 @@ qx.Class.define("osparc.dashboard.SortedByMenuButton", { field: "last_change_date", direction: "desc" }, + getSortByOptions: function() { return [{ id: "name", diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index ad1b51cc840..d87f6c690bf 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -98,6 +98,7 @@ qx.Class.define("osparc.data.Resources", { * added by oSPARC as compilation vars */ "appSummary": { + useCache: false, endpoints: { get: { method: "GET", diff --git a/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js b/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js index e4798ed1464..dc029edbba9 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js +++ b/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js @@ -103,6 +103,11 @@ qx.Class.define("osparc.navigation.UserMenu", { control.addListener("execute", () => osparc.cluster.Utils.popUpClustersDetails(), this); this.add(control); break; + case "market": + control = new qx.ui.menu.Button(this.tr("Market")); + control.addListener("execute", () => osparc.vipMarket.MarketWindow.openWindow()); + this.add(control); + break; case "about": control = new qx.ui.menu.Button(this.tr("About oSPARC")); osparc.utils.Utils.setIdToWidget(control, "userMenuAboutBtn"); @@ -178,6 +183,11 @@ qx.Class.define("osparc.navigation.UserMenu", { this.addSeparator(); this.__addAnnouncements(); + + if (osparc.product.Utils.showS4LStore()) { + this.getChildControl("market"); + } + this.getChildControl("about"); if (osparc.product.Utils.showAboutProduct()) { this.getChildControl("about-product"); @@ -241,6 +251,11 @@ qx.Class.define("osparc.navigation.UserMenu", { this.addSeparator(); this.__addAnnouncements(); + + if (osparc.product.Utils.showS4LStore()) { + this.getChildControl("market"); + } + this.getChildControl("about"); if (!osparc.product.Utils.isProduct("osparc")) { this.getChildControl("about-product"); diff --git a/services/static-webserver/client/source/class/osparc/product/Utils.js b/services/static-webserver/client/source/class/osparc/product/Utils.js index 123d993e01b..45d3b7de661 100644 --- a/services/static-webserver/client/source/class/osparc/product/Utils.js +++ b/services/static-webserver/client/source/class/osparc/product/Utils.js @@ -270,6 +270,14 @@ qx.Class.define("osparc.product.Utils", { return true; }, + showS4LStore: function() { + const platformName = osparc.store.StaticInfo.getInstance().getPlatformName(); + if (platformName !== "master") { + return false; + } + return this.isS4LProduct(); + }, + getProductThumbUrl: function(asset = "Default.png") { const base = "https://raw.githubusercontent.com/ZurichMedTech/s4l-assets/main/app/full/project_thumbnails" let url; diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js new file mode 100644 index 00000000000..40ffda37b27 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js @@ -0,0 +1,206 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + const layout = new qx.ui.layout.Grow(); + this._setLayout(layout); + + this.__poplulateLayout(); + }, + + events: { + "modelLeased": "qx.event.type.Event", + }, + + properties: { + anatomicalModelsData: { + check: "Object", + init: null, + nullable: true, + apply: "__poplulateLayout" + }, + }, + + members: { + __poplulateLayout: function() { + this._removeAll(); + + const anatomicalModelsData = this.getAnatomicalModelsData(); + if (anatomicalModelsData) { + const card = this.__createcCard(anatomicalModelsData); + this._add(card); + } else { + const selectModelLabel = new qx.ui.basic.Label().set({ + value: this.tr("Select a model for more details"), + font: "text-16", + alignX: "center", + alignY: "middle", + allowGrowX: true, + allowGrowY: true, + }); + this._add(selectModelLabel); + } + }, + + __createcCard: function(anatomicalModelsData) { + console.log(anatomicalModelsData); + + const cardGrid = new qx.ui.layout.Grid(16, 16); + const cardLayout = new qx.ui.container.Composite(cardGrid); + + const description = anatomicalModelsData["Description"]; + description.split(" - ").forEach((desc, idx) => { + const titleLabel = new qx.ui.basic.Label().set({ + value: desc, + font: "text-16", + alignX: "center", + alignY: "middle", + allowGrowX: true, + allowGrowY: true, + }); + cardLayout.add(titleLabel, { + column: 0, + row: idx, + colSpan: 2, + }); + }); + + const thumbnail = new qx.ui.basic.Image().set({ + source: anatomicalModelsData["Thumbnail"], + alignY: "middle", + scale: true, + allowGrowX: true, + allowGrowY: true, + allowShrinkX: true, + allowShrinkY: true, + maxWidth: 256, + maxHeight: 256, + }); + cardLayout.add(thumbnail, { + column: 0, + row: 2, + }); + + const features = anatomicalModelsData["Features"]; + const featuresGrid = new qx.ui.layout.Grid(8, 8); + const featuresLayout = new qx.ui.container.Composite(featuresGrid); + let idx = 0; + [ + "Name", + "Version", + "Sex", + "Age", + "Weight", + "Height", + "Date", + "Ethnicity", + "Functionality", + ].forEach(key => { + if (key.toLowerCase() in features) { + const titleLabel = new qx.ui.basic.Label().set({ + value: key, + font: "text-14", + alignX: "right", + }); + featuresLayout.add(titleLabel, { + column: 0, + row: idx, + }); + + const nameLabel = new qx.ui.basic.Label().set({ + value: features[key.toLowerCase()], + font: "text-14", + alignX: "left", + }); + featuresLayout.add(nameLabel, { + column: 1, + row: idx, + }); + + idx++; + } + }); + + const doiTitle = new qx.ui.basic.Label().set({ + value: "DOI", + font: "text-14", + alignX: "right", + marginTop: 16, + }); + featuresLayout.add(doiTitle, { + column: 0, + row: idx, + }); + + const doiValue = new qx.ui.basic.Label().set({ + value: anatomicalModelsData["DOI"] ? anatomicalModelsData["DOI"] : "-", + font: "text-14", + alignX: "left", + marginTop: 16, + }); + featuresLayout.add(doiValue, { + column: 1, + row: idx, + }); + + cardLayout.add(featuresLayout, { + column: 1, + row: 2, + }); + + const buttonsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + if (anatomicalModelsData["leased"]) { + const leaseModelButton = new qx.ui.form.Button().set({ + label: this.tr("3 seats Leased (27 days left)"), + appearance: "strong-button", + center: true, + enabled: false, + }); + buttonsLayout.add(leaseModelButton, { + flex: 1 + }); + } + const leaseModelButton = new osparc.ui.form.FetchButton().set({ + label: this.tr("Lease model (2 for months)"), + appearance: "strong-button", + center: true, + }); + leaseModelButton.addListener("execute", () => { + leaseModelButton.setFetching(true); + setTimeout(() => { + leaseModelButton.setFetching(false); + this.fireDataEvent("modelLeased", this.getAnatomicalModelsData()["ID"]); + }, 2000); + }); + buttonsLayout.add(leaseModelButton, { + flex: 1 + }); + cardLayout.add(buttonsLayout, { + column: 0, + row: 3, + colSpan: 2, + }); + + return cardLayout; + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js new file mode 100644 index 00000000000..4beac36d736 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js @@ -0,0 +1,188 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.vipMarket.AnatomicalModelListItem", { + extend: qx.ui.core.Widget, + implement : [qx.ui.form.IModel, osparc.filter.IFilterable], + include : [qx.ui.form.MModelProperty, osparc.filter.MFilterable], + + construct: function() { + this.base(arguments); + + const layout = new qx.ui.layout.Grid(5, 5); + layout.setColumnWidth(0, 64); + layout.setRowFlex(0, 1); + layout.setColumnFlex(1, 1); + layout.setColumnAlign(0, "center", "middle"); + layout.setColumnAlign(1, "left", "middle"); + this._setLayout(layout); + + this.set({ + padding: 5, + height: 48, + alignY: "middle", + decorator: "rounded", + }); + + this.addListener("pointerover", this._onPointerOver, this); + this.addListener("pointerout", this._onPointerOut, this); + }, + + events: { + /** (Fired by {@link qx.ui.form.List}) */ + "action" : "qx.event.type.Event" + }, + + properties: { + appearance: { + refine: true, + init: "selectable" + }, + + modelId: { + check: "Number", + init: null, + nullable: false, + event: "changemodelId", + }, + + thumbnail: { + check: "String", + init: null, + nullable: true, + event: "changeThumbnail", + apply: "__applyThumbnail", + }, + + name: { + check: "String", + init: null, + nullable: false, + event: "changeName", + apply: "__applyName", + }, + + date: { + check: "Date", + init: null, + nullable: true, + event: "changeDate", + }, + + leased: { + check: "Boolean", + init: false, + nullable: true, + event: "changeLeased", + apply: "__applyLeased", + }, + }, + + members: { // eslint-disable-line qx-rules/no-refs-in-members + // overridden + _forwardStates: { + focused : true, + hovered : true, + selected : true, + dragover : true + }, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "thumbnail": + control = new qx.ui.basic.Image().set({ + alignY: "middle", + scale: true, + allowGrowX: true, + allowGrowY: true, + allowShrinkX: true, + allowShrinkY: true, + maxWidth: 32, + maxHeight: 32 + }); + this._add(control, { + row: 0, + column: 0, + rowSpan: 2 + }); + break; + case "name": + control = new qx.ui.basic.Label().set({ + font: "text-14", + alignY: "middle", + }); + this._add(control, { + row: 0, + column: 1 + }); + break; + } + + return control || this.base(arguments, id); + }, + + __applyThumbnail: function(value) { + this.getChildControl("thumbnail").setSource(value); + }, + + __applyName: function(value) { + this.getChildControl("name").setValue(value); + }, + + __applyLeased: function(value) { + if (value) { + this.setBackgroundColor("strong-main"); + } + }, + + _onPointerOver: function() { + this.addState("hovered"); + }, + + _onPointerOut : function() { + this.removeState("hovered"); + }, + + _filter: function() { + this.exclude(); + }, + + _unfilter: function() { + this.show(); + }, + + _shouldApplyFilter: function(data) { + if (data.text) { + const checks = [ + this.getName(), + ]; + if (checks.filter(check => check && check.toLowerCase().trim().includes(data.text)).length == 0) { + return true; + } + } + return false; + }, + + _shouldReactToFilter: function(data) { + if (data.text && data.text.length > 1) { + return true; + } + return false; + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/Market.js b/services/static-webserver/client/source/class/osparc/vipMarket/Market.js new file mode 100644 index 00000000000..dd6a2250c44 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/vipMarket/Market.js @@ -0,0 +1,43 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.vipMarket.Market", { + extend: osparc.ui.window.TabbedView, + + construct: function() { + this.base(arguments); + + const miniWallet = osparc.desktop.credits.BillingCenter.createMiniWalletView().set({ + paddingRight: 10 + }); + this.addWidgetOnTopOfTheTabs(miniWallet); + + this.__vipMarketPage = this.__getVipMarketPage(); + }, + + members: { + __vipMarketPage: null, + + __getVipMarketPage: function() { + const title = this.tr("ViP Models"); + const iconSrc = "@FontAwesome5Solid/users/22"; + const vipMarketView = new osparc.vipMarket.VipMarket(); + const page = this.addTab(title, iconSrc, vipMarketView); + return page; + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js b/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js new file mode 100644 index 00000000000..d01207f883f --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js @@ -0,0 +1,54 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.vipMarket.MarketWindow", { + extend: osparc.ui.window.TabbedWindow, + + construct: function() { + this.base(arguments, "store", this.tr("Market")); + + + osparc.utils.Utils.setIdToWidget(this, "storeWindow"); + + const width = 1035; + const height = 700; + this.set({ + width, + height + }) + + const vipMarket = this.__vipMarket = new osparc.vipMarket.Market(); + this._setTabbedView(vipMarket); + }, + + statics: { + openWindow: function() { + const storeWindow = new osparc.vipMarket.MarketWindow(); + storeWindow.center(); + storeWindow.open(); + return storeWindow; + } + }, + + members: { + __vipMarket: null, + + openVipMarket: function() { + return this.__vipMarket.openVipMarket(); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/SortModelsButtons.js b/services/static-webserver/client/source/class/osparc/vipMarket/SortModelsButtons.js new file mode 100644 index 00000000000..da3ed278f2b --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/vipMarket/SortModelsButtons.js @@ -0,0 +1,101 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.vipMarket.SortModelsButtons", { + extend: qx.ui.form.MenuButton, + + construct: function() { + this.base(arguments, this.tr("Sort"), "@FontAwesome5Solid/chevron-down/10"); + + this.set({ + iconPosition: "left", + marginRight: 8 + }); + + const sortByMenu = new qx.ui.menu.Menu().set({ + font: "text-14" + }); + this.setMenu(sortByMenu); + + const nameAsc = new qx.ui.menu.Button().set({ + label: this.tr("Name"), + icon: "@FontAwesome5Solid/sort-alpha-down/14" + }); + nameAsc["sortBy"] = "name"; + nameAsc["orderBy"] = "down"; + const nameDesc = new qx.ui.menu.Button().set({ + label: this.tr("Name"), + icon: "@FontAwesome5Solid/sort-alpha-up/14" + }); + nameDesc["sortBy"] = "name"; + nameDesc["orderBy"] = "up"; + + const dateDesc = new qx.ui.menu.Button().set({ + label: this.tr("Date"), + icon: "@FontAwesome5Solid/arrow-down/14" + }); + dateDesc["sortBy"] = "date"; + dateDesc["orderBy"] = "down"; + const dateAsc = new qx.ui.menu.Button().set({ + label: this.tr("Date"), + icon: "@FontAwesome5Solid/arrow-up/14" + }); + dateAsc["sortBy"] = "date"; + dateAsc["orderBy"] = "down"; + + [ + nameAsc, + nameDesc, + dateDesc, + dateAsc, + ].forEach((btn, idx) => { + sortByMenu.add(btn); + + btn.addListener("execute", () => this.__buttonExecuted(btn)); + + if (idx === 0) { + btn.execute(); + } + }); + }, + + events: { + "sortBy": "qx.event.type.Data" + }, + + statics: { + DefaultSorting: { + "sort": "name", + "order": "down" + } + }, + + members: { + __buttonExecuted: function(btn) { + this.set({ + label: btn.getLabel(), + icon: btn.getIcon() + }); + + const data = { + "sort": btn["sortBy"], + "order": btn["orderBy"] + }; + this.fireDataEvent("sortBy", data); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js b/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js new file mode 100644 index 00000000000..ff0af06af15 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js @@ -0,0 +1,215 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.vipMarket.VipMarket", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(10)); + + this.__buildLayout(); + }, + + statics: { + curateAnatomicalModels: function(anatomicalModelsRaw) { + const anatomicalModels = []; + const models = anatomicalModelsRaw["availableDownloads"]; + models.forEach(model => { + const curatedModel = {}; + Object.keys(model).forEach(key => { + if (key === "Features") { + let featuresRaw = model["Features"]; + featuresRaw = featuresRaw.substring(1, featuresRaw.length-1); // remove brackets + featuresRaw = featuresRaw.split(","); // split the string by commas + const features = {}; + featuresRaw.forEach(pair => { // each pair is "key: value" + const keyValue = pair.split(":"); + features[keyValue[0].trim()] = keyValue[1].trim() + }); + curatedModel["Features"] = features; + } else { + curatedModel[key] = model[key]; + } + if (key === "ID") { + curatedModel["leased"] = [22].includes(model[key]); + } + }); + anatomicalModels.push(curatedModel); + }); + return anatomicalModels; + }, + }, + + members: { + __anatomicalModelsModel: null, + __anatomicalModels: null, + __sortByButton: null, + + __buildLayout: function() { + const toolbarLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)).set({ + alignY: "middle", + }); + this._add(toolbarLayout); + + const sortModelsButtons = this.__sortByButton = new osparc.vipMarket.SortModelsButtons().set({ + alignY: "bottom", + maxHeight: 27, + }); + toolbarLayout.add(sortModelsButtons); + + const filter = new osparc.filter.TextFilter("text", "vipModels").set({ + alignY: "middle", + allowGrowY: false, + minWidth: 170, + }); + this.addListener("appear", () => filter.getChildControl("textfield").focus()); + toolbarLayout.add(filter); + + const modelsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + this._add(modelsLayout, { + flex: 1 + }); + + const modelsUIList = new qx.ui.form.List().set({ + decorator: "no-border", + spacing: 5, + minWidth: 250, + maxWidth: 250 + }); + modelsLayout.add(modelsUIList) + + const anatomicalModelsModel = this.__anatomicalModelsModel = new qx.data.Array(); + const membersCtrl = new qx.data.controller.List(anatomicalModelsModel, modelsUIList, "name"); + membersCtrl.setDelegate({ + createItem: () => new osparc.vipMarket.AnatomicalModelListItem(), + bindItem: (ctrl, item, id) => { + ctrl.bindProperty("id", "modelId", null, item, id); + ctrl.bindProperty("thumbnail", "thumbnail", null, item, id); + ctrl.bindProperty("name", "name", null, item, id); + ctrl.bindProperty("date", "date", null, item, id); + ctrl.bindProperty("leased", "leased", null, item, id); + }, + configureItem: item => { + item.subscribeToFilterGroup("vipModels"); + }, + }); + + const loadingModel = { + id: 0, + thumbnail: "@FontAwesome5Solid/spinner/32", + name: this.tr("Loading"), + }; + this.__anatomicalModelsModel.append(qx.data.marshal.Json.createModel(loadingModel)); + + const anatomicModelDetails = new osparc.vipMarket.AnatomicalModelDetails().set({ + padding: 20, + }); + modelsLayout.add(anatomicModelDetails, { + flex: 1 + }); + + modelsUIList.addListener("changeSelection", e => { + const selection = e.getData(); + if (selection.length) { + const modelId = selection[0].getModelId(); + const modelFound = this.__anatomicalModels.find(anatomicalModel => anatomicalModel["ID"] === modelId); + if (modelFound) { + anatomicModelDetails.setAnatomicalModelsData(modelFound); + return; + } + } + anatomicModelDetails.setAnatomicalModelsData(null); + }, this); + + fetch("https://itis.swiss/PD_DirectDownload/getDownloadableItems/AnatomicalModels", { + method:"POST" + }) + .then(resp => resp.json()) + .then(anatomicalModelsRaw => { + this.__anatomicalModels = this.self().curateAnatomicalModels(anatomicalModelsRaw); + this.__populateModels(); + + anatomicModelDetails.addListener("modelLeased", e => { + const modelId = e.getData(); + const found = this.__anatomicalModels.find(model => model["ID"] === modelId); + if (found) { + found["leased"] = true; + this.__populateModels(); + anatomicModelDetails.setAnatomicalModelsData(found); + }; + }, this); + }) + .catch(err => console.error(err)); + }, + + __populateModels: function() { + const models = []; + this.__anatomicalModels.forEach(model => { + const anatomicalModel = {}; + anatomicalModel["id"] = model["ID"]; + anatomicalModel["thumbnail"] = model["Thumbnail"]; + anatomicalModel["name"] = model["Features"]["name"] + " " + model["Features"]["version"]; + anatomicalModel["date"] = new Date(model["Features"]["date"]); + anatomicalModel["leased"] = model["leased"]; + models.push(anatomicalModel); + }); + + this.__anatomicalModelsModel.removeAll(); + const sortModel = sortBy => { + models.sort((a, b) => { + // first criteria + if (b["leased"] !== a["leased"]) { + // leased first + return b["leased"] - a["leased"]; + } + // second criteria + if (sortBy) { + if (sortBy["sort"] === "name") { + if (sortBy["order"] === "down") { + // A -> Z + return a["name"].localeCompare(b["name"]); + } else { + return b["name"].localeCompare(a["name"]); + } + } else if (sortBy["sort"] === "date") { + if (sortBy["order"] === "down") { + // Now -> Yesterday + return b["date"] - a["date"]; + } else { + return a["date"] - b["date"]; + } + } + } + // default criteria + // A -> Z + return a["name"].localeCompare(b["name"]); + }); + }; + sortModel(); + models.forEach(model => this.__anatomicalModelsModel.append(qx.data.marshal.Json.createModel(model))); + + this.__sortByButton.addListener("sortBy", e => { + this.__anatomicalModelsModel.removeAll(); + const sortBy = e.getData(); + sortModel(sortBy); + models.forEach(model => this.__anatomicalModelsModel.append(qx.data.marshal.Json.createModel(model))); + }, this); + }, + } +});