From ff8bf3d5bd4007318877c836f25c11319041c752 Mon Sep 17 00:00:00 2001 From: Ned Baldessin Date: Wed, 28 Nov 2018 14:33:18 +0100 Subject: [PATCH] Refactor hierarchical menus: support pointer 'aim' for extra user affordance. --- .babelrc | 3 + package-lock.json | 99 +++----- package.json | 3 +- resources/assets/js/Collection/Collection.jsx | 8 +- .../js/Collection/Filters/Materials.jsx | 238 +++++++++++------- .../js/Collection/Filters/Materials.scss | 15 +- tsconfig.json | 6 + 7 files changed, 206 insertions(+), 166 deletions(-) create mode 100644 .babelrc create mode 100644 tsconfig.json diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..fcdb865a --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["transform-decorators-legacy"] +} diff --git a/package-lock.json b/package-lock.json index 1912a983..db17f445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -315,8 +315,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -545,7 +544,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, "requires": { "chalk": "^1.1.3", "esutils": "^2.0.2", @@ -555,14 +553,12 @@ "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -574,8 +570,7 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" } } }, @@ -810,7 +805,6 @@ "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", - "dev": true, "requires": { "babel-runtime": "^6.22.0" } @@ -830,6 +824,11 @@ "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", "dev": true }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=" + }, "babel-plugin-syntax-exponentiation-operator": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", @@ -871,6 +870,16 @@ "babel-runtime": "^6.22.0" } }, + "babel-plugin-transform-decorators-legacy": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.5.tgz", + "integrity": "sha512-jYHwjzRXRelYQ1uGm353zNzf3QmtdCfvJbuYTZ4gKveK7M9H1fs3a5AKdY1JUDl0z97E30ukORW1dzhWvsabtA==", + "requires": { + "babel-plugin-syntax-decorators": "^6.1.18", + "babel-runtime": "^6.2.0", + "babel-template": "^6.3.0" + } + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", @@ -1296,7 +1305,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -1306,7 +1314,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, "requires": { "babel-runtime": "^6.26.0", "babel-traverse": "^6.26.0", @@ -1319,7 +1326,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, "requires": { "babel-code-frame": "^6.26.0", "babel-messages": "^6.23.0", @@ -1336,7 +1342,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -1347,7 +1352,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, "requires": { "babel-runtime": "^6.26.0", "esutils": "^2.0.2", @@ -1358,8 +1362,7 @@ "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, "balanced-match": { "version": "1.0.0", @@ -2341,8 +2344,7 @@ "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", - "dev": true + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" }, "core-util-is": { "version": "1.0.2", @@ -3422,8 +3424,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.11.0", @@ -3482,8 +3483,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "etag": { "version": "1.8.1", @@ -4810,8 +4810,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" }, "globby": { "version": "6.1.0", @@ -4954,7 +4953,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5446,7 +5444,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -5687,6 +5684,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -5771,7 +5769,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isomorphic-fetch": { "version": "2.2.1", @@ -6076,8 +6075,7 @@ "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "lodash._baseassign": { "version": "3.2.0", @@ -6561,8 +6559,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multicast-dns": { "version": "6.2.3", @@ -9689,6 +9686,11 @@ "scheduler": "^0.10.0" } }, + "react-aim": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/react-aim/-/react-aim-0.2.3.tgz", + "integrity": "sha512-llc6sDCRrCIj7XMU18c1+J/3/csmbRb3AywThephS4mk0Y/Uxbl25o4yw/HtYElTogZ9imoNwm3tQEkY802c3A==" + }, "react-breakpoints": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-breakpoints/-/react-breakpoints-3.0.0.tgz", @@ -9736,34 +9738,6 @@ "scheduler": "^0.10.0" } }, - "react-hover-observer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/react-hover-observer/-/react-hover-observer-2.1.1.tgz", - "integrity": "sha512-njZdvhHRAyuvhFRB+uPj/ObRh/tKdRMnasvo0AsYgRFRbEFX6JuInxfCOTRucydtPF3cyUsWTYKXlhBLDDUQ1w==", - "requires": { - "object-assign": "^4.1.1", - "object.omit": "^3.0.0", - "prop-types": "^15.6.0" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "object.omit": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", - "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", - "requires": { - "is-extendable": "^1.0.0" - } - } - } - }, "react-infinite-scroller": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.0.tgz", @@ -9961,8 +9935,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { "version": "0.10.1", @@ -11178,7 +11151,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11310,8 +11282,7 @@ "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" }, "to-object-path": { "version": "0.3.0", diff --git a/package.json b/package.json index 7b084035..2f836507 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "webpack-bundle-analyzer": "^3.0.3" }, "dependencies": { + "babel-plugin-transform-decorators-legacy": "^1.3.5", "breakpoint-sass": "^2.7.1", "bricks.js": "^1.8.0", "deepmerge": "^2.1.1", @@ -31,11 +32,11 @@ "prop-types": "^15.6.2", "qs": "^6.5.2", "react": "^16.6.0", + "react-aim": "^0.2.3", "react-breakpoints": "^3.0.0", "react-compound-slider": "^0.16.2", "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.6.0", - "react-hover-observer": "^2.1.1", "react-infinite-scroller": "^1.2.0", "react-loading-image": "^0.5.0", "react-nl2br": "^0.4.0", diff --git a/resources/assets/js/Collection/Collection.jsx b/resources/assets/js/Collection/Collection.jsx index 4b471b94..78942b3f 100644 --- a/resources/assets/js/Collection/Collection.jsx +++ b/resources/assets/js/Collection/Collection.jsx @@ -131,10 +131,6 @@ class Collection extends Component { ); } - handleFilterChange(filterObj) { - this.setState({ filterObj: filterObj, currentPage: 1 }); - } - handleLoading() { this.setState({ isLoading: true }); } @@ -314,7 +310,9 @@ class Collection extends Component { hits: this.searches[searchUrl].data.hits, hasMore: this.searches[searchUrl].data.hasMore, currentPage: 1, - isLoading: false + isLoading: false, + filterObj: filterObj, + totalHits: this.searches[searchUrl].data.totalHits }), () => { this.historyPushState(); diff --git a/resources/assets/js/Collection/Filters/Materials.jsx b/resources/assets/js/Collection/Filters/Materials.jsx index ed6c893f..c1f320c0 100644 --- a/resources/assets/js/Collection/Filters/Materials.jsx +++ b/resources/assets/js/Collection/Filters/Materials.jsx @@ -1,6 +1,5 @@ import React, { Component } from "react"; -import { CSSTransitionGroup } from "react-transition-group"; -import ReactHoverObserver from "react-hover-observer"; +import { source, target } from "react-aim"; import difference from "lodash/difference"; const MaterialNullObject = { @@ -8,37 +7,147 @@ const MaterialNullObject = { children: [] }; +@target() +class SecondColMenu extends Component { + constructor(props) { + super(props); + this.state = {}; + this.renderSecondColumnItem = this.renderSecondColumnItem.bind(this); + } + renderSecondColumnItem(mat, parentIsSelected, i) { + return ( +
  • + +
  • + ); + } + render() { + const parentIsSelected = + this.props.selectedIds.indexOf(this.props.parentMat.id) >= 0; + return ( + + ); + } +} + +@source({ + mouseEnter: (props, component) => props.onActiveSecondCol(props.mat) +}) +class FirstColMenuItem extends Component { + constructor(props) { + super(props); + } + render() { + const mat = this.props.mat; + let classes = "Materials__lvl1-button"; + classes += this.props.selected ? " is-selected" : ""; + classes += mat.children.length > 0 ? " has-children" : ""; + classes += mat.id === this.props.expandedMaterial.id ? " is-hovered" : ""; + + let secondCol = + mat.children.length > 0 && this.props.expandedMaterial.id === mat.id ? ( + + ) : null; + return ( +
  • + + + {secondCol} +
  • + ); + } +} + class Materials extends Component { constructor(props) { super(props); this.state = { expandedMaterial: MaterialNullObject }; - this.renderFirstColumnItem = this.renderFirstColumnItem.bind(this); - this.renderSecondColumnItem = this.renderSecondColumnItem.bind(this); this.handleFirstColumnClick = this.handleFirstColumnClick.bind(this); - this.handleHoverChange = this.handleHoverChange.bind(this); this.handleAddAllClick = this.handleAddAllClick.bind(this); - } - - handleHoverChange(mat, { isHovering }) { - if (isHovering && mat.children.length > 0) { - this.setState({ expandedMaterial: mat }); - } else if (isHovering && mat.children.length === 0) { - this.setState({ expandedMaterial: MaterialNullObject }); - } + this.handleActiveSecondCol = this.handleActiveSecondCol.bind(this); + this.handleSecondColumnClick = this.handleSecondColumnClick.bind(this); } handleFirstColumnClick(mat, ev) { ev.stopPropagation(); + + if (this.props.selectedIds.indexOf(mat.id) >= 0) { + // We already are filtering with this material. Noop. + return; + } + if (mat.children.length > 0) { - // Expand the second column panel. - // this.setState({ expandedMaterial: mat }); + const childrenIds = mat.children.map(c => c.id); + const materialsToRemove = this.props.selectedIds.filter( + id => childrenIds.indexOf(id) >= 0 + ); + + if (materialsToRemove.length > 0) { + const removeObj = { + type: "material", + ids: materialsToRemove, + paramName: "material_ids" + }; + this.props.onFilterChange({ material_ids: [mat.id] }, removeObj); + } else { + this.props.onFilterAdd({ material_ids: [mat.id] }); + } } else { - // this.setState({ expandedMaterial: MaterialNullObject }); this.props.onFilterAdd({ material_ids: [mat.id] }); } } + handleSecondColumnClick(mat, ev) { ev.stopPropagation(); @@ -73,6 +182,7 @@ class Materials extends Component { ids: otherSiblings, paramName: "material_ids" }; + this.props.onFilterChange( // Add this id. { material_ids: [this.state.expandedMaterial.id] }, // Remove these ids. @@ -86,6 +196,7 @@ class Materials extends Component { } } } + handleAddAllClick(group, ev) { ev.stopPropagation(); const filtersToDelete = { @@ -102,90 +213,29 @@ class Materials extends Component { ); } - renderSecondColumnItem(mat, parentIsSelected, i) { - return ( -
  • - -
  • - ); - } - - renderFirstColumnItem(mat, i) { - let classes = "Materials__lvl1-button"; - classes += - this.props.selectedIds.indexOf(mat.id) >= 0 ? " is-selected" : ""; - classes += mat.children.length > 0 ? " has-children" : ""; - classes += mat.id === this.state.expandedMaterial.id ? " is-hovered" : ""; - return ( -
  • - - - - - {mat.children.length > 0 && - mat.id === this.state.expandedMaterial.id ? ( -
      -
    • - -
    • - {mat.children.map((m, i) => - this.renderSecondColumnItem( - m, - this.props.selectedIds.indexOf(mat.id) >= 0, - i - ) - )} -
    - ) : null} -
    -
    -
  • - ); + handleActiveSecondCol(mat) { + this.setState({ expandedMaterial: mat }); } render() { return (
      - {this.props.materials.map(this.renderFirstColumnItem)} + {this.props.materials.map((mat, i) => { + return ( + = 0} + key={i} + onActiveSecondCol={this.handleActiveSecondCol} + expandedMaterial={this.state.expandedMaterial} + selectedIds={this.props.selectedIds} + onSecondColumnClick={this.handleSecondColumnClick} + onAddAllClick={this.handleAddAllClick} + onFirstColumnClick={this.handleFirstColumnClick} + /> + ); + })}
    ); diff --git a/resources/assets/js/Collection/Filters/Materials.scss b/resources/assets/js/Collection/Filters/Materials.scss index aa747448..bc4c12d2 100644 --- a/resources/assets/js/Collection/Filters/Materials.scss +++ b/resources/assets/js/Collection/Filters/Materials.scss @@ -42,14 +42,25 @@ } .Materials__lvl1-item, .Materials__lvl2-item { - margin-bottom: 5px; + // margin-bottom: 5px; +} +.Materials__lvl1-chevron { + display: none; + stroke: inherit; + float: right; + position: relative; + top: 6px; + right: 10px; + .has-children & { + display: block; + } } .Materials__lvl1-button, .Materials__lvl2-button { display: block; width: 100%; - padding: 10px 10px 10px 60px; + padding: 10px 10px 15px 60px; box-sizing: border-box; position: relative; margin: 0; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..8f58bcd7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "allowJs": true + } +}