search-value-selector as shared component

Also making sure that every BaseModel implements onString to be
displayable by a Selector.

And adding the new search-value-selector in the motion-detail-view
This commit is contained in:
Jochen Saalfeld 2018-09-13 07:57:38 +02:00
parent 34412c7d9e
commit 535e0b2ba3
No known key found for this signature in database
GPG Key ID: 8ACD4E8264B67DF4
37 changed files with 575 additions and 164 deletions

221
client/package-lock.json generated
View File

@ -513,12 +513,12 @@
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
"integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
"dev": true,
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"globals": {
@ -526,6 +526,12 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
"integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
"dev": true
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
}
}
},
@ -1373,9 +1379,9 @@
"dev": true
},
"agent-base": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
"integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
"dev": true,
"requires": {
"es6-promisify": "^5.0.0"
@ -2062,7 +2068,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@ -3303,6 +3309,43 @@
}
}
},
"del": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
"dev": true,
"requires": {
"globby": "^5.0.0",
"is-path-cwd": "^1.0.0",
"is-path-in-cwd": "^1.0.0",
"object-assign": "^4.0.1",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0",
"rimraf": "^2.2.8"
},
"dependencies": {
"globby": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
"dev": true,
"requires": {
"array-union": "^1.0.1",
"arrify": "^1.0.0",
"glob": "^7.0.3",
"object-assign": "^4.0.1",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0"
}
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -3766,9 +3809,9 @@
}
},
"es6-promise": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
"integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz",
"integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==",
"dev": true
},
"es6-promisify": {
@ -4631,15 +4674,13 @@
"version": "1.0.0",
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4656,22 +4697,19 @@
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -4802,8 +4840,7 @@
"version": "2.0.3",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -4817,7 +4854,6 @@
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4834,7 +4870,6 @@
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -4843,15 +4878,13 @@
"version": "0.0.8",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"resolved": false,
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -4872,7 +4905,6 @@
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -4961,8 +4993,7 @@
"version": "1.0.1",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -4976,7 +5007,6 @@
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -5114,7 +5144,6 @@
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -5836,13 +5865,19 @@
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
"integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
"dev": true,
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
}
}
},
@ -6632,12 +6667,12 @@
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
"integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
"dev": true,
"requires": {
"ms": "2.0.0"
"ms": "^2.1.1"
}
},
"istanbul-lib-coverage": {
@ -6646,6 +6681,12 @@
"integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==",
"dev": true
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -6857,7 +6898,7 @@
},
"es6-promise": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
"resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
"integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
"dev": true
},
@ -6869,7 +6910,7 @@
},
"readable-stream": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
"integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
"dev": true,
"requires": {
@ -6949,9 +6990,9 @@
}
},
"karma-coverage-istanbul-reporter": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.3.tgz",
"integrity": "sha512-UVs9IDulfwkBxjEnUzfR/nIc3oBneOPuorpLVBvEMtz2hy0wnVLhCMxpkqAtuQWqvOZRQlGqs+dDtMUeRydTQA==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.4.tgz",
"integrity": "sha512-xJS7QSQIVU6VK9HuJ/ieE5yynxKhjCCkd96NLY/BX/HXsx0CskU9JJiMQbd4cHALiddMwI4OWh1IIzeWrsavJw==",
"dev": true,
"requires": {
"istanbul-api": "^2.0.5",
@ -7868,6 +7909,14 @@
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
"dev": true
},
"ngx-mat-select-search": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-1.3.1.tgz",
"integrity": "sha512-ZggcqsfcJZ1tcy6ZgNnifrvn/ahWJqRO2B7QXHt0FDUo4lrv/2cuHALzwuwlY/z8rZeH+ZEmaAevzQtYH8mKpA==",
"requires": {
"tslib": "^1.9.0"
}
},
"nice-try": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz",
@ -9173,9 +9222,9 @@
"dev": true
},
"protractor": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.0.tgz",
"integrity": "sha512-6TSYqMhUUzxr4/wN0ttSISqPMKvcVRXF4k8jOEpGWD8OioLak4KLgfzHK9FJ49IrjzRrZ+Mx1q2Op8Rk0zEcnQ==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.1.tgz",
"integrity": "sha512-ORey5ewQMYiXQxcQohsqEiKYOg/r5yJoJbt0tuROmmgajdg/CA3gTOZNIFJncUVMAJIk5YFqBBLUjKVmQO6tfA==",
"dev": true,
"requires": {
"@types/node": "^6.0.46",
@ -9192,14 +9241,14 @@
"saucelabs": "^1.5.0",
"selenium-webdriver": "3.6.0",
"source-map-support": "~0.4.0",
"webdriver-js-extender": "2.0.0",
"webdriver-js-extender": "2.1.0",
"webdriver-manager": "^12.0.6"
},
"dependencies": {
"@types/node": {
"version": "6.0.116",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.116.tgz",
"integrity": "sha512-vToa8YEeulfyYg1gSOeHjvvIRqrokng62VMSj2hoZrwZNcYrp2h3AWo6KeBVuymIklQUaY5zgVJvVsC4KiiLkQ==",
"version": "6.0.117",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.117.tgz",
"integrity": "sha512-sihk0SnN8PpiS5ihu5xJQ5ddnURNq4P+XPmW+nORlKkHy21CoZO/IVHK/Wq/l3G8fFW06Fkltgnqx229uPlnRg==",
"dev": true
},
"ansi-styles": {
@ -9210,7 +9259,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@ -9221,47 +9270,12 @@
"supports-color": "^2.0.0"
}
},
"del": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
"dev": true,
"requires": {
"globby": "^5.0.0",
"is-path-cwd": "^1.0.0",
"is-path-in-cwd": "^1.0.0",
"object-assign": "^4.0.1",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0",
"rimraf": "^2.2.8"
}
},
"globby": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
"integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
"dev": true,
"requires": {
"array-union": "^1.0.1",
"arrify": "^1.0.0",
"glob": "^7.0.3",
"object-assign": "^4.0.1",
"pify": "^2.0.0",
"pinkie-promise": "^2.0.0"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
"source-map-support": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
@ -11914,36 +11928,13 @@
}
},
"webdriver-js-extender": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.0.0.tgz",
"integrity": "sha512-fbyKiVu3azzIc5d4+26YfuPQcFTlgFQV5yQ/0OQj4Ybkl4g1YQuIPskf5v5wqwRJhHJnPHthB6tqCjWHOKLWag==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz",
"integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==",
"dev": true,
"requires": {
"@types/selenium-webdriver": "^3.0.0",
"selenium-webdriver": "^3.0.1"
},
"dependencies": {
"selenium-webdriver": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
"integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==",
"dev": true,
"requires": {
"jszip": "^3.1.3",
"rimraf": "^2.5.4",
"tmp": "0.0.30",
"xml2js": "^0.4.17"
}
},
"tmp": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
"integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.1"
}
}
}
},
"webpack": {

View File

@ -33,6 +33,7 @@
"@ngx-translate/core": "^10.0.2",
"@ngx-translate/http-loader": "^3.0.1",
"core-js": "^2.5.4",
"ngx-mat-select-search": "^1.3.1",
"rxjs": "^6.3.2",
"uuid": "^3.3.2",
"zone.js": "^0.8.26"
@ -53,13 +54,13 @@
"jasmine-spec-reporter": "~4.2.1",
"karma": "^3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.3",
"karma-coverage-istanbul-reporter": "^2.0.4",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2",
"npm-run-all": "^4.1.3",
"prettier": "^1.14.2",
"pretty-quick": "^1.6.0",
"protractor": "^5.4.0",
"protractor": "^5.4.1",
"ts-node": "~5.0.1",
"tslint": "~5.9.1",
"typescript": "~2.7.2"

View File

@ -0,0 +1,18 @@
<mat-form-field [formGroup]="form">
<mat-select [formControl]="formControl" placeholder="{{listname}}" multiple="{{multiple}}" #thisSelector>
<ngx-mat-select-search [formControl]="filterControl"></ngx-mat-select-search>
<mat-option *ngFor="let selectedItem of filteredItems | async" [value]="selectedItem">
{{selectedItem.toString()}}
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="dispSelected">
<p>
<span translate>Selected Values</span>:
</p>
<mat-chip-list #chipList>
<mat-chip *ngFor="let selectedItem of thisSelector?.value" (removed)="remove(selectedItem)">{{selectedItem.name}}
<fa-icon matSuffix icon="times" (click)="remove(selectedItem)"></fa-icon>
</mat-chip>
</mat-chip-list>
</div>

View File

@ -0,0 +1,3 @@
fa-icon {
padding-left: 5px;
}

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchValueSelectorComponent } from './search-value-selector.component';
describe('SearchValueSelectorComponent', () => {
let component: SearchValueSelectorComponent;
let fixture: ComponentFixture<SearchValueSelectorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SearchValueSelectorComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchValueSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,179 @@
import { Component, OnInit, Input, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ReplaySubject, Subject } from 'rxjs';
import { MatSelect } from '@angular/material';
import { SelectorItem } from './search-value-selector.interfaces';
import { takeUntil } from 'rxjs/operators';
/**
* Reusable Searchable Value Selector
*
* Use `multiple="true"`, `[InputListValues]=myValues`,`[formControl]="myformcontrol"`, `[form]="myform_name"` and `placeholder={{listname}}` to pass the Values and Listname
*
* ## Examples:
*
* ### Usage of the selector:
*
* ngDefaultControl: https://stackoverflow.com/a/39053470
*
* ```html
* <os-search-value-selector
* ngDefaultControl
* multiple="true"
* placeholder="Placeholder"
* [InputListValues]="myListValues",
* [form]="myform_name",
* [formControl]="myformcontrol">
* </os-search-value-selector>
* ```
*
* ### Declaration of a Selector provided as `[InputListValues]=myListValues`:
*
* Every Class that enherits of BaseModel implements the SelectorItem Interface and can
* therefore be used directly in the Selector Component.
*
* ```ts
* import { SelectorItem } from '../../shared/components/search-value-selector/search-value-selector.interfaces';
*
* const myListValues: SelectorItem[];
* myListValues = this.DS.get(User);
* ```
*/
@Component({
selector: 'os-search-value-selector',
templateUrl: './search-value-selector.component.html',
styleUrls: ['./search-value-selector.component.scss']
})
export class SearchValueSelectorComponent implements OnInit {
/**
* ngModel variable - Depricated with Angular 7
* DO NOT USE: READ AT remove() FUNCTION!
*/
public myModel = [];
/**
* Control for the filtering of the list
*/
public filterControl = new FormControl();
/**
* List of the filtered content, when entering somithing in the search bar
*/
public filteredItems: ReplaySubject<SelectorItem[]> = new ReplaySubject<SelectorItem[]>(1);
/**
* Decide if this should be a single or multi-select-field
*/
@Input()
public multiple: boolean;
/**
* The Input List Values
*/
@Input()
public InputListValues: SelectorItem[];
/**
* Placeholder of the List
*/
@Input()
public listname: String;
/**
* Form Group
*/
@Input()
public form: FormGroup;
/**
* Name of the Form
*/
@Input()
public formControl: FormControl;
/**
* DO NOT USE UNTIL BUG IN UPSTREAM ARE RESOLVED!
* READ AT FUNCTION remove()
*
* Displayes the selected Items as Chip-List
*/
// @Input()
public dispSelected = false;
/**
* The MultiSelect Component
*/
@ViewChild('thisSelector')
public thisSelector: MatSelect;
/**
* Subject that emits when the component has been destroyed
*/
private _onDestroy = new Subject<void>();
/**
* Empty constructor
*/
public constructor() {}
/**
* onInit with filter ans subscription on filter
*/
public ngOnInit(): void {
// load the initial item list
this.filteredItems.next(this.InputListValues.slice());
// listen to value changes
this.filterControl.valueChanges.pipe(takeUntil(this._onDestroy)).subscribe(() => {
this.filterItems();
});
// this.multiSelect.stateChanges.subscribe(fn => console.log('ive changed'));
}
/**
* the filter function itself
*/
private filterItems(): void {
if (!this.InputListValues) {
return;
}
// get the search keyword
let search = this.filterControl.value;
if (!search) {
this.filteredItems.next(this.InputListValues.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the values
this.filteredItems.next(
this.InputListValues.filter(
selectedItem =>
selectedItem
.toString()
.toLowerCase()
.indexOf(search) > -1
)
);
}
/**
* If the dispSelected value is marked as true, a chipList should be shown below the
* selection list. Unfortunately it is not possible (yet) to change the datamodel in the backend
* https://github.com/angular/material2/issues/10085 - therefore you can display the values in two
* places, but can't reflect the changes in both places. Until this can be done this will be unused code
* @param item the selected item to be removed
*/
public remove(item: SelectorItem): void {
const myArr = this.thisSelector.value;
const index = myArr.indexOf(item, 0);
// my model was the form according to fix
// https://github.com/angular/material2/issues/10044
// but this causes bad behaviour and will be depricated in Angular 7
this.myModel = this.myModel.slice(index, 1);
if (index > -1) {
myArr.splice(index, 1);
}
this.thisSelector.value = myArr;
}
}

View File

@ -0,0 +1,10 @@
/**
* Inteface for the Multi-Value-Selector Component to display and use
* the given values.
*/
export interface SelectorItem {
/**
* translates the displayable part of the function to a String
*/
toString(): string;
}

View File

@ -53,6 +53,10 @@ export class Item extends BaseModel {
});
}
}
public toString(): string {
return this.title;
}
}
BaseModel.registerCollectionElement('agenda/item', Item);

