From bf969ec8f764fc7101f8d0eba0a3106700e5ece7 Mon Sep 17 00:00:00 2001 From: Stephan Oeste Date: Thu, 7 Jul 2022 11:14:49 +0200 Subject: [PATCH 1/4] Make user crontab reliable in prod install script --- production/install | 75 ++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/production/install b/production/install index 19ff128a6..ddbba82ce 100755 --- a/production/install +++ b/production/install @@ -1295,18 +1295,6 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then echo "[*] Installing Bitcoin Mainnet electrs start script" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}" - echo "[*] Installing Bitcoin crontab" - case $OS in - FreeBSD) - echo [*] FIXME: must only crontab enabled daemons - osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab" - osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab" - ;; - Debian) - (crontab -l ; echo "@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" - - ;; - esac - echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" @@ -1321,13 +1309,6 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then echo "[*] Installing Bitcoin Testnet electrs start script" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}" - case $OS in - Debian) - echo "[*] Installing Bitcoin-testnet crontab" - (crontab -l ; echo "@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" - - ;; - esac - echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" @@ -1342,13 +1323,6 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then echo "[*] Installing Bitcoin Signet electrs start script" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}" - case $OS in - Debian) - echo "[*] Installing Bitcoin-signet crontab" - (crontab -l ; echo "@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" - - ;; - esac - echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" @@ -1369,9 +1343,6 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then echo [*] FIXME: must only crontab enabled daemons osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab" ;; - Debian) - (crontab -l ; echo "6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" - - ;; esac echo "[*] Configuring Elements Liquid RPC credentials in electrs start script" @@ -1388,13 +1359,6 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then echo "[*] Installing Elements Liquid Testnet electrs start script" osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}" - case $OS in - Debian) - echo "[*] Installing Elements-testnet crontab" - (crontab -l ; echo "6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" - - ;; - esac - echo "[*] Installing Elements Liquid Testnet RPC credentials" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf" @@ -1407,6 +1371,45 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet" fi +################################ +# Install all Electrs Cronjobs # +################################ +echo "[*] Installing crontabs" +case $OS in + FreeBSD) + echo [*] FIXME: must only crontab enabled daemons + osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab" + osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab" + ;; + Debian) + crontab_bitcoin=() + if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then + echo [*] Installing Electrs Mainnet Cronjob + crontab_bitcoin+="@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet\n" + fi + if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then + echo [*] Installing Electrs Testnet Cronjob + crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet\n" + fi + if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then + echo [*] Installing Electrs Signet Cronjob + crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet\n" + fi + echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" - + + crontab_elements=() + if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then + echo [*] Installing Liquid Asset Mainnet Cronjob + crontab_elements+="6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1\n" + fi + if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then + echo [*] Installing Liquid Asset Testnet Cronjob + crontab_elements+="6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1\n" + fi + echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" - + ;; +esac + ##################################### # Bisq instance for Bitcoin Mainnet # ##################################### From d57193c269c80f8ec2256bb34a85b3f443cf8140 Mon Sep 17 00:00:00 2001 From: wiz Date: Thu, 7 Jul 2022 18:32:18 +0200 Subject: [PATCH 2/4] Fix npm install commands in Dockerfiles and ops scripts --- .github/workflows/ci.yml | 4 ++-- docker/backend/Dockerfile | 2 +- docker/frontend/Dockerfile | 2 +- production/mempool-build-all | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90fcaa6d2..1d250a0d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Install (Prod dependencies only) if: ${{ matrix.flavor == 'prod'}} - run: npm ci --prod --no-optional + run: npm ci --omit=dev --omit=optional working-directory: ${{ matrix.flavor }}/backend - name: Lint @@ -70,7 +70,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Install (Prod dependencies only) - run: npm ci --prod --no-optional + run: npm ci --omit=dev --omit=optional if: ${{ matrix.flavor == 'prod'}} working-directory: ${{ matrix.flavor }}/frontend diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 31acff047..6b368f59e 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -8,7 +8,7 @@ COPY . . RUN apt-get update RUN apt-get install -y build-essential python3 pkg-config -RUN npm install +RUN npm install --omit=dev --omit=optional RUN npm run build FROM node:16.15.0-buster-slim diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index e2874ff4e..b58b8ccd0 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /build COPY . . RUN apt-get update RUN apt-get install -y build-essential rsync -RUN npm i +RUN npm install --omit=dev --omit=optional RUN npm run build FROM nginx:1.17.8-alpine diff --git a/production/mempool-build-all b/production/mempool-build-all index 11342605d..5ac25f7e4 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -56,7 +56,7 @@ build_frontend() if [ ! -e "mempool-frontend-config.json" ];then cp "${HOME}/mempool/production/mempool-frontend-config.${site}.json" "mempool-frontend-config.json" fi - npm install --prod --no-optional || exit 1 + npm install --omit=dev --omit=optional || exit 1 npm run build || exit 1 } @@ -75,7 +75,7 @@ build_backend() -e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \ "mempool-config.json" fi - npm install --prod --no-optional || exit 1 + npm install --omit=dev --omit=optional || exit 1 npm run build || exit 1 } From b4bb54212c7c1871484bc2747aaf6dc03e8dd647 Mon Sep 17 00:00:00 2001 From: Stephan Oeste Date: Thu, 7 Jul 2022 19:22:47 +0200 Subject: [PATCH 3/4] Set ulimit highter for all users in prod install --- production/install | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/production/install b/production/install index 19ff128a6..34e5d6c63 100755 --- a/production/install +++ b/production/install @@ -1553,6 +1553,29 @@ case $OS in ;; esac +##### OS set Linux user ulimits + +echo "[*] Setting ulimits for users" +case $OS in + + FreeBSD) + ;; + + Debian) + cat >> /etc/security/limits.conf <> /etc/pam.d/common-session + ;; +esac + + + + + ##### OS services #if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then @@ -1628,6 +1651,8 @@ esac ##### finish +echo 'Please reboot to start all the services.' + echo '[*] Done!' exit 0 From 5867c79a1f5d8d0244717dbd6b471cbfa48534bb Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 7 Jul 2022 20:00:13 +0200 Subject: [PATCH 4/4] Moving ngx-bootrap-multiselect to the project --- frontend/package-lock.json | 22 - frontend/package.json | 1 - .../bisq-transactions.component.html | 2 +- .../bisq-transactions.component.ts | 2 +- frontend/src/app/bisq/bisq.module.ts | 12 +- .../autofocus.directive.ts | 41 + .../ngx-bootstrap-multiselect.component.css | 48 ++ .../ngx-bootstrap-multiselect.component.html | 72 ++ .../ngx-bootstrap-multiselect.component.ts | 710 ++++++++++++++++++ .../off-click.directive.ts | 39 + .../search-filter.pipe.ts | 130 ++++ .../ngx-bootstrap-multiselect/types.ts | 82 ++ 12 files changed, 1134 insertions(+), 27 deletions(-) create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts create mode 100644 frontend/src/app/components/ngx-bootstrap-multiselect/types.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1b3e98b8a..1dc5832fa 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,7 +36,6 @@ "echarts": "~5.3.2", "express": "^4.17.1", "lightweight-charts": "~3.8.0", - "ngx-bootrap-multiselect": "^2.0.0", "ngx-echarts": "8.0.1", "ngx-infinite-scroll": "^10.0.1", "qrcode": "1.5.0", @@ -12788,19 +12787,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, - "node_modules/ngx-bootrap-multiselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz", - "integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@angular/common": "^10.0.6", - "@angular/core": "^10.0.6", - "@angular/forms": "^10.0.6" - } - }, "node_modules/ngx-echarts": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz", @@ -27418,14 +27404,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, - "ngx-bootrap-multiselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz", - "integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==", - "requires": { - "tslib": "^2.0.0" - } - }, "ngx-echarts": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6c626f3c9..573e2181f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -90,7 +90,6 @@ "echarts": "~5.3.2", "express": "^4.17.1", "lightweight-charts": "~3.8.0", - "ngx-bootrap-multiselect": "^2.0.0", "ngx-echarts": "8.0.1", "ngx-infinite-scroll": "^10.0.1", "qrcode": "1.5.0", diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html index 7a2056b46..8d34448d8 100644 --- a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html @@ -3,7 +3,7 @@
- +
diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts index 0df156ad0..d0a2ba3c5 100644 --- a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts @@ -7,7 +7,7 @@ import { BisqApiService } from '../bisq-api.service'; import { SeoService } from 'src/app/services/seo.service'; import { FormGroup, FormBuilder } from '@angular/forms'; import { Router, ActivatedRoute } from '@angular/router'; -import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect'; +import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types' import { WebsocketService } from 'src/app/services/websocket.service'; @Component({ diff --git a/frontend/src/app/bisq/bisq.module.ts b/frontend/src/app/bisq/bisq.module.ts index ebb9c3ee6..93658d95a 100644 --- a/frontend/src/app/bisq/bisq.module.ts +++ b/frontend/src/app/bisq/bisq.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { BisqRoutingModule } from './bisq.routing.module'; import { SharedModule } from '../shared/shared.module'; -import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect'; import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component'; import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component'; @@ -24,6 +23,10 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component'; import { BsqAmountComponent } from './bsq-amount/bsq-amount.component'; import { BisqTradesComponent } from './bisq-trades/bisq-trades.component'; import { CommonModule } from '@angular/common'; +import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive'; +import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe'; +import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive'; +import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component'; @NgModule({ declarations: [ @@ -44,16 +47,21 @@ import { CommonModule } from '@angular/common'; BisqMarketComponent, BisqTradesComponent, BisqMainDashboardComponent, + NgxDropdownMultiselectComponent, + AutofocusDirective, + OffClickDirective, ], imports: [ CommonModule, BisqRoutingModule, SharedModule, FontAwesomeModule, - NgxBootstrapMultiselectModule, ], providers: [ BisqApiService, + MultiSelectSearchFilter, + AutofocusDirective, + OffClickDirective, ] }) export class BisqModule { diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts new file mode 100644 index 000000000..dcf21c884 --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/autofocus.directive.ts @@ -0,0 +1,41 @@ +import { Directive, ElementRef, Host, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; + +@Directive({ + selector: '[ssAutofocus]' +}) +export class AutofocusDirective implements OnInit, OnChanges { + + /** + * Will set focus if set to falsy value or not set at all + */ + @Input() ssAutofocus: any; + + get element(): { focus?: Function } { + return this.elemRef.nativeElement; + } + + constructor( + @Host() private elemRef: ElementRef, + ) { } + + ngOnInit() { + this.focus(); + } + + ngOnChanges(changes: SimpleChanges) { + const ssAutofocusChange = changes.ssAutofocus; + + if (ssAutofocusChange && !ssAutofocusChange.isFirstChange()) { + this.focus(); + } + } + + focus() { + if (this.ssAutofocus) { + return; + } + + this.element.focus && this.element.focus(); + } + +} diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css new file mode 100644 index 000000000..fbaae90a7 --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.css @@ -0,0 +1,48 @@ +a { + outline: none !important; +} + +.dropdown-inline { + display: inline-block; +} + +.dropdown-toggle .caret { + margin-left: 4px; + white-space: nowrap; + display: inline-block; +} + +.chunkydropdown-menu { + min-width: 20em; +} + +.chunkyrow { + line-height: 2; + margin-left: 1em; + font-size: 2em; +} + +.slider { + width:3.8em; + height:3.8em; + display:block; + -webkit-transition: all 0.125s linear; + -moz-transition: all 0.125s linear; + -o-transition: all 0.125s linear; + transition: all 0.125s linear; + margin-left: 0.125em; + margin-top: auto; +} + +.slideron { + margin-left: 1.35em; +} + +.content_wrapper{ + display: table-cell; + vertical-align: middle; +} + +.search-container { + padding: 0px 5px 5px 5px; +} diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html new file mode 100644 index 000000000..055f2789e --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.html @@ -0,0 +1,72 @@ + diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts new file mode 100644 index 000000000..50b195905 --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component.ts @@ -0,0 +1,710 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DoCheck, + EventEmitter, + forwardRef, + Input, + IterableDiffers, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; + +import { + AbstractControl, + ControlValueAccessor, + FormBuilder, + FormControl, + NG_VALUE_ACCESSOR, + Validator, +} from '@angular/forms'; + +import { takeUntil } from 'rxjs/operators'; +import { MultiSelectSearchFilter } from './search-filter.pipe'; +import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from './types'; +import { Subject, Observable } from 'rxjs'; + +const MULTISELECT_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgxDropdownMultiselectComponent), + multi: true, +}; + +// tslint:disable-next-line: no-conflicting-lifecycle +@Component({ + selector: 'ngx-bootstrap-multiselect', + templateUrl: './ngx-bootstrap-multiselect.component.html', + styleUrls: ['./ngx-bootstrap-multiselect.component.css'], + providers: [MULTISELECT_VALUE_ACCESSOR, MultiSelectSearchFilter], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NgxDropdownMultiselectComponent implements OnInit, + OnChanges, + DoCheck, + OnDestroy, + ControlValueAccessor, + Validator { + + private localIsVisible = false; + private workerDocClicked = false; + + filterControl: FormControl = this.fb.control(''); + + @Input() options: Array; + @Input() settings: IMultiSelectSettings; + @Input() texts: IMultiSelectTexts; + @Input() disabled = false; + @Input() disabledSelection = false; + @Input() searchFunction: (str: string) => RegExp = this._escapeRegExp; + + @Output() selectionLimitReached = new EventEmitter(); + @Output() dropdownClosed = new EventEmitter(); + @Output() dropdownOpened = new EventEmitter(); + @Output() added = new EventEmitter(); + @Output() removed = new EventEmitter(); + @Output() lazyLoad = new EventEmitter(); + @Output() filter: Observable = this.filterControl.valueChanges; + + get focusBack(): boolean { + return this.settings.focusBack && this._focusBack; + } + + destroyed$ = new Subject(); + + filteredOptions: IMultiSelectOption[] = []; + lazyLoadOptions: IMultiSelectOption[] = []; + renderFilteredOptions: IMultiSelectOption[] = []; + model: any[] = []; + prevModel: any[] = []; + parents: any[]; + title: string; + differ: any; + numSelected = 0; + set isVisible(val: boolean) { + this.localIsVisible = val; + this.workerDocClicked = val ? false : this.workerDocClicked; + } + get isVisible(): boolean { + return this.localIsVisible; + } + renderItems = true; + checkAllSearchRegister = new Set(); + checkAllStatus = false; + loadedValueIds = []; + _focusBack = false; + focusedItem: IMultiSelectOption | undefined; + + defaultSettings: IMultiSelectSettings = { + closeOnClickOutside: true, + pullRight: false, + enableSearch: false, + searchRenderLimit: 0, + searchRenderAfter: 1, + searchMaxLimit: 0, + searchMaxRenderedItems: 0, + checkedStyle: 'checkboxes', + buttonClasses: 'btn btn-primary dropdown-toggle', + containerClasses: 'dropdown-inline', + selectionLimit: 0, + minSelectionLimit: 0, + closeOnSelect: false, + autoUnselect: false, + showCheckAll: false, + showUncheckAll: false, + fixedTitle: false, + dynamicTitleMaxItems: 3, + maxHeight: '300px', + isLazyLoad: false, + stopScrollPropagation: false, + loadViewDistance: 1, + selectAddedValues: false, + ignoreLabels: false, + maintainSelectionOrderInTitle: false, + focusBack: true + }; + defaultTexts: IMultiSelectTexts = { + checkAll: 'Select all', + uncheckAll: 'Unselect all', + checked: 'selected', + checkedPlural: 'selected', + searchPlaceholder: 'Search...', + searchEmptyResult: 'Nothing found...', + searchNoRenderText: 'Type in search box to see results...', + defaultTitle: 'Select', + allSelected: 'All selected', + }; + + get searchLimit(): number | undefined { + return this.settings.searchRenderLimit; + } + + get searchRenderAfter(): number | undefined { + return this.settings.searchRenderAfter; + } + + get searchLimitApplied(): boolean { + return this.searchLimit > 0 && this.options.length > this.searchLimit; + } + + constructor( + private fb: FormBuilder, + private searchFilter: MultiSelectSearchFilter, + differs: IterableDiffers, + private cdRef: ChangeDetectorRef + ) { + this.differ = differs.find([]).create(null); + this.settings = this.defaultSettings; + this.texts = this.defaultTexts; + } + + clickedOutside(): void { + if (!this.isVisible || !this.settings.closeOnClickOutside) { return; } + + this.isVisible = false; + this._focusBack = true; + this.dropdownClosed.emit(); + } + + getItemStyle(option: IMultiSelectOption): any { + const style = {}; + if (!option.isLabel) { + style['cursor'] = 'pointer'; + } + if (option.disabled) { + style['cursor'] = 'default'; + } + } + + getItemStyleSelectionDisabled(): any { + if (this.disabledSelection) { + return { cursor: 'default' }; + } + } + + ngOnInit(): void { + this.title = this.texts.defaultTitle || ''; + + this.filterControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(() => { + this.updateRenderItems(); + if (this.settings.isLazyLoad) { + this.load(); + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['options']) { + this.options = this.options || []; + this.parents = this.options + .filter(option => typeof option.parentId === 'number') + .map(option => option.parentId); + this.updateRenderItems(); + + if ( + this.settings.isLazyLoad && + this.settings.selectAddedValues && + this.loadedValueIds.length === 0 + ) { + this.loadedValueIds = this.loadedValueIds.concat( + changes.options.currentValue.map(value => value.id) + ); + } + if ( + this.settings.isLazyLoad && + this.settings.selectAddedValues && + changes.options.previousValue + ) { + const addedValues = changes.options.currentValue.filter( + value => this.loadedValueIds.indexOf(value.id) === -1 + ); + this.loadedValueIds.concat(addedValues.map(value => value.id)); + if (this.checkAllStatus) { + this.addChecks(addedValues); + } else if (this.checkAllSearchRegister.size > 0) { + this.checkAllSearchRegister.forEach((searchValue: string) => + this.addChecks(this.applyFilters(addedValues, searchValue)) + ); + } + } + + if (this.texts) { + this.updateTitle(); + } + + this.fireModelChange(); + } + + if (changes['settings']) { + this.settings = { ...this.defaultSettings, ...this.settings }; + } + + if (changes['texts']) { + this.texts = { ...this.defaultTexts, ...this.texts }; + if (!changes['texts'].isFirstChange()) { this.updateTitle(); } + } + } + + ngOnDestroy() { + this.destroyed$.next(false); + } + + updateRenderItems() { + this.renderItems = + !this.searchLimitApplied || + this.filterControl.value.length >= this.searchRenderAfter; + this.filteredOptions = this.applyFilters( + this.options, + this.settings.isLazyLoad ? '' : this.filterControl.value + ); + this.renderFilteredOptions = this.renderItems ? this.filteredOptions : []; + this.focusedItem = undefined; + } + + applyFilters(options: IMultiSelectOption[], value: string): IMultiSelectOption[] { + return this.searchFilter.transform( + options, + value, + this.settings.searchMaxLimit, + this.settings.searchMaxRenderedItems, + this.searchFunction + ); + } + + fireModelChange(): void { + if (this.model != this.prevModel) { + this.prevModel = this.model; + this.onModelChange(this.model); + this.onModelTouched(); + this.cdRef.markForCheck(); + } + } + + onModelChange: Function = (_: any) => { }; + onModelTouched: Function = () => { }; + + writeValue(value: any): void { + if (value !== undefined && value !== null) { + this.model = Array.isArray(value) ? value : [value]; + this.ngDoCheck(); + } else { + this.model = []; + } + } + + registerOnChange(fn: Function): void { + this.onModelChange = fn; + } + + registerOnTouched(fn: Function): void { + this.onModelTouched = fn; + } + + setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + } + + ngDoCheck() { + const changes = this.differ.diff(this.model); + if (changes) { + this.updateNumSelected(); + this.updateTitle(); + } + } + + validate(_c: AbstractControl): { [key: string]: any } { + if (this.model && this.model.length) { + return { + required: { + valid: false + } + }; + } + + if (this.options.filter(o => this.model.indexOf(o.id) && !o.disabled).length === 0) { + return { + selection: { + valid: false + } + }; + } + + return null; + } + + registerOnValidatorChange(_fn: () => void): void { + throw new Error('Method not implemented.'); + } + + clearSearch(event: Event) { + this.maybeStopPropagation(event); + this.filterControl.setValue(''); + } + + toggleDropdown(e?: Event) { + if (this.isVisible) { + this._focusBack = true; + } + + this.isVisible = !this.isVisible; + this.isVisible ? this.dropdownOpened.emit() : this.dropdownClosed.emit(); + this.focusedItem = undefined; + } + + closeDropdown(e?: Event) { + this.isVisible = true; + this.toggleDropdown(e); + } + + isSelected(option: IMultiSelectOption): boolean { + return this.model && this.model.indexOf(option.id) > -1; + } + + setSelected(_event: Event, option: IMultiSelectOption) { + if (option.isLabel) { + return; + } + + if (option.disabled) { + return; + } + + if (this.disabledSelection) { + return; + } + + setTimeout(() => { + this.maybeStopPropagation(_event); + this.maybePreventDefault(_event); + const index = this.model.indexOf(option.id); + const isAtSelectionLimit = + this.settings.selectionLimit > 0 && + this.model.length >= this.settings.selectionLimit; + const removeItem = (idx, id): void => { + this.model.splice(idx, 1); + this.removed.emit(id); + if ( + this.settings.isLazyLoad && + this.lazyLoadOptions.some(val => val.id === id) + ) { + this.lazyLoadOptions.splice( + this.lazyLoadOptions.indexOf( + this.lazyLoadOptions.find(val => val.id === id) + ), + 1 + ); + } + }; + + if (index > -1) { + if ( + this.settings.minSelectionLimit === undefined || + this.numSelected > this.settings.minSelectionLimit + ) { + removeItem(index, option.id); + } + const parentIndex = + option.parentId && this.model.indexOf(option.parentId); + if (parentIndex > -1) { + removeItem(parentIndex, option.parentId); + } else if (this.parents.indexOf(option.id) > -1) { + this.options + .filter( + child => + this.model.indexOf(child.id) > -1 && + child.parentId === option.id + ) + .forEach(child => + removeItem(this.model.indexOf(child.id), child.id) + ); + } + } else if (isAtSelectionLimit && !this.settings.autoUnselect) { + this.selectionLimitReached.emit(this.model.length); + return; + } else { + const addItem = (id): void => { + this.model.push(id); + this.added.emit(id); + if ( + this.settings.isLazyLoad && + !this.lazyLoadOptions.some(val => val.id === id) + ) { + this.lazyLoadOptions.push(option); + } + }; + + addItem(option.id); + if (!isAtSelectionLimit) { + if (option.parentId && !this.settings.ignoreLabels) { + const children = this.options.filter( + child => + child.id !== option.id && child.parentId === option.parentId + ); + if (children.every(child => this.model.indexOf(child.id) > -1)) { + addItem(option.parentId); + } + } else if (this.parents.indexOf(option.id) > -1) { + const children = this.options.filter( + child => + this.model.indexOf(child.id) < 0 && child.parentId === option.id + ); + children.forEach(child => addItem(child.id)); + } + } else { + removeItem(0, this.model[0]); + } + } + if (this.settings.closeOnSelect) { + this.toggleDropdown(); + } + this.model = this.model.slice(); + this.fireModelChange(); + + }, 0) + } + + updateNumSelected() { + this.numSelected = + this.model.filter(id => this.parents.indexOf(id) < 0).length || 0; + } + + updateTitle() { + let numSelectedOptions = this.options.length; + if (this.settings.ignoreLabels) { + numSelectedOptions = this.options.filter( + (option: IMultiSelectOption) => !option.isLabel + ).length; + } + if (this.numSelected === 0 || this.settings.fixedTitle) { + this.title = this.texts ? this.texts.defaultTitle : ''; + } else if ( + this.settings.displayAllSelectedText && + this.model.length === numSelectedOptions + ) { + this.title = this.texts ? this.texts.allSelected : ''; + } else if ( + this.settings.dynamicTitleMaxItems && + this.settings.dynamicTitleMaxItems >= this.numSelected + ) { + const useOptions = + this.settings.isLazyLoad && this.lazyLoadOptions.length + ? this.lazyLoadOptions + : this.options; + + let titleSelections: Array; + + if (this.settings.maintainSelectionOrderInTitle) { + const optionIds = useOptions.map((selectOption: IMultiSelectOption, idx: number) => selectOption.id); + titleSelections = this.model + .map((selectedId) => optionIds.indexOf(selectedId)) + .filter((optionIndex) => optionIndex > -1) + .map((optionIndex) => useOptions[optionIndex]); + } else { + titleSelections = useOptions.filter((option: IMultiSelectOption) => this.model.indexOf(option.id) > -1); + } + + this.title = titleSelections.map((option: IMultiSelectOption) => option.name).join(', '); + } else { + this.title = + this.numSelected + + ' ' + + (this.numSelected === 1 + ? this.texts.checked + : this.texts.checkedPlural); + } + this.cdRef.markForCheck(); + } + + searchFilterApplied() { + return ( + this.settings.enableSearch && + this.filterControl.value && + this.filterControl.value.length > 0 + ); + } + + addChecks(options) { + const checkedOptions = options + .filter((option: IMultiSelectOption) => { + if ( + !option.disabled && + ( + this.model.indexOf(option.id) === -1 && + !(this.settings.ignoreLabels && option.isLabel) + ) + ) { + this.added.emit(option.id); + return true; + } + return false; + }) + .map((option: IMultiSelectOption) => option.id); + + this.model = this.model.concat(checkedOptions); + } + + checkAll(): void { + if (!this.disabledSelection) { + this.addChecks( + !this.searchFilterApplied() ? this.options : this.filteredOptions + ); + if (this.settings.isLazyLoad && this.settings.selectAddedValues) { + if (this.searchFilterApplied() && !this.checkAllStatus) { + this.checkAllSearchRegister.add(this.filterControl.value); + } else { + this.checkAllSearchRegister.clear(); + this.checkAllStatus = true; + } + this.load(); + } + this.fireModelChange(); + } + } + + uncheckAll(): void { + if (!this.disabledSelection) { + const checkedOptions = this.model; + let unCheckedOptions = !this.searchFilterApplied() + ? this.model + : this.filteredOptions.map((option: IMultiSelectOption) => option.id); + // set unchecked options only to the ones that were checked + unCheckedOptions = checkedOptions.filter(item => unCheckedOptions.indexOf(item) > -1); + this.model = this.model.filter((id: number) => { + if ( + (unCheckedOptions.indexOf(id) < 0 && + this.settings.minSelectionLimit === undefined) || + unCheckedOptions.indexOf(id) < this.settings.minSelectionLimit + ) { + return true; + } else { + this.removed.emit(id); + return false; + } + }); + if (this.settings.isLazyLoad && this.settings.selectAddedValues) { + if (this.searchFilterApplied()) { + if (this.checkAllSearchRegister.has(this.filterControl.value)) { + this.checkAllSearchRegister.delete(this.filterControl.value); + this.checkAllSearchRegister.forEach(function(searchTerm) { + const filterOptions = this.applyFilters(this.options.filter(option => unCheckedOptions.indexOf(option.id) > -1), searchTerm); + this.addChecks(filterOptions); + }); + } + } else { + this.checkAllSearchRegister.clear(); + this.checkAllStatus = false; + } + this.load(); + } + this.fireModelChange(); + } + } + + preventCheckboxCheck(event: Event, option: IMultiSelectOption): void { + if ( + option.disabled || + ( + this.settings.selectionLimit && + !this.settings.autoUnselect && + this.model.length >= this.settings.selectionLimit && + this.model.indexOf(option.id) === -1 && + this.maybePreventDefault(event) + ) + ) { + this.maybePreventDefault(event); + } + } + + isCheckboxDisabled(option?: IMultiSelectOption): boolean { + return this.disabledSelection || option && option.disabled; + } + + checkScrollPosition(ev): void { + const scrollTop = ev.target.scrollTop; + const scrollHeight = ev.target.scrollHeight; + const scrollElementHeight = ev.target.clientHeight; + const roundingPixel = 1; + const gutterPixel = 1; + + if ( + scrollTop >= + scrollHeight - + (1 + this.settings.loadViewDistance) * scrollElementHeight - + roundingPixel - + gutterPixel + ) { + this.load(); + } + } + + checkScrollPropagation(ev, element): void { + const scrollTop = element.scrollTop; + const scrollHeight = element.scrollHeight; + const scrollElementHeight = element.clientHeight; + + if ( + (ev.deltaY > 0 && scrollTop + scrollElementHeight >= scrollHeight) || + (ev.deltaY < 0 && scrollTop <= 0) + ) { + ev = ev || window.event; + this.maybePreventDefault(ev); + ev.returnValue = false; + } + } + + trackById(idx: number, selectOption: IMultiSelectOption): void { + return selectOption.id; + } + + load(): void { + this.lazyLoad.emit({ + length: this.options.length, + filter: this.filterControl.value, + checkAllSearches: this.checkAllSearchRegister, + checkAllStatus: this.checkAllStatus, + }); + } + + focusItem(dir: number, e?: Event): void { + if (!this.isVisible) { + return; + } + + this.maybePreventDefault(e); + + const idx = this.filteredOptions.indexOf(this.focusedItem); + + if (idx === -1) { + this.focusedItem = this.filteredOptions[0]; + return; + } + + const nextIdx = idx + dir; + const newIdx = + nextIdx < 0 + ? this.filteredOptions.length - 1 + : nextIdx % this.filteredOptions.length; + + this.focusedItem = this.filteredOptions[newIdx]; + } + + private maybePreventDefault(e?: Event): void { + if (e && e.preventDefault) { + e.preventDefault(); + } + } + + private maybeStopPropagation(e?: Event): void { + if (e && e.stopPropagation) { + e.stopPropagation(); + } + } + + private _escapeRegExp(str: string): RegExp { + const regExpStr = str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); + return new RegExp(regExpStr, 'i'); + } +} diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts new file mode 100644 index 000000000..6620c436c --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/off-click.directive.ts @@ -0,0 +1,39 @@ +import { Directive, HostListener } from '@angular/core'; +import { EventEmitter } from '@angular/core'; +import { Output } from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[offClick]', +}) + +export class OffClickDirective { + @Output('offClick') onOffClick = new EventEmitter(); + + private _clickEvent: MouseEvent; + private _touchEvent: TouchEvent; + + @HostListener('click', ['$event']) + public onClick(event: MouseEvent): void { + this._clickEvent = event; + } + + @HostListener('touchstart', ['$event']) + public onTouch(event: TouchEvent): void { + this._touchEvent = event; + } + + @HostListener('document:click', ['$event']) + public onDocumentClick(event: MouseEvent): void { + if (event !== this._clickEvent) { + this.onOffClick.emit(event); + } + } + + @HostListener('document:touchstart', ['$event']) + public onDocumentTouch(event: TouchEvent): void { + if (event !== this._touchEvent) { + this.onOffClick.emit(event); + } + } +} diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts new file mode 100644 index 000000000..1dfb57ffd --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/search-filter.pipe.ts @@ -0,0 +1,130 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { IMultiSelectOption } from './types'; + +interface StringHashMap { + [k: string]: T; +} + +@Pipe({ + name: 'searchFilter' +}) +export class MultiSelectSearchFilter implements PipeTransform { + + private _lastOptions: IMultiSelectOption[]; + private _searchCache: StringHashMap = {}; + private _searchCacheInclusive: StringHashMap = {}; + private _prevSkippedItems: StringHashMap = {}; + + transform( + options: IMultiSelectOption[], + str = '', + limit = 0, + renderLimit = 0, + searchFunction: (str: string) => RegExp, + ): IMultiSelectOption[] { + str = str.toLowerCase(); + + // Drop cache because options were updated + if (options !== this._lastOptions) { + this._lastOptions = options; + this._searchCache = {}; + this._searchCacheInclusive = {}; + this._prevSkippedItems = {}; + } + + const filteredOpts = this._searchCache.hasOwnProperty(str) + ? this._searchCache[str] + : this._doSearch(options, str, limit, searchFunction); + + const isUnderLimit = options.length <= limit; + + return isUnderLimit + ? filteredOpts + : this._limitRenderedItems(filteredOpts, renderLimit); + } + + private _getSubsetOptions( + options: IMultiSelectOption[], + prevOptions: IMultiSelectOption[], + prevSearchStr: string + ) { + const prevInclusiveOrIdx = this._searchCacheInclusive[prevSearchStr]; + + if (prevInclusiveOrIdx === true) { + // If have previous results and it was inclusive, do only subsearch + return prevOptions; + } else if (typeof prevInclusiveOrIdx === 'number') { + // Or reuse prev results with unchecked ones + return [...prevOptions, ...options.slice(prevInclusiveOrIdx)]; + } + + return options; + } + + private _doSearch(options: IMultiSelectOption[], str: string, limit: number, searchFunction: (str: string) => RegExp) { + const prevStr = str.slice(0, -1); + const prevResults = this._searchCache[prevStr]; + const prevResultShift = this._prevSkippedItems[prevStr] || 0; + + if (prevResults) { + options = this._getSubsetOptions(options, prevResults, prevStr); + } + + const optsLength = options.length; + const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength; + const regexp = searchFunction(str); + const filteredOpts: IMultiSelectOption[] = []; + + let i = 0, founded = 0, removedFromPrevResult = 0; + + const doesOptionMatch = (option: IMultiSelectOption) => regexp.test(option.name); + const getChildren = (option: IMultiSelectOption) => + options.filter(child => child.parentId === option.id); + const getParent = (option: IMultiSelectOption) => + options.find(parent => option.parentId === parent.id); + const foundFn = (item: any) => { filteredOpts.push(item); founded++; }; + const notFoundFn = prevResults ? () => removedFromPrevResult++ : () => { }; + + for (; i < optsLength && founded < maxFound; ++i) { + const option = options[i]; + const directMatch = doesOptionMatch(option); + + if (directMatch) { + foundFn(option); + continue; + } + + if (typeof option.parentId === 'undefined') { + const childrenMatch = getChildren(option).some(doesOptionMatch); + + if (childrenMatch) { + foundFn(option); + continue; + } + } + + if (typeof option.parentId !== 'undefined') { + const parentMatch = doesOptionMatch(getParent(option)); + + if (parentMatch) { + foundFn(option); + continue; + } + } + + notFoundFn(); + } + + const totalIterations = i + prevResultShift; + + this._searchCache[str] = filteredOpts; + this._searchCacheInclusive[str] = i === optsLength || totalIterations; + this._prevSkippedItems[str] = removedFromPrevResult + prevResultShift; + + return filteredOpts; + } + + private _limitRenderedItems(items: T[], limit: number): T[] { + return items.length > limit && limit > 0 ? items.slice(0, limit) : items; + } +} diff --git a/frontend/src/app/components/ngx-bootstrap-multiselect/types.ts b/frontend/src/app/components/ngx-bootstrap-multiselect/types.ts new file mode 100644 index 000000000..0b04ea8aa --- /dev/null +++ b/frontend/src/app/components/ngx-bootstrap-multiselect/types.ts @@ -0,0 +1,82 @@ +export interface IMultiSelectOption { + id: any; + name: string; + disabled?: boolean; + isLabel?: boolean; + parentId?: any; + params?: any; + classes?: string; + image?: string; +} + +export interface IMultiSelectSettings { + pullRight?: boolean; + enableSearch?: boolean; + closeOnClickOutside?: boolean; + /** + * 0 - By default + * If `enableSearch=true` and total amount of items more then `searchRenderLimit` (0 - No limit) + * then render items only when user typed more then or equal `searchRenderAfter` charachters + */ + searchRenderLimit?: number; + /** + * 3 - By default + */ + searchRenderAfter?: number; + /** + * 0 - By default + * If >0 will render only N first items + */ + searchMaxLimit?: number; + /** + * 0 - By default + * Used with searchMaxLimit to further limit rendering for optimization + * Should be less than searchMaxLimit to take effect + */ + searchMaxRenderedItems?: number; + checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome' | 'visual'; + buttonClasses?: string; + itemClasses?: string; + containerClasses?: string; + selectionLimit?: number; + minSelectionLimit?: number; + closeOnSelect?: boolean; + autoUnselect?: boolean; + showCheckAll?: boolean; + showUncheckAll?: boolean; + fixedTitle?: boolean; + dynamicTitleMaxItems?: number; + maxHeight?: string; + displayAllSelectedText?: boolean; + isLazyLoad?: boolean; + loadViewDistance?: number; + stopScrollPropagation?: boolean; + selectAddedValues?: boolean; + /** + * false - By default + * If activated label IDs don't count and won't be written to the model. + */ + ignoreLabels?: boolean; + /** + * false - By default + * If activated, the title will show selections in the order they were selected. + */ + maintainSelectionOrderInTitle?: boolean; + /** + * @default true + * Set the focus back to the input control when the dropdown closed + */ + focusBack?: boolean; +} + +export interface IMultiSelectTexts { + checkAll?: string; + uncheckAll?: string; + checked?: string; + checkedPlural?: string; + searchPlaceholder?: string; + searchEmptyResult?: string; + searchNoRenderText?: string; + defaultTitle?: string; + allSelected?: string; +}