View File

@ -53,6 +53,10 @@ export class Assignment extends BaseModel {
});
}
}
public toString(): string {
return this.title;
}
}
BaseModel.registerCollectionElement('assignments/assignment', Assignment);

View File

@ -1,6 +1,7 @@
import { OpenSlidesComponent } from 'app/openslides.component';
import { Deserializable } from './deserializable.model';
import { CollectionStringModelMapperService } from '../../core/services/collectionStringModelMapper.service';
import { SelectorItem } from '../components/search-value-selector/search-value-selector.interfaces';
export interface ModelConstructor<T extends BaseModel> {
new (...args: any[]): T;
@ -9,7 +10,7 @@ export interface ModelConstructor<T extends BaseModel> {
/**
* Abstract parent class to set rules and functions for all models.
*/
export abstract class BaseModel extends OpenSlidesComponent implements Deserializable {
export abstract class BaseModel extends OpenSlidesComponent implements Deserializable, SelectorItem {
/**
* Register the collection string to the type.
* @param collectionString
@ -55,6 +56,10 @@ export abstract class BaseModel extends OpenSlidesComponent implements Deseriali
}
});
}
/**
* force children to have a toString() method
*/
public abstract toString(): string;
/**
* returns the collectionString.

View File

@ -18,6 +18,10 @@ export class ChatMessage extends BaseModel {
public getUser(): User {
return this.DS.get<User>('users/user', this.user_id);
}
public toString(): string {
return this.message;
}
}
BaseModel.registerCollectionElement('core/chat-message', ChatMessage);

View File

@ -12,6 +12,10 @@ export class Config extends BaseModel {
public constructor(input?: any) {
super('core/config', input);
}
public toString(): string {
return this.key;
}
}
BaseModel.registerCollectionElement('core/config', Config);

View File

@ -14,6 +14,10 @@ export class Countdown extends BaseModel {
public constructor(input?: any) {
super('core/countdown');
}
public toString(): string {
return this.description;
}
}
BaseModel.registerCollectionElement('core/countdown', Countdown);

View File

@ -11,6 +11,10 @@ export class ProjectorMessage extends BaseModel {
public constructor(input?: any) {
super('core/projector-message', input);
}
public toString(): string {
return this.message;
}
}
BaseModel.registerCollectionElement('core/projector-message', ProjectorMessage);

View File

@ -18,6 +18,10 @@ export class Projector extends BaseModel {
public constructor(input?: any) {
super('core/projector', input);
}
public toString(): string {
return this.name;
}
}
BaseModel.registerCollectionElement('core/projector', Projector);

View File

@ -11,6 +11,10 @@ export class Tag extends BaseModel {
public constructor(input?: any) {
super('core/tag', input);
}
public toString(): string {
return this.name;
}
}
BaseModel.registerCollectionElement('core/tag', Tag);

View File

@ -28,6 +28,10 @@ export class Mediafile extends BaseModel {
public getUploader(): User {
return this.DS.get<User>('users/user', this.uploader_id);
}
public toString(): string {
return this.title;
}
}
BaseModel.registerCollectionElement('amediafiles/mediafile', Mediafile);

View File

@ -13,9 +13,9 @@ export class Category extends BaseModel {
super('motions/category', input);
}
public toString = (): string => {
public toString(): string {
return this.prefix + ' - ' + this.name;
};
}
}
BaseModel.registerCollectionElement('motions/category', Category);

View File

@ -17,6 +17,10 @@ export class MotionBlock extends BaseModel {
public getAgenda(): BaseModel | BaseModel[] {
return this.DS.get<Item>('agenda/item', this.agenda_item_id);
}
public toString(): string {
return this.title;
}
}
BaseModel.registerCollectionElement('motions/motion-block', MotionBlock);

View File

@ -18,6 +18,10 @@ export class MotionChangeReco extends BaseModel {
public constructor(input?: any) {
super('motions/motion-change-recommendation', input);
}
public toString(): string {
return this.text;
}
}
BaseModel.registerCollectionElement('motions/motion-change-recommendation', MotionChangeReco);

View File

@ -13,6 +13,10 @@ export class MotionCommentSection extends BaseModel {
public constructor(input?: any) {
super('motions/motion-comment-section', input);
}
public toString(): string {
return this.name;
}
}
BaseModel.registerCollectionElement('motions/motion-comment-section', MotionCommentSection);

View File

@ -1,4 +1,5 @@
import { Deserializer } from '../deserializer.model';
import { User } from '../users/user';
/**
* Representation of a Motion Submitter.
@ -11,7 +12,16 @@ export class MotionSubmitter extends Deserializer {
public motion_id: number;
public weight: number;
public constructor(input?: any) {
super(input);
public constructor(input?: any, motion_id?: number, weight?: number) {
super();
this.id = input.id;
if (input instanceof User) {
const user_obj = input as User;
this.user_id = user_obj.id;
this.motion_id = motion_id;
this.weight = weight;
} else {
this.deserialize(input);
}
}
}

View File

@ -25,6 +25,7 @@ export class Motion extends BaseModel {
public motion_block_id: number;
public origin: string;
public submitters: MotionSubmitter[];
public submitters_id: number[];
public supporters_id: number[];
public comments: MotionComment[];
public workflow_id: number;
@ -61,15 +62,16 @@ export class Motion extends BaseModel {
.map((submitter: MotionSubmitter) => submitter.user_id);
}
/**
* returns the Motion name
*/
public toString(): string {
return this.title;
}
public deserialize(input: any): void {
Object.assign(this, input);
if (input.submitters instanceof Array) {
input.submitters.forEach(SubmitterData => {
this.submitters.push(new MotionSubmitter(SubmitterData));
});
}
this.log_messages = [];
if (input.log_messages instanceof Array) {
input.log_messages.forEach(logData => {

View File

@ -53,6 +53,10 @@ export class Workflow extends BaseModel {
});
}
}
public toString(): string {
return this.name;
}
}
BaseModel.registerCollectionElement('motions/workflow', Workflow);

View File

@ -24,6 +24,10 @@ export class Topic extends BaseModel {
public getAgenda(): Item {
return this.DS.get<Item>('agenda/item', this.agenda_item_id);
}
public toString(): string {
return this.title;
}
}
BaseModel.registerCollectionElement('topics/topic', Topic);

View File

@ -20,6 +20,10 @@ export class Group extends BaseModel {
return user.groups_id.includes(this.id);
});
}
public toString(): string {
return this.name;
}
}
BaseModel.registerCollectionElement('users/group', Group);

View File

@ -17,6 +17,10 @@ export class PersonalNote extends BaseModel {
public getUser(): User {
return this.DS.get<User>('users/user', this.user_id);
}
public toString(): string {
return this.notes.toString();
}
}
BaseModel.registerCollectionElement('users/personal-note', PersonalNote);

View File

@ -82,9 +82,9 @@ export class User extends BaseModel {
return shortName.trim();
}
public toString = (): string => {
public toString(): string {
return this.short_name;
};
}
}
BaseModel.registerCollectionElement('users/user', User);

View File

@ -18,6 +18,9 @@ import {
MatSortModule,
MatTabsModule
} from '@angular/material';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material';
import { NgxMatSelectSearchModule } from 'ngx-mat-select-search';
import { MatDialogModule } from '@angular/material/dialog';
import { MatListModule } from '@angular/material/list';
import { MatExpansionModule } from '@angular/material/expansion';
@ -40,6 +43,7 @@ import { HeadBarComponent } from './components/head-bar/head-bar.component';
import { FooterComponent } from './components/footer/footer.component';
import { LegalNoticeContentComponent } from './components/legal-notice-content/legal-notice-content.component';
import { PrivacyPolicyContentComponent } from './components/privacy-policy-content/privacy-policy-content.component';
import { SearchValueSelectorComponent } from './components/search-value-selector/search-value-selector.component';
library.add(fas);
@ -59,6 +63,7 @@ library.add(fas);
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatButtonModule,
MatCheckboxModule,
MatToolbarModule,
@ -76,10 +81,13 @@ library.add(fas);
MatSnackBarModule,
FontAwesomeModule,
TranslateModule.forChild(),
RouterModule
RouterModule,
MatChipsModule,
NgxMatSelectSearchModule
],
exports: [
FormsModule,
MatAutocompleteModule,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule,
@ -99,12 +107,14 @@ library.add(fas);
MatDialogModule,
MatSnackBarModule,
MatTabsModule,
NgxMatSelectSearchModule,
FontAwesomeModule,
TranslateModule,
PermsDirective,
DomChangeDirective,
FooterComponent,
HeadBarComponent,
SearchValueSelectorComponent,
LegalNoticeContentComponent,
PrivacyPolicyContentComponent
],
@ -114,7 +124,8 @@ library.add(fas);
HeadBarComponent,
FooterComponent,
LegalNoticeContentComponent,
PrivacyPolicyContentComponent
PrivacyPolicyContentComponent,
SearchValueSelectorComponent
]
})
export class SharedModule {}

View File

@ -68,6 +68,28 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
});
}
/**
* Saves the update to an existing model. So called "update"-function
* @param update the update that should be created
* @param viewModel the view model that the update is based on
*/
public abstract save(update: M, viewModel: V): Observable<M>;
/**
* Deletes a given Model
* @param update the update that should be created
* @param viewModel the view model that the update is based on
*/
public abstract delete(viewModel: V): Observable<M>;
/**
* Creates a new model
* @param update the update that should be created
* @param viewModel the view model that the update is based on
* TODO: remove the viewModel
*/
public abstract create(update: M, viewModel: V): Observable<M>;
protected abstract createViewModel(model: M): V;
/**

View File

@ -146,14 +146,33 @@
<!-- Submitter -->
<div *ngIf="motion && motion.submitters || editMotion">
<h3 translate>Submitters</h3>
{{motion.submitters}}
<div *ngIf="editMotion && newMotion">
<div *ngIf="motion && editMotion">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="this.metaInfoForm.get('submitters')" multiple="true" listname="Submitter" [InputListValues]="getAllUsers()"></os-search-value-selector>
</div>
</div>
<div *ngIf="!editMotion || !newMotion">
<h3 translate>Submitters</h3>
<ul *ngFor="let submitters of motion.submitters">
<li>{{submitters}}</li>
</ul>
</div>
</div>
<!-- Supporter -->
<div *ngIf='motion && motion.hasSupporters() || editMotion'>
<h3 translate>Supporters</h3>
<!-- print all motion supporters -->
<div *ngIf="editMotion">
<div *ngIf="motion && editMotion">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="this.metaInfoForm.get('supporters_id')" multiple="true" listname="Supporter" [InputListValues]="getAllUsers()"></os-search-value-selector>
</div>
</div>
<div *ngIf="!editMotion && motion.hasSupporters()">
<h3 translate>Supporters</h3>
<ul *ngFor="let supporters of motion.supporters">
<li>{{supporters}}</li>
</ul>
</div>
</div>
<!-- State -->
@ -202,13 +221,9 @@
<h3 translate> Category</h3>
{{motion.category}}
</div>
<mat-form-field *ngIf="motion && editMotion">
<mat-select placeholder='Category' formControlName='category_id'>
<mat-option>None</mat-option>
<mat-divider></mat-divider>
<mat-option *ngFor="let cat of getMotionCategories()" [value]="cat.id">{{cat}}</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="editMotion">
<os-search-value-selector ngDefaultControl [form]="metaInfoForm" [formControl]="this.metaInfoForm.get('category_id')" multiple="false" listname="Category" [InputListValues]="getMotionCategories()"></os-search-value-selector>
</div>
</div>
<!-- Origin -->

View File

@ -8,6 +8,7 @@ import { Category } from '../../../../shared/models/motions/category';
import { ViewportService } from '../../../../core/services/viewport.service';
import { MotionRepositoryService } from '../../services/motion-repository.service';
import { ViewMotion } from '../../models/view-motion';
import { User } from '../../../../shared/models/users/user';
/**
* Component for the motion detail view
@ -22,13 +23,15 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
* MatExpansionPanel for the meta info
* Only relevant in mobile view
*/
@ViewChild('metaInfoPanel') public metaInfoPanel: MatExpansionPanel;
@ViewChild('metaInfoPanel')
public metaInfoPanel: MatExpansionPanel;
/**
* MatExpansionPanel for the content panel
* Only relevant in mobile view
*/
@ViewChild('contentPanel') public contentPanel: MatExpansionPanel;
@ViewChild('contentPanel')
public contentPanel: MatExpansionPanel;
/**
* Motions meta-info
@ -103,6 +106,8 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
public patchForm(formMotion: ViewMotion): void {
this.metaInfoForm.patchValue({
category_id: formMotion.categoryId,
supporters_id: formMotion.supporters,
submitters: formMotion.submitters,
state_id: formMotion.stateId,
recommendation_id: formMotion.recommendationId,
identifier: formMotion.identifier,
@ -126,6 +131,8 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
category_id: [''],
state_id: [''],
recommendation_id: [''],
submitters: [''],
supporters_id: [''],
origin: ['']
});
this.contentForm = this.formBuilder.group({
@ -149,11 +156,11 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
public saveMotion(): void {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
if (this.newMotion) {
this.repo.saveMotion(newMotionValues).subscribe(response => {
this.repo.create(newMotionValues).subscribe(response => {
this.router.navigate(['./motions/' + response.id]);
});
} else {
this.repo.saveMotion(newMotionValues, this.motionCopy).subscribe();
this.repo.save(newMotionValues, this.motionCopy).subscribe();
}
}
@ -201,11 +208,18 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
* TODO: Repo should handle
*/
public deleteMotionButton(): void {
this.repo.deleteMotion(this.motion).subscribe(answer => {
this.repo.delete(this.motion).subscribe(answer => {
this.router.navigate(['./motions/']);
});
}
/**
* returns all Possible supporters
*/
public getAllUsers(): User[] {
return this.DS.getAll(User);
}
/**
* Init. Does nothing here.
*/

View File

@ -56,7 +56,20 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
}
/**
* Creates and updates a motion
* Creates a motion
* Creates a (real) motion with patched data and delegate it
* to the {@link DataSendService}
*
* @param update the form data containing the update values
* @param viewMotion The View Motion. If not present, a new motion will be created
* TODO: Remove the viewMotion and make it actually distignuishable from save()
*/
public create(update: any, viewMotion?: ViewMotion): Observable<any> {
return this.save(update, viewMotion);
}
/**
* updates a motion
*
* Creates a (real) motion with patched data and delegate it
* to the {@link DataSendService}
@ -64,9 +77,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
* @param update the form data containing the update values
* @param viewMotion The View Motion. If not present, a new motion will be created
*/
public saveMotion(update: any, viewMotion?: ViewMotion): Observable<any> {
public save(update: any, viewMotion?: ViewMotion): Observable<any> {
let updateMotion: Motion;
if (viewMotion) {
// implies that an existing motion was updated
updateMotion = viewMotion.motion;
@ -74,6 +86,28 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
// implies that a new motion was created
updateMotion = new Motion();
}
// submitters: User[] -> submitter: MotionSubmitter[]
const submitters = update.submitters as User[];
// The server doesn't really accept MotionSubmitter arrays on create.
// We simply need to send an number[] on create.
// MotionSubmitter[] should be send on update
update.submitters = undefined;
const submitterIds: number[] = [];
if (submitters.length > 0) {
submitters.forEach(submitter => {
submitterIds.push(submitter.id);
});
}
update.submitters_id = submitterIds;
// supporters[]: User -> supporters_id: number[];
const supporters = update.supporters_id as User[];
const supporterIds: number[] = [];
if (supporters.length > 0) {
supporters.forEach(supporter => {
supporterIds.push(supporter.id);
});
}
update.supporters_id = supporterIds;
updateMotion.patchValues(update);
return this.dataSend.saveModel(updateMotion);
}
@ -85,7 +119,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
* to {@link DataSendService}
* @param viewMotion
*/
public deleteMotion(viewMotion: ViewMotion): Observable<any> {
public delete(viewMotion: ViewMotion): Observable<any> {
return this.dataSend.delete(viewMotion.motion);
}
}

View File

@ -42,7 +42,6 @@ export class StartComponent extends BaseComponent implements OnInit {
// tslint:disable-next-line
const welcomeTitleTranslateDummy = this.translate.instant('Welcome to OpenSlides');
super.setTitle('Home');
// set welcome title and text
const welcomeTitleConfig = this.DS.filter<Config>(
Config,

View File

@ -4,11 +4,8 @@
"Category": "",
"Change Password": "Passwort ändern",
"Content": "",
"Cookies": "",
"Copyright by": "Copyright by",
"Database": "",
"DeleteMotion": "",
"Deleting Files": "",
"Edit Profile": "Profil bearbeiten",
"English": "Englisch",
"Export As": {
@ -24,8 +21,9 @@
"German": "Deutsch",
"Home": "Startseite",
"Identifier": "",
"Installed plugins": "",
"Legal Notice": "Impressum",
"Logfiles": "",
"License": "",
"Login": "",
"Login as Guest": "",
"Logout": "Abmelden",
@ -38,6 +36,7 @@
},
"Origin": "",
"Participants": "Teilnehmer",
"Personal Note": "",
"Personal note": "",
"Privacy Policy": "Datenschutz",
"Project": "",
@ -46,11 +45,15 @@
"Reset State": "",
"Reset recommendation": "",
"SORT": "",
"Selected Values": "",
"Settings": "Einstellungen",
"State": "",
"Submitters": "",
"Supporters": "",
"The assembly may decide:": "",
"The event manager hasn't set up a privacy policy yet": {
"0": ""
},
"Welcome to OpenSlides": "Willkommen bei OpenSlides",
"by": ""
}

View File

@ -4,11 +4,8 @@
"Category": "",
"Change Password": "",
"Content": "",
"Cookies": "",
"Copyright by": "",
"Database": "",
"DeleteMotion": "",
"Deleting Files": "",
"Edit Profile": "",
"English": "",
"Export As": {
@ -24,8 +21,9 @@
"German": "",
"Home": "",
"Identifier": "",
"Installed plugins": "",
"Legal Notice": "",
"Logfiles": "",
"License": "",
"Login": "",
"Login as Guest": "",
"Logout": "",
@ -38,6 +36,7 @@
},
"Origin": "",
"Participants": "",
"Personal Note": "",
"Personal note": "",
"Privacy Policy": "",
"Project": "",
@ -46,11 +45,15 @@
"Reset State": "",
"Reset recommendation": "",
"SORT": "",
"Selected Values": "",
"Settings": "",
"State": "",
"Submitters": "",
"Supporters": "",
"The assembly may decide:": "",
"The event manager hasn't set up a privacy policy yet": {
"0": ""
},
"Welcome to OpenSlides": "",
"by": ""
}

View File

@ -4,11 +4,8 @@
"Category": "",
"Change Password": "",
"Content": "",
"Cookies": "",
"Copyright by": "",
"Database": "",
"DeleteMotion": "",
"Deleting Files": "",
"Edit Profile": "",
"English": "",
"Export As": {
@ -24,8 +21,9 @@
"German": "",
"Home": "",
"Identifier": "",
"Installed plugins": "",
"Legal Notice": "",
"Logfiles": "",
"License": "",
"Login": "",
"Login as Guest": "",
"Logout": "",
@ -38,6 +36,7 @@
},
"Origin": "",
"Participants": "",
"Personal Note": "",
"Personal note": "",
"Privacy Policy": "",
"Project": "",
@ -46,11 +45,15 @@
"Reset State": "",
"Reset recommendation": "",
"SORT": "",
"Selected Values": "",
"Settings": "",
"State": "",
"Submitters": "",
"Supporters": "",
"The assembly may decide:": "",
"The event manager hasn't set up a privacy policy yet": {
"0": ""
},
"Welcome to OpenSlides": "",
"by": ""
}