From c30a3300d3a93f04b82995f6527bbb83310759d0 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Thu, 12 Aug 2021 10:10:56 +0300 Subject: [PATCH] clone theme-shared package to nx workspace --- npm/ng-packs/nx/ng-packs/angular.json | 89 ++++- .../ng-packs/libs/core/locale/ng-package.json | 2 +- .../nx/ng-packs/libs/core/ng-package.json | 2 +- .../libs/core/src/{index.ts => public-api.ts} | 0 .../libs/core/testing/ng-package.json | 2 +- .../nx/ng-packs/libs/core/tsconfig.json | 9 - .../ng-packs/libs/core/tsconfig.lib.prod.json | 4 +- .../ng-packs/libs/theme-shared/.eslintrc.json | 36 ++ .../nx/ng-packs/libs/theme-shared/README.md | 7 + .../theme-shared/extensions/ng-package.json | 7 + .../src/lib/adapters/date-time.adapter.ts | 56 +++ .../src/lib/adapters/date.adapter.ts | 40 ++ .../src/lib/adapters/time.adapter.ts | 35 ++ .../abstract-actions.component.ts | 32 ++ .../date-time-picker.component.ts | 76 ++++ .../extensible-form-prop.component.html | 138 +++++++ .../extensible-form-prop.component.ts | 193 +++++++++ .../extensible-form.component.html | 23 ++ .../extensible-form.component.ts | 64 +++ .../extensible-table.component.html | 40 ++ .../extensible-table.component.ts | 145 +++++++ .../grid-actions/grid-actions.component.html | 43 ++ .../grid-actions/grid-actions.component.ts | 36 ++ .../page-toolbar/page-toolbar.component.html | 20 + .../page-toolbar/page-toolbar.component.ts | 49 +++ .../src/lib/constants/extra-properties.ts | 1 + .../src/lib/directives/disabled.directive.ts | 19 + .../src/lib/directives/prop-data.directive.ts | 54 +++ .../extensions/src/lib/enums/props.enum.ts | 15 + .../extensions/src/lib/models/actions.ts | 79 ++++ .../src/lib/models/entity-actions.ts | 53 +++ .../extensions/src/lib/models/entity-props.ts | 71 ++++ .../extensions/src/lib/models/form-props.ts | 109 ++++++ .../lib/models/internal/object-extensions.ts | 108 +++++ .../src/lib/models/object-extensions.ts | 3 + .../extensions/src/lib/models/props.ts | 85 ++++ .../src/lib/models/toolbar-actions.ts | 78 ++++ .../src/lib/services/extensions.service.ts | 16 + .../src/lib/tokens/extensions.token.ts | 13 + .../src/lib/ui-extensions.module.ts | 56 +++ .../extensions/src/lib/utils/actions.util.ts | 49 +++ .../extensions/src/lib/utils/enum.util.ts | 73 ++++ .../extensions/src/lib/utils/factory.util.ts | 3 + .../src/lib/utils/form-props.util.ts | 61 +++ .../src/lib/utils/localization.util.ts | 33 ++ .../extensions/src/lib/utils/props.util.ts | 62 +++ .../extensions/src/lib/utils/state.util.ts | 200 ++++++++++ .../src/lib/utils/typeahead.util.ts | 72 ++++ .../src/lib/utils/validation.util.ts | 16 + .../theme-shared/extensions/src/public-api.ts | 70 ++++ .../extensions/src/tests/actions.spec.ts | 22 ++ .../extensions/src/tests/actions.util.spec.ts | 53 +++ .../src/tests/date-time.adapter.spec.ts | 35 ++ .../extensions/src/tests/date.adapter.spec.ts | 31 ++ .../src/tests/entity-actions.spec.ts | 151 +++++++ .../extensions/src/tests/entity-props.spec.ts | 166 ++++++++ .../extensions/src/tests/enum.util.spec.ts | 114 ++++++ .../src/tests/extensions.service.spec.ts | 50 +++ .../extensions/src/tests/factory.util.spec.ts | 28 ++ .../extensions/src/tests/form-props.spec.ts | 193 +++++++++ .../src/tests/form-props.util.spec.ts | 142 +++++++ .../src/tests/localization.util.spec.ts | 34 ++ .../extensions/src/tests/props.spec.ts | 22 ++ .../extensions/src/tests/props.util.spec.ts | 72 ++++ .../extensions/src/tests/state.util.spec.ts | 369 ++++++++++++++++++ .../extensions/src/tests/time.adapter.spec.ts | 36 ++ .../src/tests/toolbar-actions.spec.ts | 216 ++++++++++ .../src/tests/typeahead.util.spec.ts | 72 ++++ .../src/tests/validation.util.spec.ts | 20 + .../src/lib/ui-extensions-testing.module.ts | 8 + .../extensions/testing/src/public-api.ts | 1 + .../extensions/testing/ssng-package.json | 7 + .../ng-packs/libs/theme-shared/jest.config.js | 20 + .../libs/theme-shared/ng-package.json | 16 + .../ng-packs/libs/theme-shared/package.json | 22 ++ .../src/lib/animations/bounce.animations.ts | 23 ++ .../src/lib/animations/collapse.animations.ts | 90 +++++ .../src/lib/animations/fade.animations.ts | 74 ++++ .../theme-shared/src/lib/animations/index.ts | 6 + .../src/lib/animations/modal.animations.ts | 12 + .../src/lib/animations/slide.animations.ts | 7 + .../src/lib/animations/toast.animations.ts | 17 + .../breadcrumb/breadcrumb.component.html | 13 + .../breadcrumb/breadcrumb.component.ts | 58 +++ .../lib/components/button/button.component.ts | 82 ++++ .../lib/components/chart/chart.component.html | 11 + .../lib/components/chart/chart.component.ts | 142 +++++++ .../confirmation/confirmation.component.html | 39 ++ .../confirmation/confirmation.component.scss | 82 ++++ .../confirmation/confirmation.component.ts | 37 ++ .../http-error-wrapper.component.html | 37 ++ .../http-error-wrapper.component.scss | 14 + .../http-error-wrapper.component.ts | 90 +++++ .../theme-shared/src/lib/components/index.ts | 15 + .../loader-bar/loader-bar.component.scss | 24 ++ .../loader-bar/loader-bar.component.ts | 120 ++++++ .../components/loading/loading.component.ts | 40 ++ .../components/modal/modal-close.directive.ts | 16 + .../modal/modal-container.component.ts | 13 + .../lib/components/modal/modal-ref.service.ts | 26 ++ .../lib/components/modal/modal.component.html | 23 ++ .../lib/components/modal/modal.component.scss | 25 ++ .../lib/components/modal/modal.component.ts | 253 ++++++++++++ .../sort-order-icon.component.html | 3 + .../sort-order-icon.component.ts | 63 +++ .../table-empty-message.component.ts | 28 ++ .../lib/components/table/table.component.html | 81 ++++ .../lib/components/table/table.component.scss | 337 ++++++++++++++++ .../lib/components/table/table.component.ts | 110 ++++++ .../toast-container.component.html | 11 + .../toast-container.component.scss | 13 + .../toast-container.component.ts | 46 +++ .../lib/components/toast/toast.component.html | 17 + .../lib/components/toast/toast.component.scss | 81 ++++ .../lib/components/toast/toast.component.ts | 52 +++ .../theme-shared/src/lib/constants/scripts.ts | 1 + .../theme-shared/src/lib/constants/styles.ts | 181 +++++++++ .../src/lib/constants/validation.ts | 16 + .../src/lib/directives/ellipsis.directive.ts | 52 +++ .../theme-shared/src/lib/directives/index.ts | 5 + .../src/lib/directives/loading.directive.ts | 102 +++++ .../ngx-datatable-default.directive.ts | 84 ++++ .../ngx-datatable-list.directive.ts | 106 +++++ .../lib/directives/table-sort.directive.ts | 58 +++ .../libs/theme-shared/src/lib/enums/index.ts | 1 + .../theme-shared/src/lib/enums/route-names.ts | 3 + .../src/lib/handlers/error.handler.ts | 338 ++++++++++++++++ .../theme-shared/src/lib/handlers/index.ts | 2 + .../src/lib/handlers/lazy-style.handler.ts | 111 ++++++ .../theme-shared/src/lib/models/common.ts | 27 ++ .../src/lib/models/confirmation.ts | 29 ++ .../libs/theme-shared/src/lib/models/index.ts | 5 + .../theme-shared/src/lib/models/nav-item.ts | 15 + .../theme-shared/src/lib/models/statistics.ts | 14 + .../theme-shared/src/lib/models/toaster.ts | 55 +++ .../theme-shared/src/lib/providers/index.ts | 2 + .../providers/ng-bootstrap-config.provider.ts | 21 + .../src/lib/providers/route.provider.ts | 20 + .../src/lib/services/confirmation.service.ts | 100 +++++ .../theme-shared/src/lib/services/index.ts | 5 + .../src/lib/services/modal.service.ts | 54 +++ .../src/lib/services/nav-items.service.ts | 50 +++ .../src/lib/services/page-alert.service.ts | 34 ++ .../src/lib/services/toaster.service.ts | 138 +++++++ .../lib/tests/append-content.token.spec.ts | 30 ++ .../lib/tests/breadcrumb.component.spec.ts | 84 ++++ .../src/lib/tests/button.component.spec.ts | 56 +++ .../src/lib/tests/chart.component.spec.ts | 126 ++++++ .../lib/tests/confirmation.service.spec.ts | 135 +++++++ .../src/lib/tests/ellipsis.directive.spec.ts | 55 +++ .../src/lib/tests/error.component.spec.ts | 44 +++ .../src/lib/tests/error.handler.spec.ts | 328 ++++++++++++++++ .../src/lib/tests/lazy-style.handler.spec.ts | 66 ++++ .../lib/tests/loader-bar.component.spec.ts | 94 +++++ .../src/lib/tests/loading.directive.spec.ts | 93 +++++ .../tests/modal-container.component.spec.ts | 34 ++ .../src/lib/tests/modal.component.spec.ts | 213 ++++++++++ .../src/lib/tests/modal.service.spec.ts | 87 +++++ .../tests/sort-order-icon.component.spec.ts | 46 +++ .../lib/tests/table-sort.directive.spec.ts | 32 ++ .../src/lib/tests/table.component.spec.ts | 75 ++++ .../src/lib/tests/toaster.service.spec.ts | 122 ++++++ .../src/lib/tests/validation-utils.spec.ts | 52 +++ .../src/lib/theme-shared.module.ts | 140 +++++++ .../src/lib/tokens/append-content.token.ts | 15 + .../src/lib/tokens/http-error.token.ts | 18 + .../libs/theme-shared/src/lib/tokens/index.ts | 5 + .../src/lib/tokens/lazy-styles.token.ts | 3 + .../tokens/ngx-datatable-messages.token.ts | 17 + .../suppress-unsaved-changes-warning.token.ts | 6 + .../src/lib/utils/date-parser-formatter.ts | 56 +++ .../libs/theme-shared/src/lib/utils/index.ts | 3 + .../src/lib/utils/validation-utils.ts | 45 +++ .../src/lib/utils/widget-utils.ts | 16 + .../libs/theme-shared/src/public-api.ts | 17 + .../libs/theme-shared/src/test-setup.ts | 1 + .../theme-shared/styles/bootstrap-rtl.min.css | 7 + .../libs/theme-shared/testing/ng-package.json | 7 + .../testing/src/lib/models/config.ts | 5 + .../testing/src/lib/models/index.ts | 1 + .../src/lib/theme-shared-testing.module.ts | 52 +++ .../theme-shared/testing/src/public-api.ts | 1 + .../ng-packs/libs/theme-shared/tsconfig.json | 16 + .../libs/theme-shared/tsconfig.lib.json | 14 + .../libs/theme-shared/tsconfig.lib.prod.json | 11 + .../libs/theme-shared/tsconfig.spec.json | 10 + npm/ng-packs/nx/ng-packs/nx.json | 6 + npm/ng-packs/nx/ng-packs/package.json | 19 +- npm/ng-packs/nx/ng-packs/tsconfig.base.json | 5 +- npm/ng-packs/nx/ng-packs/yarn.lock | 125 +++--- npm/ng-packs/yarn.lock | 68 ++-- 191 files changed, 10657 insertions(+), 111 deletions(-) rename npm/ng-packs/nx/ng-packs/libs/core/src/{index.ts => public-api.ts} (100%) create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/.eslintrc.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/README.md create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/ng-package.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date.adapter.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/time.adapter.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/constants/extra-properties.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/disabled.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/prop-data.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/enums/props.enum.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/actions.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-actions.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-props.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/form-props.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/internal/object-extensions.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/object-extensions.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/props.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/toolbar-actions.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/services/extensions.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/tokens/extensions.token.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/ui-extensions.module.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/actions.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/enum.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/factory.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/form-props.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/localization.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/props.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/state.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/typeahead.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/validation.util.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/public-api.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date-time.adapter.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date.adapter.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-actions.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-props.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/enum.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/extensions.service.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/factory.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/localization.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/state.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/time.adapter.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/toolbar-actions.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/typeahead.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/validation.util.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/lib/ui-extensions-testing.module.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/public-api.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/ssng-package.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/jest.config.js create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/ng-package.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/package.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/bounce.animations.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/collapse.animations.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/fade.animations.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/modal.animations.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/slide.animations.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/toast.animations.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/button/button.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loading/loading.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-close.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-container.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-ref.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table-empty-message/table-empty-message.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.html create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.scss create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/scripts.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/styles.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/validation.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ellipsis.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/loading.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/table-sort.directive.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/route-names.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/error.handler.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/lazy-style.handler.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/common.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/confirmation.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/nav-item.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/statistics.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/toaster.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/ng-bootstrap-config.provider.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/route.provider.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/confirmation.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/modal.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/nav-items.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/page-alert.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/toaster.service.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/append-content.token.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/breadcrumb.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/button.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/chart.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/confirmation.service.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/ellipsis.directive.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.handler.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/lazy-style.handler.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loader-bar.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loading.directive.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal-container.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.service.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table-sort.directive.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table.component.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/toaster.service.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/validation-utils.spec.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/theme-shared.module.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/append-content.token.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/http-error.token.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/lazy-styles.token.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/ngx-datatable-messages.token.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/date-parser-formatter.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/validation-utils.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/widget-utils.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/public-api.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/src/test-setup.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/styles/bootstrap-rtl.min.css create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/ng-package.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/config.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/index.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/theme-shared-testing.module.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/public-api.ts create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.prod.json create mode 100644 npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.spec.json diff --git a/npm/ng-packs/nx/ng-packs/angular.json b/npm/ng-packs/nx/ng-packs/angular.json index b8ff6319a1..bee1d52477 100644 --- a/npm/ng-packs/nx/ng-packs/angular.json +++ b/npm/ng-packs/nx/ng-packs/angular.json @@ -8,9 +8,9 @@ "prefix": "abp", "architect": { "build": { - "builder": "@nrwl/angular:ng-packagr-lite", - "outputs": ["dist/libs/core"], + "builder": "@angular-devkit/build-ng-packagr:build", "options": { + "tsConfig": "libs/core/tsconfig.lib.json", "project": "libs/core/ng-package.json" }, "configurations": { @@ -160,6 +160,88 @@ } } } + }, + "theme-basic": { + "projectType": "library", + "root": "libs/theme-basic", + "sourceRoot": "libs/theme-basic/src", + "prefix": "abp", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "libs/theme-basic/tsconfig.lib.json", + "project": "libs/theme-basic/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/theme-basic/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/theme-basic/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/theme-basic"], + "options": { + "jestConfig": "libs/theme-basic/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/theme-basic/src/**/*.ts", + "libs/theme-basic/src/**/*.html" + ] + } + } + } + }, + "theme-shared": { + "projectType": "library", + "root": "libs/theme-shared", + "sourceRoot": "libs/theme-shared/src", + "prefix": "abp", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "tsConfig": "libs/theme-shared/tsconfig.lib.json", + "project": "libs/theme-shared/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/theme-shared/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/theme-shared/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/theme-shared"], + "options": { + "jestConfig": "libs/theme-shared/jest.config.js", + "passWithNoTests": true + } + }, + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/theme-shared/src/**/*.ts", + "libs/theme-shared/src/**/*.html" + ] + } + } + } } }, "cli": { @@ -186,7 +268,8 @@ "@nrwl/angular:library": { "style": "scss", "linter": "eslint", - "unitTestRunner": "jest" + "unitTestRunner": "jest", + "strict": false }, "@nrwl/angular:component": { "style": "scss" diff --git a/npm/ng-packs/nx/ng-packs/libs/core/locale/ng-package.json b/npm/ng-packs/nx/ng-packs/libs/core/locale/ng-package.json index f2542d13fb..2ebc715ba0 100644 --- a/npm/ng-packs/nx/ng-packs/libs/core/locale/ng-package.json +++ b/npm/ng-packs/nx/ng-packs/libs/core/locale/ng-package.json @@ -1,6 +1,6 @@ { "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/core/locale", + "dest": "../../../dist/libs/core/locale", "lib": { "entryFile": "src/public-api.ts" } diff --git a/npm/ng-packs/nx/ng-packs/libs/core/ng-package.json b/npm/ng-packs/nx/ng-packs/libs/core/ng-package.json index 7ebb0a5287..670a0ac99d 100644 --- a/npm/ng-packs/nx/ng-packs/libs/core/ng-package.json +++ b/npm/ng-packs/nx/ng-packs/libs/core/ng-package.json @@ -2,7 +2,7 @@ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../dist/libs/core", "lib": { - "entryFile": "src/index.ts" + "entryFile": "src/public-api.ts" }, "allowedNonPeerDependencies": [ "@abp/utils", diff --git a/npm/ng-packs/nx/ng-packs/libs/core/src/index.ts b/npm/ng-packs/nx/ng-packs/libs/core/src/public-api.ts similarity index 100% rename from npm/ng-packs/nx/ng-packs/libs/core/src/index.ts rename to npm/ng-packs/nx/ng-packs/libs/core/src/public-api.ts diff --git a/npm/ng-packs/nx/ng-packs/libs/core/testing/ng-package.json b/npm/ng-packs/nx/ng-packs/libs/core/testing/ng-package.json index e647bd0d3e..45fdf674ea 100644 --- a/npm/ng-packs/nx/ng-packs/libs/core/testing/ng-package.json +++ b/npm/ng-packs/nx/ng-packs/libs/core/testing/ng-package.json @@ -1,6 +1,6 @@ { "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/core/testing", + "dest": "../../../dist/libs/core/testing", "lib": { "entryFile": "src/public-api.ts" } diff --git a/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.json b/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.json index d2b6463421..692cc17b32 100644 --- a/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.json +++ b/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.json @@ -11,15 +11,6 @@ } ], "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "strict": false, - "noImplicitReturns": false, - "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, - }, - "angularCompilerOptions": { - "strictInjectionParameters": true, - "strictInputAccessModifiers": false, - "strictTemplates": false } } diff --git a/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.lib.prod.json b/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.lib.prod.json index 1bcbd8c948..7f09086ce4 100644 --- a/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.lib.prod.json +++ b/npm/ng-packs/nx/ng-packs/libs/core/tsconfig.lib.prod.json @@ -5,7 +5,7 @@ "declarationMap": false, }, "angularCompilerOptions": { - "compilationMode": "partial" - // "enableIvy": false + "compilationMode": "partial", + "enableIvy": false } } diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/.eslintrc.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/.eslintrc.json new file mode 100644 index 0000000000..9c51584494 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "abp", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "abp", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": {} + } + ] +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/README.md b/npm/ng-packs/nx/ng-packs/libs/theme-shared/README.md new file mode 100644 index 0000000000..0578cf669d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/README.md @@ -0,0 +1,7 @@ +# theme-shared + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test theme-shared` to execute the unit tests. diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/ng-package.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/ng-package.json new file mode 100644 index 0000000000..345e5ff42e --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/libs/theme-shared/extensions", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts new file mode 100644 index 0000000000..11da774d1b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; + +@Injectable() +export class DateTimeAdapter { + value: NgbDateTimeStruct; + + fromModel(value: string | Date): NgbDateTimeStruct | null { + if (!value) return null; + + const date = new Date(value); + + if (isNaN((date as unknown) as number)) return null; + + this.value = { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + }; + + return this.value; + } + + toModel(value: NgbDateTimeStruct | null): string { + if (!value) return ''; + + const now = new Date(); + + value = { + year: now.getUTCFullYear(), + month: now.getMonth() + 1, + day: now.getDate(), + hour: 0, + minute: 0, + second: 0, + ...this.value, + ...value, + }; + + const date = new Date( + value.year, + value.month - 1, + value.day, + value.hour, + value.minute, + value.second, + ); + + return new Date(date).toISOString(); + } +} + +type NgbDateTimeStruct = NgbDateStruct & NgbTimeStruct; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date.adapter.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date.adapter.ts new file mode 100644 index 0000000000..17db36415e --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/date.adapter.ts @@ -0,0 +1,40 @@ +import { formatDate } from '@angular/common'; +import { Injectable } from '@angular/core'; +import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; + +@Injectable() +export class DateAdapter extends NgbDateAdapter { + fromModel(value: string | Date): NgbDateStruct | null { + if (!value) return null; + + let date: Date; + + if (typeof value === 'string') { + date = this.dateOf(value); + } else { + date = new Date(value); + } + + if (isNaN(date as unknown as number)) return null; + + return { + day: date.getDate(), + month: date.getMonth() + 1, + year: date.getFullYear(), + }; + } + + toModel(value: NgbDateStruct | null): string { + if (!value) return ''; + + const date = new Date(value.year, value.month - 1, value.day); + const formattedDate = formatDate(date, 'yyyy-MM-dd', 'en'); + + return formattedDate; + } + + protected dateOf(value: string): Date { + const dateUtc = new Date(Date.parse(value)); + return new Date(dateUtc.getTime() + Math.abs(dateUtc.getTimezoneOffset() * 60000)); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/time.adapter.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/time.adapter.ts new file mode 100644 index 0000000000..33c25ddd8c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/adapters/time.adapter.ts @@ -0,0 +1,35 @@ +import { formatDate } from '@angular/common'; +import { Injectable } from '@angular/core'; +import { NgbTimeAdapter, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; + +@Injectable() +export class TimeAdapter extends NgbTimeAdapter { + fromModel(value: string | Date): NgbTimeStruct | null { + if (!value) return null; + + const date = isTimeStr(value) + ? new Date(0, 0, 1, ...value.split(':').map(Number)) + : new Date(value); + + if (isNaN((date as unknown) as number)) return null; + + return { + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + }; + } + + toModel(value: NgbTimeStruct | null): string { + if (!value) return ''; + + const date = new Date(0, 0, 1, value.hour, value.minute, value.second); + const formattedDate = formatDate(date, 'HH:mm', 'en'); + + return formattedDate; + } +} + +function isTimeStr(value: string | Date): value is string { + return /^((2[123])|[01][0-9])(\:[0-5][0-9]){1,2}$/.test(String(value)); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts new file mode 100644 index 0000000000..14d680059a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts @@ -0,0 +1,32 @@ +import { Directive, Injector, Input } from '@angular/core'; +import { ActionData, ActionList } from '../../models/actions'; +import { ExtensionsService } from '../../services/extensions.service'; +import { EXTENSIONS_ACTION_TYPE, EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; + +// tslint:disable: directive-class-suffix +// Fix for https://github.com/angular/angular/issues/23904 +// @dynamic +@Directive() +export abstract class AbstractActionsComponent> extends ActionData< + InferredRecord +> { + readonly actionList: L; + + readonly getInjected: InferredData['getInjected']; + + @Input() readonly record: InferredData['record']; + + constructor(injector: Injector) { + super(); + + // tslint:disable-next-line + this.getInjected = injector.get.bind(injector); + const extensions = injector.get(ExtensionsService); + const name = injector.get(EXTENSIONS_IDENTIFIER); + const type = injector.get(EXTENSIONS_ACTION_TYPE); + this.actionList = (extensions[type].get(name).actions as unknown) as L; + } +} + +type InferredData = ActionData>; +type InferredRecord = L extends ActionList ? R : never; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts new file mode 100644 index 0000000000..7890e65b36 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts @@ -0,0 +1,76 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + Optional, + SkipSelf, + ViewChild, +} from '@angular/core'; +import { ControlContainer } from '@angular/forms'; +import { + NgbDateAdapter, + NgbInputDatepicker, + NgbTimeAdapter, + NgbTimepicker, +} from '@ng-bootstrap/ng-bootstrap'; +import { DateTimeAdapter } from '../../adapters/date-time.adapter'; +import { FormProp } from '../../models/form-props'; +import { selfFactory } from '../../utils/factory.util'; + +@Component({ + exportAs: 'abpDateTimePicker', + selector: 'abp-date-time-picker', + template: ` + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + { + provide: ControlContainer, + useFactory: selfFactory, + deps: [[new Optional(), new SkipSelf(), ControlContainer]], + }, + { + provide: NgbDateAdapter, + useClass: DateTimeAdapter, + }, + { + provide: NgbTimeAdapter, + useClass: DateTimeAdapter, + }, + ], +}) +export class DateTimePickerComponent { + @Input() prop: FormProp; + @Input() meridian = false; + + @ViewChild(NgbInputDatepicker) date: NgbInputDatepicker; + @ViewChild(NgbTimepicker) time: NgbTimepicker; + + constructor(public readonly cdRef: ChangeDetectorRef) {} + + setDate(datestr: string) { + this.date.writeValue(datestr); + } + + setTime(datestr: string) { + this.time.writeValue(datestr); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html new file mode 100644 index 0000000000..a197405837 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html @@ -0,0 +1,138 @@ +
+ + + + + + + + + + +
+ + +
+
+ + + + + + + + + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts new file mode 100644 index 0000000000..0f792e8578 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts @@ -0,0 +1,193 @@ +import { ABP, AbpValidators, ConfigStateService, TrackByService } from '@abp/ng.core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnChanges, + Optional, + SimpleChanges, + SkipSelf, + ViewChild, +} from '@angular/core'; +import { + ControlContainer, + FormGroup, + FormGroupDirective, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { Observable, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; +import snq from 'snq'; +import { DateAdapter } from '../../adapters/date.adapter'; +import { TimeAdapter } from '../../adapters/time.adapter'; +import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; +import { ePropType } from '../../enums/props.enum'; +import { FormProp } from '../../models/form-props'; +import { PropData } from '../../models/props'; +import { selfFactory } from '../../utils/factory.util'; +import { addTypeaheadTextSuffix } from '../../utils/typeahead.util'; + +@Component({ + selector: 'abp-extensible-form-prop', + templateUrl: './extensible-form-prop.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + { + provide: ControlContainer, + useFactory: selfFactory, + deps: [[new Optional(), new SkipSelf(), ControlContainer]], + }, + { provide: NgbDateAdapter, useClass: DateAdapter }, + { provide: NgbTimeAdapter, useClass: TimeAdapter }, + ], +}) +export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit { + @Input() data: PropData; + + @Input() prop: FormProp; + + @Input() first: boolean; + + @ViewChild('field') private fieldRef: ElementRef; + + asterisk = ''; + + options$: Observable[]> = of([]); + + validators: ValidatorFn[] = []; + + readonly: boolean; + + disabled: boolean; + + private readonly form: FormGroup; + + typeaheadModel: any; + + setTypeaheadValue(selectedOption: ABP.Option) { + this.typeaheadModel = selectedOption || { key: null, value: null }; + const { key, value } = this.typeaheadModel; + const [keyControl, valueControl] = this.getTypeaheadControls(); + if (valueControl.value && !value) valueControl.markAsDirty(); + keyControl.setValue(key); + valueControl.setValue(value); + } + + search = (text$: Observable) => + text$ + ? text$.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap(text => this.prop.options(this.data, text)), + ) + : of([]); + + typeaheadFormatter = (option: ABP.Option) => option.key; + + get meridian() { + return ( + this.configState.getDeep('localization.currentCulture.dateTimeFormat.shortTimePattern') || '' + ).includes('tt'); + } + + get isInvalid() { + const control = this.form.get(this.prop.name); + return control.touched && control.invalid; + } + + constructor( + public readonly cdRef: ChangeDetectorRef, + public readonly track: TrackByService, + protected configState: ConfigStateService, + groupDirective: FormGroupDirective, + ) { + this.form = groupDirective.form; + } + + private getTypeaheadControls() { + const { name } = this.prop; + const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`; + const keyControl = + this.form.get(addTypeaheadTextSuffix(extraPropName)) || + this.form.get(addTypeaheadTextSuffix(name)); + const valueControl = this.form.get(extraPropName) || this.form.get(name); + return [keyControl, valueControl]; + } + + private setAsterisk() { + this.asterisk = this.validators.some(isRequired) ? '*' : ''; + } + + ngAfterViewInit() { + if (this.first && this.fieldRef) { + this.fieldRef.nativeElement.focus(); + } + } + + getComponent(prop: FormProp): string { + switch (prop.type) { + case ePropType.Boolean: + return 'checkbox'; + case ePropType.Date: + return 'date'; + case ePropType.DateTime: + return 'dateTime'; + case ePropType.Hidden: + return 'hidden'; + case ePropType.MultiSelect: + return 'multiselect'; + case ePropType.Text: + return 'textarea'; + case ePropType.Time: + return 'time'; + case ePropType.Typeahead: + return 'typeahead'; + default: + return prop.options ? 'select' : 'input'; + } + } + + getType(prop: FormProp): string { + switch (prop.type) { + case ePropType.Date: + case ePropType.String: + return 'text'; + case ePropType.Boolean: + return 'checkbox'; + case ePropType.Number: + return 'number'; + case ePropType.Email: + return 'email'; + case ePropType.Password: + return 'password'; + default: + return 'hidden'; + } + } + + ngOnChanges({ prop }: SimpleChanges) { + const currentProp = snq(() => prop.currentValue); + const { options, readonly, disabled, validators } = currentProp || {}; + + if (options) this.options$ = options(this.data); + if (readonly) this.readonly = readonly(this.data); + if (disabled) this.disabled = disabled(this.data); + if (validators) { + this.validators = validators(this.data); + this.setAsterisk(); + } + + const [keyControl, valueControl] = this.getTypeaheadControls(); + if (keyControl && valueControl) + this.typeaheadModel = { key: keyControl.value, value: valueControl.value }; + } +} + +function isRequired(validator: ValidatorFn) { + return validator === Validators.required || validator === AbpValidators.required; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html new file mode 100644 index 0000000000..259538820b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts new file mode 100644 index 0000000000..f15f173faa --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts @@ -0,0 +1,64 @@ +import { TrackByService } from '@abp/ng.core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + Input, + Optional, + QueryList, + SkipSelf, + ViewChildren, +} from '@angular/core'; +import { ControlContainer, FormGroup } from '@angular/forms'; +import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; +import { FormPropList } from '../../models/form-props'; +import { ExtensionsService } from '../../services/extensions.service'; +import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; +import { selfFactory } from '../../utils/factory.util'; +import { ExtensibleFormPropComponent } from './extensible-form-prop.component'; + +@Component({ + exportAs: 'abpExtensibleForm', + selector: 'abp-extensible-form', + templateUrl: './extensible-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + { + provide: ControlContainer, + useFactory: selfFactory, + deps: [[new Optional(), new SkipSelf(), ControlContainer]], + }, + ], +}) +export class ExtensibleFormComponent { + @ViewChildren(ExtensibleFormPropComponent) + formProps: QueryList; + + @Input() + set selectedRecord(record: R) { + const type = !record || JSON.stringify(record) === '{}' ? 'create' : 'edit'; + this.propList = this.extensions[`${type}FormProps`].get(this.identifier).props; + this.record = record; + } + + extraPropertiesKey = EXTRA_PROPERTIES_KEY; + propList: FormPropList; + record: R; + + get form(): FormGroup { + return (this.container ? this.container.control : { controls: {} }) as FormGroup; + } + + get extraProperties(): FormGroup { + return (this.form.controls.extraProperties || { controls: {} }) as FormGroup; + } + + constructor( + public readonly cdRef: ChangeDetectorRef, + public readonly track: TrackByService, + private container: ControlContainer, + private extensions: ExtensionsService, + @Inject(EXTENSIONS_IDENTIFIER) private identifier: string, + ) {} +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html new file mode 100644 index 0000000000..81e2845ac0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + +
+
+
+
+
+
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts new file mode 100644 index 0000000000..6aea101dc7 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts @@ -0,0 +1,145 @@ +import { + ListService, + ConfigStateService, + getShortDateFormat, + getShortDateShortTimeFormat, + getShortTimeFormat, + PermissionService, +} from '@abp/ng.core'; +import { formatDate } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Inject, + Injector, + Input, + LOCALE_ID, + TemplateRef, + TrackByFunction, + Type, + InjectionToken, + InjectFlags, + SimpleChanges, + OnChanges, +} from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ePropType } from '../../enums/props.enum'; +import { EntityProp, EntityPropList } from '../../models/entity-props'; +import { PropData } from '../../models/props'; +import { ExtensionsService } from '../../services/extensions.service'; +import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; +import { EntityActionList } from '../../models/entity-actions'; +const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; + +@Component({ + exportAs: 'abpExtensibleTable', + selector: 'abp-extensible-table', + templateUrl: './extensible-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExtensibleTableComponent implements OnChanges { + protected _actionsText: string; + @Input() + set actionsText(value: string) { + this._actionsText = value; + } + get actionsText(): string { + return this._actionsText ?? (this.actionList.length > 1 ? 'AbpUi::Actions' : ''); + } + + @Input() data: R[]; + @Input() list: ListService; + @Input() recordsTotal: number; + @Input() set actionsColumnWidth(width: number) { + this.setColumnWidths(width ? Number(width) : undefined); + } + @Input() actionsTemplate: TemplateRef; + + getInjected: (token: Type | InjectionToken, notFoundValue?: T, flags?: InjectFlags) => T; + + readonly columnWidths: number[]; + + readonly propList: EntityPropList; + + readonly actionList: EntityActionList; + + readonly trackByFn: TrackByFunction> = (_, item) => item.name; + + hasAtLeastOnePermittedAction: boolean; + + constructor( + @Inject(LOCALE_ID) private locale: string, + private config: ConfigStateService, + injector: Injector, + ) { + // tslint:disable-next-line + this.getInjected = injector.get.bind(injector); + const extensions = injector.get(ExtensionsService); + const name = injector.get(EXTENSIONS_IDENTIFIER); + this.propList = extensions.entityProps.get(name).props; + this.actionList = extensions['entityActions'].get(name) + .actions as unknown as EntityActionList; + + const permissionService = injector.get(PermissionService); + this.hasAtLeastOnePermittedAction = + permissionService.filterItemsByPolicy( + this.actionList.toArray().map(action => ({ requiredPolicy: action.permission })), + ).length > 0; + this.setColumnWidths(DEFAULT_ACTIONS_COLUMN_WIDTH); + } + + private setColumnWidths(actionsColumn: number) { + const widths = [actionsColumn]; + this.propList.forEach(({ value: prop }) => { + widths.push(prop.columnWidth); + }); + (this.columnWidths as any) = widths; + } + + private getDate(value: Date, format: string) { + return value ? formatDate(value, format, this.locale) : ''; + } + + private getIcon(value: boolean) { + return value + ? '
' + : '
'; + } + + getContent(prop: EntityProp, data: PropData): Observable { + return prop.valueResolver(data).pipe( + map(value => { + switch (prop.type) { + case ePropType.Boolean: + return this.getIcon(value); + case ePropType.Date: + return this.getDate(value, getShortDateFormat(this.config)); + case ePropType.Time: + return this.getDate(value, getShortTimeFormat(this.config)); + case ePropType.DateTime: + return this.getDate(value, getShortDateShortTimeFormat(this.config)); + default: + return value; + // More types can be handled in the future + } + }), + ); + } + + ngOnChanges({ data }: SimpleChanges) { + if (!data?.currentValue) return; + + this.data = data.currentValue.map((record, index) => { + this.propList.forEach(prop => { + const propData = { getInjected: this.getInjected, record, index } as any; + record[`_${prop.value.name}`] = { + visible: prop.value.visible(propData), + value: this.getContent(prop.value, propData), + }; + }); + + return record; + }); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html new file mode 100644 index 0000000000..6172cacd20 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html @@ -0,0 +1,43 @@ +
+ +
+ + +
+
+ + + + + + + + diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts new file mode 100644 index 0000000000..30d36699b6 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts @@ -0,0 +1,36 @@ +import { + ChangeDetectionStrategy, + Component, + Injector, + Input, + TrackByFunction, +} from '@angular/core'; +import { EntityAction, EntityActionList } from '../../models/entity-actions'; +import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; +import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; + +@Component({ + exportAs: 'abpGridActions', + selector: 'abp-grid-actions', + templateUrl: './grid-actions.component.html', + providers: [ + { + provide: EXTENSIONS_ACTION_TYPE, + useValue: 'entityActions', + }, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GridActionsComponent extends AbstractActionsComponent> { + @Input() icon = 'fa fa-cog'; + + @Input() readonly index: number; + + @Input() text = ''; + + readonly trackByFn: TrackByFunction> = (_, item) => item.text; + + constructor(injector: Injector) { + super(injector); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html new file mode 100644 index 0000000000..38be2289d7 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html @@ -0,0 +1,20 @@ +
+
+ + + + + + + + + + + +
+
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts new file mode 100644 index 0000000000..dfd7c0105d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts @@ -0,0 +1,49 @@ +import { + ChangeDetectionStrategy, + Component, + InjectFlags, + InjectionToken, + Injector, + TrackByFunction, + Type, +} from '@angular/core'; +import { ToolbarActionList, ToolbarComponent } from '../../models/toolbar-actions'; +import { + EXTENSIONS_ACTION_CALLBACK, + EXTENSIONS_ACTION_DATA, + EXTENSIONS_ACTION_TYPE, +} from '../../tokens/extensions.token'; +import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; + +@Component({ + exportAs: 'abpPageToolbar', + selector: 'abp-page-toolbar', + templateUrl: './page-toolbar.component.html', + providers: [ + { + provide: EXTENSIONS_ACTION_TYPE, + useValue: 'toolbarActions', + }, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PageToolbarComponent extends AbstractActionsComponent> { + readonly trackByFn: TrackByFunction> = (_, item) => + item.action || item.component; + + constructor(private readonly injector: Injector) { + super(injector); + } + + createInjector(action: ToolbarComponent): Injector { + const get = (token: Type | InjectionToken, notFoundValue?: T, flags?: InjectFlags) => { + return token === EXTENSIONS_ACTION_DATA + ? this.data + : token === EXTENSIONS_ACTION_CALLBACK + ? (data = this.data) => action.action(data) + : this.getInjected.call(this.injector, token, notFoundValue, flags); + }; + + return { get }; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/constants/extra-properties.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/constants/extra-properties.ts new file mode 100644 index 0000000000..e46e633e07 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/constants/extra-properties.ts @@ -0,0 +1 @@ +export const EXTRA_PROPERTIES_KEY = 'extraProperties'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/disabled.directive.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/disabled.directive.ts new file mode 100644 index 0000000000..da3b985666 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/disabled.directive.ts @@ -0,0 +1,19 @@ +import { Directive, Host, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { NgControl } from '@angular/forms'; + +@Directive({ + selector: '[abpDisabled]', +}) +export class DisabledDirective implements OnChanges { + @Input() + abpDisabled: boolean; + + constructor(@Host() private ngControl: NgControl) {} + + // Related issue: https://github.com/angular/angular/issues/35330 + ngOnChanges({ abpDisabled }: SimpleChanges) { + if (this.ngControl.control && abpDisabled) { + this.ngControl.control[abpDisabled.currentValue ? 'disable' : 'enable'](); + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/prop-data.directive.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/prop-data.directive.ts new file mode 100644 index 0000000000..8de502f14b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/directives/prop-data.directive.ts @@ -0,0 +1,54 @@ +import { + Directive, + Injector, + Input, + OnChanges, + OnDestroy, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { PropData, PropList } from '../models/props'; + +@Directive({ + exportAs: 'abpPropData', + selector: '[abpPropData]', +}) +export class PropDataDirective> extends PropData> + implements OnChanges, OnDestroy { + /* tslint:disable:no-input-rename */ + @Input('abpPropDataFromList') readonly propList: L; + + @Input('abpPropDataWithRecord') readonly record: InferredData['record']; + + @Input('abpPropDataAtIndex') readonly index: number; + /* tslint:enable:no-input-rename */ + + readonly getInjected: InferredData['getInjected']; + + constructor( + private tempRef: TemplateRef, + private vcRef: ViewContainerRef, + injector: Injector, + ) { + super(); + + // tslint:disable-next-line + this.getInjected = injector.get.bind(injector); + } + + ngOnChanges() { + this.vcRef.clear(); + + this.vcRef.createEmbeddedView(this.tempRef, { + $implicit: this.data, + index: 0, + }); + } + + ngOnDestroy() { + this.vcRef.clear(); + } +} + +type InferredData = PropData>; +type InferredRecord = L extends PropList ? R : never; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/enums/props.enum.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/enums/props.enum.ts new file mode 100644 index 0000000000..27cb4990a6 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/enums/props.enum.ts @@ -0,0 +1,15 @@ +export const enum ePropType { + Boolean = 'boolean', + Date = 'date', + DateTime = 'dateTime', + Email = 'email', + Enum = 'enum', + Hidden = 'hidden', + MultiSelect = 'multiselect', + Number = 'number', + Password = 'password', + String = 'string', + Text = 'text', + Time = 'time', + Typeahead = 'typeahead', +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/actions.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/actions.ts new file mode 100644 index 0000000000..4b1d1bd740 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/actions.ts @@ -0,0 +1,79 @@ +/* tslint:disable:variable-name */ +import { LinkedList } from '@abp/utils'; +import { InjectFlags, InjectionToken, Type } from '@angular/core'; +import { O } from 'ts-toolbelt'; + +export abstract class ActionList> extends LinkedList {} + +export abstract class ActionData { + abstract getInjected: ( + token: Type | InjectionToken, + notFoundValue?: T, + flags?: InjectFlags, + ) => T; + index?: number; + abstract record: R; + + get data(): ReadonlyActionData { + return { + getInjected: this.getInjected, + index: this.index, + record: this.record, + }; + } +} + +export type ReadonlyActionData = O.Readonly, 'data'>>; + +export abstract class Action { + constructor( + public readonly permission: string, + public readonly visible: ActionPredicate = _ => true, + public readonly action: ActionCallback = _ => {}, + ) {} +} + +export type ActionCallback = (data?: Omit, 'data'>) => R; +export type ActionPredicate = (data?: Omit, 'data'>) => boolean; + +export abstract class ActionsFactory> { + protected abstract _ctor: Type; + private contributorCallbacks: ActionContributorCallbacks> = {}; + + get(name: string): C { + this.contributorCallbacks[name] = this.contributorCallbacks[name] || []; + + return new this._ctor(this.contributorCallbacks[name]); + } +} + +export abstract class Actions { + protected abstract _ctor: Type; + + get actions(): L { + const actionList = new this._ctor(); + + this.callbackList.forEach(callback => callback(actionList)); + + return actionList; + } + + constructor(private readonly callbackList: ActionContributorCallback[]) {} + + addContributor(contributeCallback: ActionContributorCallback) { + this.callbackList.push(contributeCallback); + } + + clearContributors() { + while (this.callbackList.length) this.callbackList.pop(); + } +} + +export type ActionContributorCallbacks> = Record< + string, + ActionContributorCallback[] +>; + +export type ActionContributorCallback> = (actionList: L) => any; + +type InferredActionList = C extends Actions ? L : never; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-actions.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-actions.ts new file mode 100644 index 0000000000..4938e10337 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-actions.ts @@ -0,0 +1,53 @@ +/* tslint:disable:variable-name */ +import { Type } from '@angular/core'; +import { O } from 'ts-toolbelt'; +import { + Action, + ActionContributorCallback, + ActionContributorCallbacks, + ActionList, + Actions, + ActionsFactory, +} from './actions'; + +export class EntityActionList extends ActionList> {} + +export class EntityActions extends Actions> { + protected _ctor: Type> = EntityActionList; +} + +export class EntityActionsFactory extends ActionsFactory> { + protected _ctor: Type> = EntityActions; +} + +export class EntityAction extends Action { + readonly text: string; + readonly icon: string; + + constructor(options: EntityActionOptions) { + super(options.permission, options.visible, options.action); + this.text = options.text; + this.icon = options.icon || ''; + } + + static create(options: EntityActionOptions) { + return new EntityAction(options); + } + + static createMany(arrayOfOptions: EntityActionOptions[]) { + return arrayOfOptions.map(EntityAction.create); + } +} + +export type EntityActionOptions = O.Optional< + O.Writable>, + 'permission' | 'visible' | 'icon' +>; + +export type EntityActionDefaults = Record[]>; +export type EntityActionContributorCallback = ActionContributorCallback< + EntityActionList +>; +export type EntityActionContributorCallbacks = ActionContributorCallbacks< + EntityActionList +>; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-props.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-props.ts new file mode 100644 index 0000000000..84ae0c360f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/entity-props.ts @@ -0,0 +1,71 @@ +/* tslint:disable:variable-name */ +import { Type } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { O } from 'ts-toolbelt'; +import { + Prop, + PropCallback, + PropContributorCallback, + PropContributorCallbacks, + PropList, + Props, + PropsFactory, +} from './props'; +import { ActionCallback } from './actions'; + +export class EntityPropList extends PropList> {} + +export class EntityProps extends Props> { + protected _ctor: Type> = EntityPropList; +} + +export class EntityPropsFactory extends PropsFactory> { + protected _ctor: Type> = EntityProps; +} + +export class EntityProp extends Prop { + readonly columnWidth: number | undefined; + readonly sortable: boolean; + readonly valueResolver: PropCallback>; + readonly action: ActionCallback; + + constructor(options: EntityPropOptions) { + super( + options.type, + options.name, + options.displayName, + options.permission, + options.visible, + options.isExtra, + ); + + this.columnWidth = options.columnWidth; + this.sortable = options.sortable || false; + this.valueResolver = options.valueResolver || (data => of(data.record[this.name])); + this.action = options.action; + } + + static create(options: EntityPropOptions) { + return new EntityProp(options); + } + + static createMany(arrayOfOptions: EntityPropOptions[]) { + return arrayOfOptions.map(EntityProp.create); + } +} + +export type EntityPropOptions = O.Optional< + O.Writable>, + | 'permission' + | 'visible' + | 'displayName' + | 'isExtra' + | 'columnWidth' + | 'sortable' + | 'valueResolver' + | 'action' +>; + +export type EntityPropDefaults = Record[]>; +export type EntityPropContributorCallback = PropContributorCallback>; +export type EntityPropContributorCallbacks = PropContributorCallbacks>; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/form-props.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/form-props.ts new file mode 100644 index 0000000000..f9e6dca9ff --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/form-props.ts @@ -0,0 +1,109 @@ +/* tslint:disable:variable-name */ +import { ABP } from '@abp/ng.core'; +import { Injector, Type } from '@angular/core'; +import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { O } from 'ts-toolbelt'; +import { + Prop, + PropCallback, + PropContributorCallback, + PropContributorCallbacks, + PropData, + PropList, + PropPredicate, + Props, + PropsFactory, +} from './props'; + +export class FormPropList extends PropList> {} + +export class FormProps extends Props> { + protected _ctor: Type> = FormPropList; +} + +export class CreateFormPropsFactory extends PropsFactory> { + protected _ctor: Type> = FormProps; +} + +export class EditFormPropsFactory extends PropsFactory> { + protected _ctor: Type> = FormProps; +} + +export class FormProp extends Prop { + readonly validators: PropCallback; + readonly asyncValidators: PropCallback; + readonly disabled: PropPredicate; + readonly readonly: PropPredicate; + readonly autocomplete: string; + readonly defaultValue: boolean | number | string | Date; + readonly options: PropCallback[]>> | undefined; + readonly id: string | undefined; + + constructor(options: FormPropOptions) { + super( + options.type, + options.name, + options.displayName, + options.permission, + options.visible, + options.isExtra, + ); + + this.asyncValidators = options.asyncValidators || (_ => []); + this.validators = options.validators || (_ => []); + this.disabled = options.disabled || (_ => false); + this.readonly = options.readonly || (_ => false); + this.autocomplete = options.autocomplete || 'off'; + this.options = options.options; + this.id = options.id || options.name; + const defaultValue = options.defaultValue; + this.defaultValue = isFalsyValue(defaultValue) ? defaultValue : defaultValue || null; + } + + static create(options: FormPropOptions) { + return new FormProp(options); + } + + static createMany(arrayOfOptions: FormPropOptions[]) { + return arrayOfOptions.map(FormProp.create); + } +} + +export class FormPropData extends PropData { + getInjected: PropData['getInjected']; + + constructor(injector: Injector, public readonly record: R) { + super(); + + // tslint:disable-next-line + this.getInjected = injector.get.bind(injector); + } +} + +export type FormPropOptions = O.Optional< + O.Writable>, + | 'permission' + | 'visible' + | 'displayName' + | 'isExtra' + | 'validators' + | 'asyncValidators' + | 'disabled' + | 'readonly' + | 'autocomplete' + | 'defaultValue' + | 'options' + | 'id' +>; + +export type CreateFormPropDefaults = Record[]>; +export type CreateFormPropContributorCallback = PropContributorCallback>; +export type CreateFormPropContributorCallbacks = PropContributorCallbacks>; +export type EditFormPropDefaults = Record[]>; +export type EditFormPropContributorCallback = PropContributorCallback>; +export type EditFormPropContributorCallbacks = PropContributorCallbacks>; + +function isFalsyValue(defaultValue: FormProp['defaultValue']): boolean { + return [0, '', false].indexOf(defaultValue as any) > -1; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/internal/object-extensions.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/internal/object-extensions.ts new file mode 100644 index 0000000000..776785ed4c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/internal/object-extensions.ts @@ -0,0 +1,108 @@ +import { ePropType } from '../../enums/props.enum'; +import { EntityPropList } from '../entity-props'; +import { FormPropList } from '../form-props'; +import { PropContributorCallbacks } from '../props'; + +export type DisplayNameGeneratorFn = ( + displayName: LocalizableStringDto, + fallback: LocalizableStringDto, +) => string; + +export type EntityExtensions = Record; + +export interface EntityExtensionDto { + properties: EntityExtensionProperties; + configuration: Record; +} + +export type EntityExtensionProperties = Record; + +export interface ExtensionEnumDto { + fields: ExtensionEnumFieldDto[]; + localizationResource?: string; + transformed?: any; +} + +export interface ExtensionEnumFieldDto { + name?: string; + value: any; +} + +export interface ExtensionPropertyApiCreateDto { + isAvailable: boolean; +} + +export interface ExtensionPropertyApiDto { + onGet: ExtensionPropertyApiGetDto; + onCreate: ExtensionPropertyApiCreateDto; + onUpdate: ExtensionPropertyApiUpdateDto; +} + +export interface ExtensionPropertyApiGetDto { + isAvailable: boolean; +} + +export interface ExtensionPropertyApiUpdateDto { + isAvailable: boolean; +} + +export interface ExtensionPropertyAttributeDto { + typeSimple?: string; + config: Record; +} + +export interface ExtensionPropertyDto { + type?: string; + typeSimple?: ePropType; + displayName: LocalizableStringDto; + api: ExtensionPropertyApiDto; + ui: ExtensionPropertyUiDto; + attributes: ExtensionPropertyAttributeDto[]; + configuration: Record; + defaultValue: any; +} + +export interface ExtensionPropertyUiDto { + onTable: ExtensionPropertyUiTableDto; + onCreateForm: ExtensionPropertyUiFormDto; + onEditForm: ExtensionPropertyUiFormDto; + lookup?: ExtensionPropertyUiLookupDto; +} + +export interface ExtensionPropertyUiFormDto { + isVisible: boolean; +} + +export interface ExtensionPropertyUiLookupDto { + url?: string; + resultListPropertyName?: string; + displayPropertyName?: string; + valuePropertyName?: string; + filterParamName?: string; +} + +export interface ExtensionPropertyUiTableDto { + isSortable?: boolean; + isVisible: boolean; +} + +export interface LocalizableStringDto { + name?: string; + resource?: string; +} + +export interface ModuleExtensionDto { + entities: Record; + configuration: Record; +} + +export interface ObjectExtensionsDto { + modules: Record; + enums: Record; +} + +export interface PropContributors { + prop: PropContributorCallbacks>; + createForm: PropContributorCallbacks>; + editForm: PropContributorCallbacks>; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/object-extensions.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/object-extensions.ts new file mode 100644 index 0000000000..65bc2a7fdf --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/object-extensions.ts @@ -0,0 +1,3 @@ +import * as ObjectExtensions from './internal/object-extensions'; + +export { ObjectExtensions }; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/props.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/props.ts new file mode 100644 index 0000000000..ef37bee218 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/props.ts @@ -0,0 +1,85 @@ +/* tslint:disable:variable-name */ +import { LinkedList } from '@abp/utils'; +import { InjectFlags, InjectionToken, Type } from '@angular/core'; +import { O } from 'ts-toolbelt'; +import { ePropType } from '../enums/props.enum'; + +export abstract class PropList> extends LinkedList {} + +export abstract class PropData { + abstract getInjected: ( + token: Type | InjectionToken, + notFoundValue?: T, + flags?: InjectFlags, + ) => T; + index?: number; + abstract record: R; + + get data(): ReadonlyPropData { + return { + getInjected: this.getInjected, + index: this.index, + record: this.record, + }; + } +} + +export type ReadonlyPropData = O.Readonly, 'data'>>; + +export abstract class Prop { + constructor( + public readonly type: ePropType, + public readonly name: string, + public readonly displayName: string, + public readonly permission: string, + public readonly visible: PropPredicate = _ => true, + public readonly isExtra = false, + ) { + this.displayName = this.displayName || this.name; + } +} + +export type PropCallback = (data?: Omit, 'data'>, auxData?: any) => R; +export type PropPredicate = (data?: Omit, 'data'>, auxData?: any) => boolean; + +export abstract class PropsFactory> { + protected abstract _ctor: Type; + private contributorCallbacks: PropContributorCallbacks> = {}; + + get(name: string): C { + this.contributorCallbacks[name] = this.contributorCallbacks[name] || []; + + return new this._ctor(this.contributorCallbacks[name]); + } +} + +export abstract class Props { + protected abstract _ctor: Type; + + get props(): L { + const propList = new this._ctor(); + + this.callbackList.forEach(callback => callback(propList)); + + return propList; + } + + constructor(private readonly callbackList: PropContributorCallback[]) {} + + addContributor(contributeCallback: PropContributorCallback) { + this.callbackList.push(contributeCallback); + } + + clearContributors() { + while (this.callbackList.length) this.callbackList.pop(); + } +} + +export type PropContributorCallbacks> = Record< + string, + PropContributorCallback[] +>; + +export type PropContributorCallback> = (propList: L) => any; + +type InferredPropList = C extends Props ? L : never; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/toolbar-actions.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/toolbar-actions.ts new file mode 100644 index 0000000000..1a166b083d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/models/toolbar-actions.ts @@ -0,0 +1,78 @@ +/* tslint:disable:variable-name */ +import { Type } from '@angular/core'; +import { O } from 'ts-toolbelt'; +import { + Action, + ActionContributorCallback, + ActionContributorCallbacks, + ActionList, + Actions, + ActionsFactory, +} from './actions'; + +export class ToolbarActionList extends ActionList< + R, + ToolbarAction | ToolbarComponent +> {} + +export class ToolbarActions extends Actions> { + protected _ctor: Type> = ToolbarActionList; +} + +export class ToolbarActionsFactory extends ActionsFactory> { + protected _ctor: Type> = ToolbarActions; +} + +export class ToolbarAction extends Action { + readonly text: string; + readonly icon: string; + + constructor(options: ToolbarActionOptions) { + super(options.permission, options.visible, options.action); + this.text = options.text; + this.icon = options.icon || ''; + } + + static create(options: ToolbarActionOptions) { + return new ToolbarAction(options); + } + + static createMany(arrayOfOptions: ToolbarActionOptions[]) { + return arrayOfOptions.map(ToolbarAction.create); + } +} + +export class ToolbarComponent extends Action { + readonly component: Type; + + constructor(options: ToolbarComponentOptions) { + super(options.permission, options.visible, options.action); + this.component = options.component; + } + + static create(options: ToolbarComponentOptions) { + return new ToolbarComponent(options); + } + + static createMany(arrayOfOptions: ToolbarComponentOptions[]) { + return arrayOfOptions.map(ToolbarComponent.create); + } +} + +export type ToolbarActionOptions = O.Optional< + O.Writable>, + 'permission' | 'visible' | 'icon' +>; + +export type ToolbarComponentOptions = O.Optional< + O.Writable>, + 'permission' | 'visible' | 'action' +>; + +export type ToolbarActionDefaults = Record[]>; +export type ToolbarActionContributorCallback = ActionContributorCallback< + ToolbarActionList +>; +export type ToolbarActionContributorCallbacks = ActionContributorCallbacks< + ToolbarActionList +>; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/services/extensions.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/services/extensions.service.ts new file mode 100644 index 0000000000..e57a874ef8 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/services/extensions.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { EntityActionsFactory } from '../models/entity-actions'; +import { EntityPropsFactory } from '../models/entity-props'; +import { CreateFormPropsFactory, EditFormPropsFactory } from '../models/form-props'; +import { ToolbarActionsFactory } from '../models/toolbar-actions'; + +@Injectable({ + providedIn: 'root', +}) +export class ExtensionsService { + readonly entityActions = new EntityActionsFactory(); + readonly toolbarActions = new ToolbarActionsFactory(); + readonly entityProps = new EntityPropsFactory(); + readonly createFormProps = new CreateFormPropsFactory(); + readonly editFormProps = new EditFormPropsFactory(); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/tokens/extensions.token.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/tokens/extensions.token.ts new file mode 100644 index 0000000000..ba504756aa --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/tokens/extensions.token.ts @@ -0,0 +1,13 @@ +import { InjectionToken } from '@angular/core'; +import { ActionCallback, ReadonlyActionData as ActionData } from '../models/actions'; +import { ExtensionsService } from '../services/extensions.service'; + +export const EXTENSIONS_IDENTIFIER = new InjectionToken('EXTENSIONS_IDENTIFIER'); +export type ActionKeys = Extract<'entityActions' | 'toolbarActions', keyof ExtensionsService>; + +export const EXTENSIONS_ACTION_TYPE = new InjectionToken('EXTENSIONS_ACTION_TYPE'); + +export const EXTENSIONS_ACTION_DATA = new InjectionToken('EXTENSIONS_ACTION_DATA'); +export const EXTENSIONS_ACTION_CALLBACK = new InjectionToken>( + 'EXTENSIONS_ACTION_DATA', +); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/ui-extensions.module.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/ui-extensions.module.ts new file mode 100644 index 0000000000..28456e0324 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/ui-extensions.module.ts @@ -0,0 +1,56 @@ +import { CoreModule } from '@abp/ng.core'; +import { ThemeSharedModule } from '@abp/ng.theme.shared'; +import { NgModule } from '@angular/core'; +import { + NgbDatepickerModule, + NgbDropdownModule, + NgbTimepickerModule, + NgbTypeaheadModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { NgxValidateCoreModule } from '@ngx-validate/core'; +import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component'; +import { ExtensibleFormPropComponent } from './components/extensible-form/extensible-form-prop.component'; +import { ExtensibleFormComponent } from './components/extensible-form/extensible-form.component'; +import { ExtensibleTableComponent } from './components/extensible-table/extensible-table.component'; +import { GridActionsComponent } from './components/grid-actions/grid-actions.component'; +import { PageToolbarComponent } from './components/page-toolbar/page-toolbar.component'; +import { DisabledDirective } from './directives/disabled.directive'; +import { PropDataDirective } from './directives/prop-data.directive'; + +@NgModule({ + exports: [ + DateTimePickerComponent, + PageToolbarComponent, + GridActionsComponent, + ExtensibleFormComponent, + ExtensibleTableComponent, + PropDataDirective, + DisabledDirective, + ], + declarations: [ + DateTimePickerComponent, + PageToolbarComponent, + GridActionsComponent, + ExtensibleFormPropComponent, + ExtensibleFormComponent, + ExtensibleTableComponent, + PropDataDirective, + DisabledDirective, + ], + imports: [ + CoreModule, + ThemeSharedModule, + NgxValidateCoreModule, + NgbDatepickerModule, + NgbDropdownModule, + NgbTimepickerModule, + NgbTypeaheadModule, + ], +}) +export class BaseUiExtensionsModule {} + +@NgModule({ + exports: [BaseUiExtensionsModule], + imports: [BaseUiExtensionsModule], +}) +export class UiExtensionsModule {} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/actions.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/actions.util.ts new file mode 100644 index 0000000000..f4501317df --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/actions.util.ts @@ -0,0 +1,49 @@ +import { ActionContributorCallback, ActionList, ActionsFactory } from '../models/actions'; +import { + EntityActionContributorCallbacks, + EntityActionDefaults, + EntityActions, + EntityActionsFactory, +} from '../models/entity-actions'; +import { + ToolbarActionContributorCallbacks, + ToolbarActionDefaults, + ToolbarActions, + ToolbarActionsFactory, +} from '../models/toolbar-actions'; + +export function mergeWithDefaultActions>( + extension: F, + defaultActions: InferredActionDefaults, + ...contributors: InferredActionContributorCallbacks[] +) { + Object.keys(defaultActions).forEach((name: string) => { + const actions: InferredActions = extension.get(name); + actions.clearContributors(); + actions.addContributor((actionList: ActionList) => + actionList.addManyTail(defaultActions[name]), + ); + contributors.forEach(contributor => + (contributor[name] || []).forEach((callback: ActionContributorCallback) => + actions.addContributor(callback), + ), + ); + }); +} +type InferredActionDefaults = F extends EntityActionsFactory + ? EntityActionDefaults + : F extends ToolbarActionsFactory + ? ToolbarActionDefaults + : never; + +type InferredActionContributorCallbacks = F extends EntityActionsFactory + ? EntityActionContributorCallbacks + : F extends ToolbarActionsFactory + ? ToolbarActionContributorCallbacks + : never; + +type InferredActions = F extends EntityActionsFactory + ? EntityActions + : F extends ToolbarActionsFactory + ? ToolbarActions + : never; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/enum.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/enum.util.ts new file mode 100644 index 0000000000..bb8da3694f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/enum.util.ts @@ -0,0 +1,73 @@ +import { ABP, LocalizationService } from '@abp/ng.core'; +import { merge, Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; +import { ObjectExtensions } from '../models/object-extensions'; +import { PropCallback } from '../models/props'; + +export function createEnum(members: ObjectExtensions.ExtensionEnumFieldDto[]) { + const enumObject: any = {}; + + members.forEach(({ name, value }) => { + enumObject[(enumObject[name] = value as any)] = name; + }); + + return enumObject; +} + +export function createEnumValueResolver( + enumType: string, + lookupEnum: ObjectExtensions.ExtensionEnumDto, + propName: string, +): PropCallback> { + return data => { + const value = data.record[EXTRA_PROPERTIES_KEY][propName]; + const key = lookupEnum.transformed[value]; + const l10n = data.getInjected(LocalizationService); + const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum); + + return createLocalizationStream(l10n, localizeEnum(key)); + }; +} + +export function createEnumOptions( + enumType: string, + lookupEnum: ObjectExtensions.ExtensionEnumDto, +): PropCallback[]>> { + return data => { + const l10n = data.getInjected(LocalizationService); + const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum); + + return createLocalizationStream( + l10n, + lookupEnum.fields.map(({ name, value }) => ({ + key: localizeEnum(name), + value, + })), + ); + }; +} + +function createLocalizationStream(l10n: LocalizationService, mapTarget: any) { + return merge(of(null), l10n.languageChange$).pipe(map(() => mapTarget)); +} + +function createEnumLocalizer( + l10n: LocalizationService, + enumType: string, + lookupEnum: ObjectExtensions.ExtensionEnumDto, +): (key: string) => string { + const resource = lookupEnum.localizationResource; + const shortType = getShortEnumType(enumType); + + return key => + l10n.localizeWithFallbackSync( + [resource], + ['Enum:' + shortType + '.' + key, shortType + '.' + key, key], + key, + ); +} + +function getShortEnumType(enumType: string): string { + return enumType.split('.').pop(); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/factory.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/factory.util.ts new file mode 100644 index 0000000000..5bb6265075 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/factory.util.ts @@ -0,0 +1,3 @@ +export function selfFactory(dependency?: any) { + return dependency; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/form-props.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/form-props.util.ts new file mode 100644 index 0000000000..828ac383d0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/form-props.util.ts @@ -0,0 +1,61 @@ +import { FormControl, FormGroup } from '@angular/forms'; +import { DateTimeAdapter } from '../adapters/date-time.adapter'; +import { DateAdapter } from '../adapters/date.adapter'; +import { TimeAdapter } from '../adapters/time.adapter'; +import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; +import { ePropType } from '../enums/props.enum'; +import { FormPropList } from '../models/form-props'; +import { PropData } from '../models/props'; +import { ExtensionsService } from '../services/extensions.service'; +import { EXTENSIONS_IDENTIFIER } from '../tokens/extensions.token'; + +export function generateFormFromProps(data: PropData) { + const extensions = data.getInjected(ExtensionsService); + const identifier = data.getInjected(EXTENSIONS_IDENTIFIER); + + const form = new FormGroup({}); + const extraForm = new FormGroup({}); + form.addControl(EXTRA_PROPERTIES_KEY, extraForm); + + const record = data.record || {}; + const type = JSON.stringify(record) === '{}' ? 'create' : 'edit'; + const props: FormPropList = extensions[`${type}FormProps`].get(identifier).props; + const extraProperties = record[EXTRA_PROPERTIES_KEY] || {}; + + props.forEach(({ value: prop }) => { + const name = prop.name; + const isExtraProperty = prop.isExtra || name in extraProperties; + let value = isExtraProperty ? extraProperties[name] : name in record ? record[name] : undefined; + + if (typeof value === 'undefined') value = prop.defaultValue; + + if (value) { + let adapter: DateAdapter | TimeAdapter | DateTimeAdapter; + switch (prop.type) { + case ePropType.Date: + adapter = new DateAdapter(); + value = adapter.toModel(adapter.fromModel(value)); + break; + case ePropType.Time: + adapter = new TimeAdapter(); + value = adapter.toModel(adapter.fromModel(value)); + break; + case ePropType.DateTime: + adapter = new DateTimeAdapter(); + value = adapter.toModel(adapter.fromModel(value) as any); + break; + default: + break; + } + } + + const formControl = new FormControl(value, { + asyncValidators: prop.asyncValidators(data), + validators: prop.validators(data), + }); + + (isExtraProperty ? extraForm : form).addControl(name, formControl); + }); + + return form; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/localization.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/localization.util.ts new file mode 100644 index 0000000000..227735591b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/localization.util.ts @@ -0,0 +1,33 @@ +import { + ApplicationLocalizationConfigurationDto, + createLocalizationPipeKeyGenerator, +} from '@abp/ng.core'; +import { ObjectExtensions } from '../models/object-extensions'; + +export function createDisplayNameLocalizationPipeKeyGenerator( + localization: ApplicationLocalizationConfigurationDto, +) { + const generateLocalizationPipeKey = createLocalizationPipeKeyGenerator(localization); + + return ( + displayName: ObjectExtensions.LocalizableStringDto, + fallback: ObjectExtensions.LocalizableStringDto, + ) => { + if (displayName && displayName.name) + return generateLocalizationPipeKey( + [displayName.resource], + [displayName.name], + displayName.name, + ); + + const key = generateLocalizationPipeKey( + [fallback.resource], + ['DisplayName:' + fallback.name], + undefined, + ); + + if (key) return key; + + return generateLocalizationPipeKey([fallback.resource], [fallback.name], fallback.name); + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/props.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/props.util.ts new file mode 100644 index 0000000000..b750131065 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/props.util.ts @@ -0,0 +1,62 @@ +import { of } from 'rxjs'; +import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; +import { + EntityPropContributorCallbacks, + EntityPropDefaults, + EntityProps, + EntityPropsFactory, +} from '../models/entity-props'; +import { + CreateFormPropContributorCallbacks, + CreateFormPropDefaults, + CreateFormPropsFactory, + EditFormPropContributorCallbacks, + EditFormPropDefaults, + EditFormPropsFactory, + FormProps, +} from '../models/form-props'; +import { PropContributorCallback, PropData, PropList, PropsFactory } from '../models/props'; + +export function createExtraPropertyValueResolver(name: string) { + return (data?: PropData) => of(data.record[EXTRA_PROPERTIES_KEY][name]); +} + +export function mergeWithDefaultProps>( + extension: F, + defaultProps: InferredPropDefaults, + ...contributors: InferredPropContributorCallbacks[] +) { + Object.keys(defaultProps).forEach((name: string) => { + const props: InferredProps = extension.get(name); + props.clearContributors(); + props.addContributor((propList: PropList) => propList.addManyTail(defaultProps[name])); + contributors.forEach(contributor => + (contributor[name] || []).forEach((callback: PropContributorCallback) => + props.addContributor(callback), + ), + ); + }); +} +type InferredPropDefaults = F extends EntityPropsFactory + ? EntityPropDefaults + : F extends CreateFormPropsFactory + ? CreateFormPropDefaults + : F extends EditFormPropsFactory + ? EditFormPropDefaults + : never; + +type InferredPropContributorCallbacks = F extends EntityPropsFactory + ? EntityPropContributorCallbacks + : F extends CreateFormPropsFactory + ? CreateFormPropContributorCallbacks + : F extends EditFormPropsFactory + ? EditFormPropContributorCallbacks + : never; + +type InferredProps = F extends EntityPropsFactory + ? EntityProps + : F extends CreateFormPropsFactory + ? FormProps + : F extends EditFormPropsFactory + ? FormProps + : never; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/state.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/state.util.ts new file mode 100644 index 0000000000..4920687077 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/state.util.ts @@ -0,0 +1,200 @@ +import { + ABP, + ApplicationLocalizationConfigurationDto, + ConfigStateService, + ExtensionPropertyUiLookupDto, +} from '@abp/ng.core'; +import { Observable, pipe, zip } from 'rxjs'; +import { filter, map, switchMap, take } from 'rxjs/operators'; +import { ePropType } from '../enums/props.enum'; +import { EntityProp, EntityPropList } from '../models/entity-props'; +import { FormProp, FormPropList } from '../models/form-props'; +import { ObjectExtensions } from '../models/object-extensions'; +import { PropCallback } from '../models/props'; +import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util'; +import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util'; +import { createExtraPropertyValueResolver } from './props.util'; +import { + createTypeaheadDisplayNameGenerator, + createTypeaheadOptions, + getTypeaheadType, + hasTypeaheadTextSuffix, +} from './typeahead.util'; +import { getValidatorsFromProperty } from './validation.util'; + +function selectObjectExtensions( + configState: ConfigStateService, +): Observable { + return configState.getOne$('objectExtensions'); +} + +function selectLocalization( + configState: ConfigStateService, +): Observable { + return configState.getOne$('localization'); +} + +function selectEnums( + configState: ConfigStateService, +): Observable> { + return selectObjectExtensions(configState).pipe( + map((extensions: ObjectExtensions.ObjectExtensionsDto) => + Object.keys(extensions.enums).reduce((acc, key) => { + const { fields, localizationResource } = extensions.enums[key]; + acc[key] = { + fields, + localizationResource, + transformed: createEnum(fields), + }; + return acc; + }, {} as Record), + ), + ); +} + +export function getObjectExtensionEntitiesFromStore( + configState: ConfigStateService, + moduleKey: string, +) { + return selectObjectExtensions(configState).pipe( + map(extensions => { + if (!extensions) return null; + + return (extensions.modules[moduleKey] || ({} as ObjectExtensions.ModuleExtensionDto)) + .entities; + }), + map(entities => (isUndefined(entities) ? {} : entities)), + filter(Boolean), + take(1), + ); +} + +export function mapEntitiesToContributors( + configState: ConfigStateService, + resource: string, +) { + return pipe( + switchMap(entities => + zip(selectLocalization(configState), selectEnums(configState)).pipe( + map(([localization, enums]) => { + const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator(localization); + + return Object.keys(entities).reduce( + (acc, key: keyof ObjectExtensions.EntityExtensions) => { + acc.prop[key] = []; + acc.createForm[key] = []; + acc.editForm[key] = []; + + const entity: ObjectExtensions.EntityExtensionDto = entities[key]; + if (!entity) return acc; + + const properties = entity.properties; + if (!properties) return acc; + + const mapPropertiesToContributors = createPropertiesToContributorsMapper( + generateDisplayName, + resource, + enums, + ); + + return mapPropertiesToContributors(properties, acc, key); + }, + { + prop: {}, + createForm: {}, + editForm: {}, + } as ObjectExtensions.PropContributors, + ); + }), + ), + ), + take(1), + ); +} + +function createPropertiesToContributorsMapper( + generateDisplayName: ObjectExtensions.DisplayNameGeneratorFn, + resource: string, + enums: Record, +) { + return ( + properties: ObjectExtensions.EntityExtensionProperties, + contributors: ObjectExtensions.PropContributors, + key: string, + ) => { + const isExtra = true; + const generateTypeaheadDisplayName = createTypeaheadDisplayNameGenerator( + generateDisplayName, + properties, + ); + + Object.keys(properties).forEach((name: string) => { + const property = properties[name]; + const propName = name; + const lookup = property.ui.lookup || ({} as ExtensionPropertyUiLookupDto); + const type = getTypeaheadType(lookup, name) || getTypeFromProperty(property); + const generateDN = hasTypeaheadTextSuffix(name) + ? generateTypeaheadDisplayName + : generateDisplayName; + const displayName = generateDN(property.displayName, { name, resource }); + + if (property.ui.onTable.isVisible) { + const sortable = Boolean(property.ui.onTable.isSortable); + const columnWidth = type === ePropType.Boolean ? 150 : 250; + const valueResolver = + type === ePropType.Enum + ? createEnumValueResolver(property.type, enums[property.type], propName) + : createExtraPropertyValueResolver(propName); + + const entityProp = new EntityProp({ + type, + name: propName, + displayName, + sortable, + columnWidth, + valueResolver, + isExtra, + }); + + const contributor = (propList: EntityPropList) => propList.addTail(entityProp); + contributors.prop[key].push(contributor); + } + + const isOnCreateForm = property.ui.onCreateForm.isVisible; + const isOnEditForm = property.ui.onEditForm.isVisible; + + if (isOnCreateForm || isOnEditForm) { + const defaultValue = property.defaultValue; + const validators = () => getValidatorsFromProperty(property); + let options: PropCallback[]>>; + if (type === ePropType.Enum) options = createEnumOptions(propName, enums[property.type]); + else if (type === ePropType.Typeahead) options = createTypeaheadOptions(lookup); + + const formProp = new FormProp({ + type, + name: propName, + displayName, + options, + defaultValue, + validators, + isExtra, + }); + + const formContributor = (propList: FormPropList) => propList.addTail(formProp); + + if (isOnCreateForm) contributors.createForm[key].push(formContributor); + if (isOnEditForm) contributors.editForm[key].push(formContributor); + } + }); + + return contributors; + }; +} + +function getTypeFromProperty(property: ObjectExtensions.ExtensionPropertyDto): ePropType { + return (property.typeSimple.replace(/\?$/, '') as string) as ePropType; +} + +function isUndefined(obj: any): obj is undefined { + return typeof obj === 'undefined'; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/typeahead.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/typeahead.util.ts new file mode 100644 index 0000000000..b34528104d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/typeahead.util.ts @@ -0,0 +1,72 @@ +import { ABP, ExtensionPropertyUiLookupDto, RestService } from '@abp/ng.core'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ePropType } from '../enums/props.enum'; +import { ObjectExtensions } from '../models/object-extensions'; +import { PropCallback } from '../models/props'; + +const TYPEAHEAD_TEXT_SUFFIX = '_Text'; +const TYPEAHEAD_TEXT_SUFFIX_REGEX = /_Text$/; + +export function createTypeaheadOptions( + lookup: ExtensionPropertyUiLookupDto, +): PropCallback[]>> { + return (data, searchText) => + searchText + ? data + .getInjected(RestService) + .request( + { + method: 'GET', + url: lookup.url, + params: { + [lookup.filterParamName]: searchText, + }, + }, + { apiName: 'Default' }, + ) + .pipe( + map(response => { + const list = response[lookup.resultListPropertyName]; + const mapToOption = (item: any) => ({ + key: item[lookup.displayPropertyName], + value: item[lookup.valuePropertyName], + }); + return list.map(mapToOption); + }), + ) + : of([]); +} + +export function getTypeaheadType(lookup: ExtensionPropertyUiLookupDto, name: string) { + return Boolean(lookup.url) + ? ePropType.Typeahead + : name.endsWith(TYPEAHEAD_TEXT_SUFFIX) + ? ePropType.Hidden + : undefined; +} + +export function createTypeaheadDisplayNameGenerator( + displayNameGeneratorFn: ObjectExtensions.DisplayNameGeneratorFn, + properties: ObjectExtensions.EntityExtensionProperties, +): ObjectExtensions.DisplayNameGeneratorFn { + return (displayName, fallback) => { + const name = removeTypeaheadTextSuffix(fallback.name); + return displayNameGeneratorFn(displayName || properties[name].displayName, { + name, + resource: fallback.resource, + }); + }; +} + +export function addTypeaheadTextSuffix(name: string) { + return name + TYPEAHEAD_TEXT_SUFFIX; +} + +export function hasTypeaheadTextSuffix(name: string) { + return TYPEAHEAD_TEXT_SUFFIX_REGEX.test(name); +} + +export function removeTypeaheadTextSuffix(name: string) { + return name.replace(TYPEAHEAD_TEXT_SUFFIX_REGEX, ''); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/validation.util.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/validation.util.ts new file mode 100644 index 0000000000..d87c6c1260 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/lib/utils/validation.util.ts @@ -0,0 +1,16 @@ +import { AbpValidators } from '@abp/ng.core'; +import { ValidatorFn } from '@angular/forms'; +import { ObjectExtensions } from '../models/object-extensions'; + +export function getValidatorsFromProperty( + property: ObjectExtensions.ExtensionPropertyDto, +): ValidatorFn[] { + const validators: ValidatorFn[] = []; + + property.attributes.forEach(attr => { + if (attr.typeSimple in AbpValidators) + validators.push(AbpValidators[attr.typeSimple](attr.config)); + }); + + return validators; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/public-api.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/public-api.ts new file mode 100644 index 0000000000..a3afbe7efd --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/public-api.ts @@ -0,0 +1,70 @@ +export * from './lib/adapters/date-time.adapter'; +export * from './lib/adapters/date.adapter'; +export * from './lib/adapters/time.adapter'; +export * from './lib/components/date-time-picker/date-time-picker.component'; +export * from './lib/components/extensible-form/extensible-form-prop.component'; +export * from './lib/components/extensible-form/extensible-form.component'; +export * from './lib/components/extensible-table/extensible-table.component'; +export * from './lib/components/grid-actions/grid-actions.component'; +export * from './lib/components/page-toolbar/page-toolbar.component'; +export * from './lib/constants/extra-properties'; +export * from './lib/directives/disabled.directive'; +export * from './lib/directives/prop-data.directive'; +export * from './lib/enums/props.enum'; +export { + ActionCallback, + ActionList, + ActionPredicate, + ReadonlyActionData as ActionData, +} from './lib/models/actions'; +export { + EntityAction, + EntityActionContributorCallback, + EntityActionList, + EntityActionOptions, + EntityActions, + EntityActionsFactory, +} from './lib/models/entity-actions'; +export { + EntityProp, + EntityPropContributorCallback, + EntityPropList, + EntityPropOptions, + EntityProps, + EntityPropsFactory, +} from './lib/models/entity-props'; +export { + CreateFormPropContributorCallback, + CreateFormPropsFactory, + EditFormPropContributorCallback, + EditFormPropsFactory, + FormProp, + FormPropData, + FormPropList, + FormPropOptions, + FormProps, +} from './lib/models/form-props'; +export * from './lib/models/object-extensions'; +export { + PropCallback, + PropList, + PropPredicate, + ReadonlyPropData as PropData, +} from './lib/models/props'; +export { + ToolbarAction, + ToolbarActionContributorCallback, + ToolbarActionList, + ToolbarActionOptions, + ToolbarActions, + ToolbarActionsFactory, + ToolbarComponent, + ToolbarComponentOptions, +} from './lib/models/toolbar-actions'; +export * from './lib/services/extensions.service'; +export * from './lib/tokens/extensions.token'; +export * from './lib/ui-extensions.module'; +export * from './lib/utils/actions.util'; +export * from './lib/utils/form-props.util'; +export * from './lib/utils/props.util'; +export * from './lib/utils/state.util'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.spec.ts new file mode 100644 index 0000000000..ea8d111bec --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.spec.ts @@ -0,0 +1,22 @@ +import { ActionData } from '../lib/models/actions'; + +describe('ActionData', () => { + describe('#data', () => { + it('should return record and getInjected', () => { + const spy = jest.fn(); + class Data extends ActionData { + index = 0; + record = 'X'; + getInjected = spy; + } + + const data = new Data(); + data.data.getInjected(null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(null); + expect(data.data.index).toBe(0); + expect(data.data.record).toBe('X'); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.util.spec.ts new file mode 100644 index 0000000000..fd3b27c564 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/actions.util.spec.ts @@ -0,0 +1,53 @@ +import { + EntityAction, + EntityActionContributorCallbacks, + EntityActionDefaults, + EntityActionsFactory, +} from '../lib/models/entity-actions'; +import { mergeWithDefaultActions } from '../lib/utils/actions.util'; + +describe('Entity Action Utils', () => { + describe('#mergeEntityActions', () => { + let entityActions: EntityActionsFactory; + + beforeEach(() => { + entityActions = new EntityActionsFactory(); + }); + + it('should merge default actions with action contributors', () => { + const defaults: EntityActionDefaults = { + x: [(1 as any) as EntityAction, (2 as any) as EntityAction, (3 as any) as EntityAction], + y: [(1 as any) as EntityAction, (2 as any) as EntityAction, (3 as any) as EntityAction], + }; + + const contributors1: EntityActionContributorCallbacks = { + x: [ + actionList => { + const x2 = actionList.dropByIndex(1); // 1 <-> 3 + actionList.addHead(x2.value); // 2 <-> 1 <-> 3 + }, + actionList => { + actionList.dropTail(); // 2 <-> 1 + }, + ], + }; + + const contributors2: EntityActionContributorCallbacks = { + y: [ + actionList => { + const y2 = actionList.dropByIndex(1); // 1 <-> 3 + actionList.addTail(y2.value); // 1 <-> 3 <-> 2 + }, + actionList => { + actionList.dropHead(); // 3 <-> 2 + }, + ], + }; + + mergeWithDefaultActions(entityActions, defaults, contributors1, contributors2); + + expect(entityActions.get('x').actions.toString()).toBe('2 <-> 1'); + expect(entityActions.get('y').actions.toString()).toBe('3 <-> 2'); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date-time.adapter.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date-time.adapter.spec.ts new file mode 100644 index 0000000000..621246b5e0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date-time.adapter.spec.ts @@ -0,0 +1,35 @@ +import { DateTimeAdapter } from '../lib/adapters/date-time.adapter'; + +describe('DateTime Adapter', () => { + const adapter = new DateTimeAdapter(); + const date = new Date(2002, 2, 30, 13, 30, 45, 0); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hour = date.getHours(); + const minute = date.getMinutes(); + const second = date.getSeconds(); + + describe('#fromModel', () => { + test.each` + param | expected + ${undefined} | ${null} + ${null} | ${null} + ${'x'} | ${null} + ${date} | ${{ year, month, day, hour, minute, second }} + `('should return $expected when $param is given', ({ param, expected }) => { + expect(adapter.fromModel(param)).toEqual(expected); + }); + }); + + describe('#toModel', () => { + test.each` + param | expected + ${undefined} | ${''} + ${null} | ${''} + ${{ year, month, day, hour, minute, second }} | ${date.toISOString()} + `('should return $expected when $param is given', ({ param, expected }) => { + expect(adapter.toModel(param)).toEqual(expected); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date.adapter.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date.adapter.spec.ts new file mode 100644 index 0000000000..e1101f6a29 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/date.adapter.spec.ts @@ -0,0 +1,31 @@ +import { DateAdapter } from '../lib/adapters/date.adapter'; + +describe('Date Adapter', () => { + const adapter = new DateAdapter(); + + describe('#fromModel', () => { + test.each` + param | expected + ${undefined} | ${null} + ${null} | ${null} + ${'x'} | ${null} + ${'2002-03-30'} | ${{ day: 30, month: 3, year: 2002 }} + ${'03/30/2002'} | ${{ day: 30, month: 3, year: 2002 }} + ${new Date(0)} | ${{ day: 1, month: 1, year: 1970 }} + `('should return $expected when $param is given', ({ param, expected }) => { + expect(adapter.fromModel(param)).toEqual(expected); + }); + }); + + describe('#toModel', () => { + test.each` + param | expected + ${undefined} | ${''} + ${null} | ${''} + ${{ day: 30, month: 3, year: 2002 }} | ${'2002-03-30'} + ${{ day: 1, month: 1, year: 1970 }} | ${'1970-01-01'} + `('should return $expected when $param is given', ({ param, expected }) => { + expect(adapter.toModel(param)).toEqual(expected); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-actions.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-actions.spec.ts new file mode 100644 index 0000000000..35833a1950 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-actions.spec.ts @@ -0,0 +1,151 @@ +import { LinkedList } from '@abp/utils'; +import { + EntityAction, + EntityActionContributorCallback, + EntityActionList, + EntityActions, + EntityActionsFactory, +} from '../lib/models/entity-actions'; + +describe('EntityActionList', () => { + it('should inherit from LinkedList', () => { + expect(new EntityActionList() instanceof LinkedList).toBe(true); + }); +}); + +describe('EntityActions', () => { + const add1toTail: EntityActionContributorCallback = actionList => actionList.addTail(1 as any); + const add2toTail: EntityActionContributorCallback = actionList => actionList.addTail(2 as any); + const add3toTail: EntityActionContributorCallback = actionList => actionList.addTail(3 as any); + const dropIndex1: EntityActionContributorCallback = actionList => actionList.dropByIndex(1); + + describe('#actions', () => { + test.each` + callbackList | expected + ${[]} | ${''} + ${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} + ${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} + ${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} + `( + 'should return $expected when given callbackList is $callbackList', + ({ callbackList, expected }) => { + const creator = new EntityActions(callbackList); + expect(creator.actions.toString()).toBe(expected); + }, + ); + }); + + describe('#addContributor', () => { + const creator = new EntityActions([]); + + test.each` + callbackList | callback | expected + ${[]} | ${add1toTail} | ${'1'} + ${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} + ${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} + ${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} + `( + 'should set actions to $expected when callbackList is $callbackList and given callback is $callback', + ({ callback, expected }) => { + creator.addContributor(callback); + expect(creator.actions.toString()).toBe(expected); + }, + ); + }); +}); + +describe('EntityActionsFactory', () => { + describe('#get', () => { + it('should create and return an EntityActions instance', () => { + const entityActions = new EntityActionsFactory(); + const creator = entityActions.get(''); + + expect(creator).toBeInstanceOf(EntityActions); + }); + + it('should store and pass contributorCallbacks to EntityActionsCreator instance it returns', () => { + const entityActions = new EntityActionsFactory(); + const creatorX1 = entityActions.get('X'); + + expect(creatorX1).toBeInstanceOf(EntityActions); + expect(creatorX1.actions.toArray()).toHaveLength(0); + + creatorX1.addContributor(actionList => actionList.addTail(1 as any)); + + const creatorX2 = entityActions.get('X'); + expect(creatorX2.actions.toArray()).toHaveLength(1); + }); + }); +}); + +describe('EntityAction', () => { + it('should be created when options object is passed as argument', () => { + const options = { + text: 'TEXT', + action: () => 'ACTION', + permission: 'PERMISSION', + visible: () => false, + icon: 'ICON', + }; + + const action = new EntityAction(options); + + expect(action.text).toBe(options.text); + expect(action.action(null)).toBe(options.action()); + expect(action.permission).toBe(options.permission); + expect(action.visible(null)).toBe(options.visible()); + expect(action.icon).toBe(options.icon); + }); + + it('should be created when only required options are passed', () => { + const options = { + text: 'TEXT', + action: () => 'ACTION', + }; + + const action = new EntityAction(options); + + expect(action.text).toBe(options.text); + expect(action.action).toBe(options.action); + expect(action.permission).toBeUndefined(); + expect(action.visible(null)).toBe(true); + expect(action.icon).toBe(''); + }); + + describe('#create', () => { + it('should return a new instance from given options', () => { + const options = { + text: 'TEXT', + action: () => 'ACTION', + }; + + const action = EntityAction.create(options); + + expect(action).toBeInstanceOf(EntityAction); + expect(action.text).toBe(options.text); + expect(action.action).toBe(options.action); + }); + }); + + describe('#createMany', () => { + it('should return multiple instances from given options array', () => { + const options1 = { + text: 'TEXT 1', + action: () => 'ACTION 1', + }; + const options2 = { + text: 'TEXT 2', + action: () => 'ACTION 2', + }; + + const [action1, action2] = EntityAction.createMany([options1, options2]); + + expect(action1).toBeInstanceOf(EntityAction); + expect(action1.text).toBe(options1.text); + expect(action1.action).toBe(options1.action); + expect(action2).toBeInstanceOf(EntityAction); + expect(action2.text).toBe(options2.text); + expect(action2.action).toBe(options2.action); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-props.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-props.spec.ts new file mode 100644 index 0000000000..faccbf53b2 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/entity-props.spec.ts @@ -0,0 +1,166 @@ +import { LinkedList } from '@abp/utils'; +import { NEVER } from 'rxjs'; +import { ePropType } from '../lib/enums/props.enum'; +import { + EntityProp, + EntityPropContributorCallback, + EntityPropList, + EntityProps, + EntityPropsFactory, +} from '../lib/models/entity-props'; +import { PropData } from '../lib/models/props'; + +describe('EntityPropList', () => { + it('should inherit from LinkedList', () => { + expect(new EntityPropList() instanceof LinkedList).toBe(true); + }); +}); + +describe('EntityProps', () => { + const add1toTail: EntityPropContributorCallback = propList => propList.addTail(1 as any); + const add2toTail: EntityPropContributorCallback = propList => propList.addTail(2 as any); + const add3toTail: EntityPropContributorCallback = propList => propList.addTail(3 as any); + const dropIndex1: EntityPropContributorCallback = propList => propList.dropByIndex(1); + + describe('#props', () => { + test.each` + callbackList | expected + ${[]} | ${''} + ${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} + ${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} + ${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} + `( + 'should return $expected when given callbackList is $callbackList', + ({ callbackList, expected }) => { + const creator = new EntityProps(callbackList); + expect(creator.props.toString()).toBe(expected); + }, + ); + }); + + describe('#addContributor', () => { + const creator = new EntityProps([]); + + test.each` + callbackList | callback | expected + ${[]} | ${add1toTail} | ${'1'} + ${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} + ${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} + ${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} + `( + 'should set props to $expected when callbackList is $callbackList and given callback is $callback', + ({ callback, expected }) => { + creator.addContributor(callback); + expect(creator.props.toString()).toBe(expected); + }, + ); + }); +}); + +describe('EntityPropsFactory', () => { + describe('#get', () => { + it('should create and return an EntityProps instance', () => { + const entityProps = new EntityPropsFactory(); + const creator = entityProps.get(''); + + expect(creator).toBeInstanceOf(EntityProps); + }); + + it('should store and pass contributorCallbacks to EntityPropsCreator instance it returns', () => { + const entityProps = new EntityPropsFactory(); + const creatorX1 = entityProps.get('X'); + + expect(creatorX1).toBeInstanceOf(EntityProps); + expect(creatorX1.props.toArray()).toHaveLength(0); + + creatorX1.addContributor(propList => propList.addTail(1 as any)); + + const creatorX2 = entityProps.get('X'); + expect(creatorX2.props.toArray()).toHaveLength(1); + }); + }); +}); + +describe('EntityProp', () => { + it('should be created when options object is passed as argument', () => { + const options = { + type: ePropType.String, + name: 'NAME', + displayName: 'DISPLAY NAME', + permission: 'PERMISSION', + visible: () => false, + valueResolver: () => NEVER, + sortable: true, + columnWidth: 999, + }; + + const prop = new EntityProp(options); + + expect(prop.type).toBe(options.type); + expect(prop.name).toBe(options.name); + expect(prop.displayName).toBe(options.displayName); + expect(prop.permission).toBe(options.permission); + expect(prop.visible()).toBe(options.visible()); + expect(prop.valueResolver()).toBe(options.valueResolver()); + expect(prop.sortable).toBe(options.sortable); + expect(prop.columnWidth).toBe(options.columnWidth); + }); + + it('should be created when only required options are passed', done => { + const options = { + type: ePropType.String, + name: 'NAME', + }; + + const prop = new EntityProp(options); + + expect(prop.type).toBe(options.type); + expect(prop.name).toBe(options.name); + expect(prop.displayName).toBe(options.name); + expect(prop.permission).toBeUndefined(); + expect(prop.visible()).toBe(true); + expect(prop.sortable).toBe(false); + expect(prop.columnWidth).toBeUndefined(); + prop.valueResolver({ record: { NAME: 'X' } } as PropData).subscribe(value => { + expect(value).toBe('X'); + done(); + }); + }); + + describe('#create', () => { + it('should return a new instance from given options', () => { + const options = { + type: ePropType.String, + name: 'NAME', + }; + + const prop = EntityProp.create(options); + + expect(prop).toBeInstanceOf(EntityProp); + expect(prop.type).toBe(options.type); + expect(prop.name).toBe(options.name); + }); + }); + + describe('#createMany', () => { + it('should return multiple instances from given options array', () => { + const options1 = { + type: ePropType.String, + name: 'NAME 1', + }; + const options2 = { + type: ePropType.Boolean, + name: 'NAME 2', + }; + + const [prop1, prop2] = EntityProp.createMany([options1, options2]); + + expect(prop1).toBeInstanceOf(EntityProp); + expect(prop1.type).toBe(options1.type); + expect(prop1.name).toBe(options1.name); + expect(prop2).toBeInstanceOf(EntityProp); + expect(prop2.type).toBe(options2.type); + expect(prop2.name).toBe(options2.name); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/enum.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/enum.util.spec.ts new file mode 100644 index 0000000000..6c9ffeaa38 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/enum.util.spec.ts @@ -0,0 +1,114 @@ +import { ConfigStateService, LocalizationService } from '@abp/ng.core'; +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { PropData } from '../lib/models/props'; +import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util'; + +const mockSessionState = { + languageChange$: new BehaviorSubject('tr'), + getLanguage: () => 'tr', + onLanguageChange$: () => new BehaviorSubject('tr'), +} as any; + +const fields = [ + { name: 'foo', value: 1 }, + { name: 'bar', value: 2 }, + { name: 'baz', value: 3 }, +]; + +class MockPropData extends PropData { + getInjected: PropData['getInjected']; + + constructor(public readonly record: R) { + super(); + } +} + +const mockL10n = { + values: { + Default: { + 'Enum:MyEnum.foo': 'Foo', + 'MyEnum.bar': 'Bar', + baz: 'Baz', + }, + }, + defaultResourceName: 'Default', + currentCulture: null, + languages: [], +}; + +describe('Enum Utils', () => { + describe('#createEnum', () => { + const enumFromFields = createEnum(fields); + + test.each` + key | expected + ${'foo'} | ${1} + ${'bar'} | ${2} + ${'baz'} | ${3} + ${1} | ${'foo'} + ${2} | ${'bar'} + ${3} | ${'baz'} + `('should create an enum that returns $expected when $key is accessed', ({ key, expected }) => { + expect(enumFromFields[key]).toBe(expected); + }); + }); + + describe('#createEnumValueResolver', () => { + test.each` + value | expected + ${1} | ${'Foo'} + ${2} | ${'Bar'} + ${3} | ${'Baz'} + `( + 'should create a resolver that returns observable $expected when enum value is $value', + async ({ value, expected }) => { + const service = createMockLocalizationService(); + const valueResolver = createEnumValueResolver( + 'MyCompanyName.MyProjectName.MyEnum', + { + fields, + localizationResource: null, + transformed: createEnum(fields), + }, + 'EnumProp', + ); + const propData = new MockPropData({ extraProperties: { EnumProp: value } }); + propData.getInjected = () => service as any; + + const resolved = await valueResolver(propData).pipe(take(1)).toPromise(); + + expect(resolved).toBe(expected); + }, + ); + }); + + describe('#createEnumOptions', () => { + it('should create a generator that returns observable options from enums', async () => { + const service = createMockLocalizationService(); + const options = createEnumOptions('MyCompanyName.MyProjectName.MyEnum', { + fields, + localizationResource: null, + transformed: createEnum(fields), + }); + + const propData = new MockPropData({}); + propData.getInjected = () => service as any; + + const resolved = await options(propData).pipe(take(1)).toPromise(); + + expect(resolved).toEqual([ + { key: 'Foo', value: 1 }, + { key: 'Bar', value: 2 }, + { key: 'Baz', value: 3 }, + ]); + }); + }); +}); + +function createMockLocalizationService() { + const configState = new ConfigStateService(); + configState.setState({ localization: mockL10n } as any); + + return new LocalizationService(mockSessionState, null, null, configState, null); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/extensions.service.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/extensions.service.spec.ts new file mode 100644 index 0000000000..73aa7efe12 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/extensions.service.spec.ts @@ -0,0 +1,50 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { EntityActionsFactory } from '../lib/models/entity-actions'; +import { EntityPropsFactory } from '../lib/models/entity-props'; +import { CreateFormPropsFactory, EditFormPropsFactory } from '../lib/models/form-props'; +import { ToolbarActionsFactory } from '../lib/models/toolbar-actions'; +import { ExtensionsService } from '../lib/services/extensions.service'; + +describe('ExtensionsService', () => { + let service: ExtensionsService; + let spectator: SpectatorService; + + const createService = createServiceFactory({ + service: ExtensionsService, + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + }); + + describe('#entityActions', () => { + it('should be an instance of EntityActionsFactory class', () => { + expect(service.entityActions).toBeInstanceOf(EntityActionsFactory); + }); + }); + + describe('#toolbarActions', () => { + it('should be an instance of ToolbarActionsFactory class', () => { + expect(service.toolbarActions).toBeInstanceOf(ToolbarActionsFactory); + }); + }); + + describe('#entityProps', () => { + it('should be an instance of EntityPropsFactory class', () => { + expect(service.entityProps).toBeInstanceOf(EntityPropsFactory); + }); + }); + + describe('#createFormProps', () => { + it('should be an instance of CreateFormPropsFactory class', () => { + expect(service.createFormProps).toBeInstanceOf(CreateFormPropsFactory); + }); + }); + + describe('#editFormProps', () => { + it('should be an instance of EditFormPropsFactory class', () => { + expect(service.editFormProps).toBeInstanceOf(EditFormPropsFactory); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/factory.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/factory.util.spec.ts new file mode 100644 index 0000000000..61bd744f23 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/factory.util.spec.ts @@ -0,0 +1,28 @@ +import { selfFactory } from '../lib/utils/factory.util'; + +describe('Factory Utils', () => { + describe('#selfFactory', () => { + const arr = []; + const obj = {}; + const date = new Date(); + const promise = Promise.resolve(null); + + test.each` + parameter + ${'x'} + ${''} + ${1} + ${0} + ${true} + ${false} + ${arr} + ${obj} + ${date} + ${promise} + ${null} + ${undefined} + `('should return given $parameter back', ({ parameter }) => { + expect(selfFactory(parameter)).toBe(parameter); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.spec.ts new file mode 100644 index 0000000000..a7ad56aca9 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.spec.ts @@ -0,0 +1,193 @@ +import { LinkedList } from '@abp/utils'; +import { NEVER } from 'rxjs'; +import { ePropType } from '../lib/enums/props.enum'; +import { + CreateFormPropContributorCallback, + CreateFormPropsFactory, + EditFormPropsFactory, + FormProp, + FormPropList, + FormProps, +} from '../lib/models/form-props'; + +describe('FormPropList', () => { + it('should inherit from LinkedList', () => { + expect(new FormPropList() instanceof LinkedList).toBe(true); + }); +}); + +describe('FormProps', () => { + const add1toTail: CreateFormPropContributorCallback = propList => propList.addTail(1 as any); + const add2toTail: CreateFormPropContributorCallback = propList => propList.addTail(2 as any); + const add3toTail: CreateFormPropContributorCallback = propList => propList.addTail(3 as any); + const dropIndex1: CreateFormPropContributorCallback = propList => propList.dropByIndex(1); + + describe('#props', () => { + test.each` + callbackList | expected + ${[]} | ${''} + ${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} + ${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} + ${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} + `( + 'should return $expected when given callbackList is $callbackList', + ({ callbackList, expected }) => { + const creator = new FormProps(callbackList); + expect(creator.props.toString()).toBe(expected); + }, + ); + }); + + describe('#addContributor', () => { + const creator = new FormProps([]); + + test.each` + callbackList | callback | expected + ${[]} | ${add1toTail} | ${'1'} + ${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} + ${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} + ${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} + `( + 'should set props to $expected when callbackList is $callbackList and given callback is $callback', + ({ callback, expected }) => { + creator.addContributor(callback); + expect(creator.props.toString()).toBe(expected); + }, + ); + }); +}); + +describe('FormPropsFactory', () => { + describe('#get', () => { + it('should create and return an FormProps instance', () => { + const formProps = new CreateFormPropsFactory(); + const creator = formProps.get(''); + + expect(creator).toBeInstanceOf(FormProps); + }); + + it('should store and pass contributorCallbacks to FormPropsCreator instance it returns', () => { + const formProps = new EditFormPropsFactory(); + const creatorX1 = formProps.get('X'); + + expect(creatorX1).toBeInstanceOf(FormProps); + expect(creatorX1.props.toArray()).toHaveLength(0); + + creatorX1.addContributor(propList => propList.addTail(1 as any)); + + const creatorX2 = formProps.get('X'); + expect(creatorX2.props.toArray()).toHaveLength(1); + }); + }); +}); + +describe('FormProp', () => { + it('should be created when options object is passed as argument', () => { + const options = { + type: ePropType.String, + name: 'NAME', + displayName: 'DISPLAY NAME', + permission: 'PERMISSION', + visible: () => false, + asyncValidators: () => [null], + validators: () => [null], + disabled: () => true, + readonly: () => true, + autocomplete: 'AUTOCOMPLETE', + defaultValue: 'DEFAULT VALUE', + options: () => NEVER, + id: 'ID', + }; + + const prop = new FormProp(options); + + expect(prop.type).toBe(options.type); + expect(prop.name).toBe(options.name); + expect(prop.displayName).toBe(options.displayName); + expect(prop.permission).toBe(options.permission); + expect(prop.visible()).toBe(options.visible()); + expect(prop.asyncValidators()).toEqual(options.asyncValidators()); + expect(prop.validators()).toEqual(options.validators()); + expect(prop.disabled()).toBe(options.disabled()); + expect(prop.readonly()).toBe(options.readonly()); + expect(prop.autocomplete).toBe(options.autocomplete); + expect(prop.defaultValue).toBe(options.defaultValue); + expect(prop.options()).toBe(options.options()); + expect(prop.id).toBe(options.id); + }); + + it('should be created when only required options are passed', () => { + const options = { + type: ePropType.String, + name: 'NAME', + }; + + const prop = new FormProp(options); + + expect(prop.type).toBe(options.type); + expect(prop.name).toBe(options.name); + expect(prop.displayName).toBe(options.name); + expect(prop.permission).toBeUndefined(); + expect(prop.visible()).toBe(true); + expect(prop.asyncValidators()).toEqual([]); + expect(prop.validators()).toEqual([]); + expect(prop.disabled()).toBe(false); + expect(prop.readonly()).toBe(false); + expect(prop.autocomplete).toBe('off'); + expect(prop.defaultValue).toBeNull(); + expect(prop.options).toBeUndefined(); + expect(prop.id).toBe(options.name); + }); + + test.each` + defaultValue | expected + ${0} | ${0} + ${''} | ${''} + ${false} | ${false} + ${undefined} | ${null} + `( + 'should set defaultValue as $expected when $defaultValue is given', + ({ defaultValue, expected }) => { + const options = { type: null, name: null, defaultValue }; + const prop = new FormProp(options); + expect(prop.defaultValue).toBe(expected); + }, + ); + + describe('#create', () => { + it('should return a new instance from given options', () => { + const options = { + type: ePropType.String, + name: 'NAME', + }; + + const prop = FormProp.create(options); + + expect(prop).toBeInstanceOf(FormProp); + expect(prop.type).toBe(options.type); + expect(prop.name).toBe(options.name); + }); + }); + + describe('#createMany', () => { + it('should return multiple instances from given options array', () => { + const options1 = { + type: ePropType.String, + name: 'NAME 1', + }; + const options2 = { + type: ePropType.Boolean, + name: 'NAME 2', + }; + + const [prop1, prop2] = FormProp.createMany([options1, options2]); + + expect(prop1).toBeInstanceOf(FormProp); + expect(prop1.type).toBe(options1.type); + expect(prop1.name).toBe(options1.name); + expect(prop2).toBeInstanceOf(FormProp); + expect(prop2.type).toBe(options2.type); + expect(prop2.name).toBe(options2.name); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.util.spec.ts new file mode 100644 index 0000000000..123912cb9a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/form-props.util.spec.ts @@ -0,0 +1,142 @@ +import { LocalizationService } from '@abp/ng.core'; +import { Injector } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { ePropType } from '../lib/enums/props.enum'; +import { FormProp, FormPropData } from '../lib/models/form-props'; +import { ExtensionsService } from '../lib/services/extensions.service'; +import { EXTENSIONS_IDENTIFIER } from '../lib/tokens/extensions.token'; +import { generateFormFromProps } from '../lib/utils/form-props.util'; + +describe('Form Prop Utils', () => { + describe('#generateFormFromProps', () => { + let spectator: SpectatorService>; + let injector: Injector; + const identifier = 'X'; + + const createService = createServiceFactory({ + service: ExtensionsService, + providers: [ + { + provide: EXTENSIONS_IDENTIFIER, + useValue: identifier, + }, + { + provide: LocalizationService, + useValue: { currentLang: 'en' }, + }, + ], + }); + + beforeEach(() => { + spectator = createService(); + const props = FormProp.createMany([ + { + type: ePropType.String, + name: 'foo', + validators: () => [Validators.required], + defaultValue: 'bar', + }, + { + type: ePropType.Boolean, + name: 'bool', + }, + { + type: ePropType.Date, + name: 'date', + }, + { + type: ePropType.DateTime, + name: 'dateTime', + }, + { + type: ePropType.Time, + name: 'time', + }, + ]); + + spectator.service.createFormProps + .get(identifier) + .addContributor(propList => propList.addManyTail(props)); + spectator.service.editFormProps + .get(identifier) + .addContributor(propList => propList.addManyTail(props)); + + const generator = getInjected(spectator); + injector = { + get: () => generator.next().value as any, + }; + }); + + it('should return a blank FormGroup instance', () => { + const data = new FormPropData(injector, null); + + const formGroup = generateFormFromProps(data); + expect(formGroup).toBeInstanceOf(FormGroup); + expect(formGroup.value.foo).toBe('bar'); + + const formControl = formGroup.get('foo'); + expect(formControl).toBeInstanceOf(FormControl); + expect(formControl.valid).toBe(true); + }); + + it('should return a prefilled FormGroup instance', () => { + const data = new FormPropData(injector, { id: 1, foo: null }); + + const formGroup = generateFormFromProps(data); + expect(formGroup).toBeInstanceOf(FormGroup); + expect(formGroup.value.foo).toBe(null); + + const formControl = formGroup.get('foo'); + expect(formControl).toBeInstanceOf(FormControl); + expect(formControl.invalid).toBe(true); + }); + + it('should add a FormGroup named extraProperties', () => { + const data = new FormPropData(injector, null); + + const formGroup = generateFormFromProps(data); + const extraPropertiesGroup = formGroup.get('extraProperties'); + expect(extraPropertiesGroup).toBeInstanceOf(FormGroup); + }); + + it('should add extraProperties to extraProperties FormGroup', () => { + const data = new FormPropData(injector, { + id: 1, + foo: undefined, + extraProperties: { + bool: true, + date: '03/30/2002', + dateTime: '2002-03-30 13:30:59Z', + time: '13:30:59', + }, + }); + + const formGroup = generateFormFromProps(data); + const extraPropertiesGroup = formGroup.get('extraProperties'); + expect(extraPropertiesGroup.value).toEqual({ + bool: true, + date: '2002-03-30', + dateTime: '2002-03-30T13:30:59.000Z', + time: '13:30', + }); + }); + }); +}); + +function* getInjected(spectator: SpectatorService) { + yield spectator.service; + yield spectator.inject(EXTENSIONS_IDENTIFIER); + yield spectator.inject(LocalizationService); +} + +interface Foo { + id: number; + foo: string; + extraProperties?: { + bool: boolean; + date: string; + dateTime: string; + time: string; + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/localization.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/localization.util.spec.ts new file mode 100644 index 0000000000..007b304b42 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/localization.util.spec.ts @@ -0,0 +1,34 @@ +import { ApplicationLocalizationConfigurationDto } from '@abp/ng.core'; +import { createDisplayNameLocalizationPipeKeyGenerator } from '../lib/utils/localization.util'; + +describe('Localization Utils', () => { + describe('#createDisplayNameLocalizationPipeKeyGenerator', () => { + const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator({ + values: { + Foo: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' }, + Default: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' }, + }, + defaultResourceName: 'Default', + currentCulture: null, + languages: [], + languageFilesMap: null, + languagesMap: null, + } as ApplicationLocalizationConfigurationDto); + + test.each` + displayName | fallback | expected + ${{ name: 'Bar', resource: 'Foo' }} | ${null} | ${'Foo::Bar'} + ${{ name: 'Baz', resource: 'Foo' }} | ${null} | ${'Baz'} + ${null} | ${{ name: 'Bar', resource: 'Foo' }} | ${'Foo::DisplayName:Bar'} + ${null} | ${{ name: 'Bar', resource: 'Default' }} | ${'Default::DisplayName:Bar'} + ${null} | ${{ name: 'Baz', resource: 'Default' }} | ${'Baz'} + `( + 'should return $expected when diplay name is $displayName and fallback is $fallback', + ({ displayName, fallback, expected }) => { + const result = generateDisplayName(displayName, fallback); + + expect(result).toBe(expected); + }, + ); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.spec.ts new file mode 100644 index 0000000000..adaaea677d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.spec.ts @@ -0,0 +1,22 @@ +import { PropData } from '../lib/models/props'; + +describe('PropData', () => { + describe('#data', () => { + it('should return record and getInjected', () => { + const spy = jest.fn(); + class Data extends PropData { + index = 0; + record = 'X'; + getInjected = spy; + } + + const data = new Data(); + data.data.getInjected(null); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(null); + expect(data.data.index).toBe(0); + expect(data.data.record).toBe('X'); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.util.spec.ts new file mode 100644 index 0000000000..0c18d71eb5 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/props.util.spec.ts @@ -0,0 +1,72 @@ +import { + EntityProp, + EntityPropContributorCallbacks, + EntityPropDefaults, + EntityPropsFactory, +} from '../lib/models/entity-props'; +import { PropData } from '../lib/models/props'; +import { createExtraPropertyValueResolver, mergeWithDefaultProps } from '../lib/utils/props.util'; + +class MockPropData extends PropData { + getInjected: PropData['getInjected']; + + constructor(public readonly record: R) { + super(); + } +} + +describe('Entity Prop Utils', () => { + describe('#createExtraPropertyValueResolver', () => { + it('should return a resolver that resolves an observable value from extraProperties', async () => { + const valueResolver = createExtraPropertyValueResolver('foo'); + const propData = new MockPropData({ extraProperties: { foo: 'bar' } }); + + const bar = await valueResolver(propData).toPromise(); + expect(bar).toBe('bar'); + }); + }); + + describe('#mergeEntityProps', () => { + let entityProps: EntityPropsFactory; + + beforeEach(() => { + entityProps = new EntityPropsFactory(); + }); + + it('should merge default props with prop contributors', () => { + const defaults: EntityPropDefaults = { + x: [(1 as any) as EntityProp, (2 as any) as EntityProp, (3 as any) as EntityProp], + y: [(1 as any) as EntityProp, (2 as any) as EntityProp, (3 as any) as EntityProp], + }; + + const contributors1: EntityPropContributorCallbacks = { + x: [ + propList => { + const x2 = propList.dropByIndex(1); // 1 <-> 3 + propList.addHead(x2.value); // 2 <-> 1 <-> 3 + }, + propList => { + propList.dropTail(); // 2 <-> 1 + }, + ], + }; + + const contributors2: EntityPropContributorCallbacks = { + y: [ + propList => { + const y2 = propList.dropByIndex(1); // 1 <-> 3 + propList.addTail(y2.value); // 1 <-> 3 <-> 2 + }, + propList => { + propList.dropHead(); // 3 <-> 2 + }, + ], + }; + + mergeWithDefaultProps(entityProps, defaults, contributors1, contributors2); + + expect(entityProps.get('x').props.toString()).toBe('2 <-> 1'); + expect(entityProps.get('y').props.toString()).toBe('3 <-> 2'); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/state.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/state.util.spec.ts new file mode 100644 index 0000000000..f9538a25eb --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/state.util.spec.ts @@ -0,0 +1,369 @@ +import { ConfigStateService } from '@abp/ng.core'; +import { of } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ePropType } from '../lib/enums/props.enum'; +import { EntityPropList } from '../lib/models/entity-props'; +import { FormPropList } from '../lib/models/form-props'; +import { ObjectExtensions } from '../lib/models/object-extensions'; +import { + getObjectExtensionEntitiesFromStore, + mapEntitiesToContributors, +} from '../lib/utils/state.util'; + +const configState = new ConfigStateService(); +configState.setState(createMockState() as any); + +describe('State Utils', () => { + describe('#getObjectExtensionEntitiesFromStore', () => { + it('should return observable entities of an existing module', async () => { + const entities = await getObjectExtensionEntitiesFromStore( + configState, + 'Identity', + ).toPromise(); + expect('Role' in entities).toBe(true); + }); + + it('should return observable empty object if module does not exist', async () => { + const entities = await getObjectExtensionEntitiesFromStore(configState, 'Saas').toPromise(); + expect(entities).toEqual({}); + }); + + it('should not emit when object extensions do not exist', done => { + const emptyConfigState = new ConfigStateService(); + const emit = jest.fn(); + + getObjectExtensionEntitiesFromStore(emptyConfigState, 'Identity').subscribe(emit); + + setTimeout(() => { + expect(emit).not.toHaveBeenCalled(); + done(); + }, 1000); + }); + }); + + describe('#mapEntitiesToContributors', () => { + it('should return contributors from given entities', async () => { + const contributors = await of(createMockEntities()) + .pipe(mapEntitiesToContributors(configState, 'AbpIdentity'), take(1)) + .toPromise(); + + const propList = new EntityPropList(); + contributors.prop.Role.forEach(callback => callback(propList)); + + expect(propList.length).toBe(4); + expect(propList.head.value.name).toBe('Title'); + expect(propList.head.next.value.name).toBe('IsHero'); + expect(propList.head.next.next.value.name).toBe('MyEnum'); + expect(propList.head.next.next.next.value.name).toBe('Foo_Text'); + + const createFormList = new FormPropList(); + contributors.createForm.Role.forEach(callback => callback(createFormList)); + + expect(createFormList.length).toBe(4); + expect(createFormList.head.value.name).toBe('Title'); + expect(createFormList.head.next.value.name).toBe('MyEnum'); + expect(createFormList.head.next.next.value.name).toBe('Foo'); + expect(createFormList.head.next.next.next.value.name).toBe('Foo_Text'); + + const editFormList = new FormPropList(); + contributors.editForm.Role.forEach(callback => callback(editFormList)); + + expect(editFormList.length).toBe(4); + expect(editFormList.head.value.name).toBe('Title'); + expect(editFormList.head.next.value.name).toBe('IsHero'); + expect(editFormList.head.next.next.value.name).toBe('Foo'); + expect(editFormList.head.next.next.next.value.name).toBe('Foo_Text'); + }); + }); +}); + +function createMockState() { + return { + objectExtensions: { + modules: { + Identity: { + entities: createMockEntities(), + configuration: null, + }, + }, + enums: { + 'MyCompanyName.MyProjectName.MyEnum': { + fields: [ + { + name: 'MyEnumValue0', + value: 0, + }, + { + name: 'MyEnumValue1', + value: 1, + }, + { + name: 'MyEnumValue2', + value: 2, + }, + ], + localizationResource: null, + }, + }, + }, + localization: { + values: { + Default: {}, + AbpIdentity: {}, + }, + defaultResourceName: 'Default', + currentCulture: null, + languages: [], + }, + }; +} + +function createMockEntities(): Record { + return { + Role: { + properties: { + Title: { + type: 'System.String', + typeSimple: ePropType.String, + displayName: null, + api: { + onGet: { + isAvailable: true, + }, + onCreate: { + isAvailable: true, + }, + onUpdate: { + isAvailable: true, + }, + }, + ui: { + onTable: { + isSortable: true, + isVisible: true, + }, + onCreateForm: { + isVisible: true, + }, + onEditForm: { + isVisible: true, + }, + lookup: null, + }, + attributes: [ + { + typeSimple: 'required', + config: {}, + }, + { + typeSimple: 'stringLength', + config: { + maximumLength: 20, + minimumLength: 2, + }, + }, + ], + configuration: {}, + defaultValue: null, + }, + IsHero: { + type: 'System.Boolean', + typeSimple: ePropType.Boolean, + displayName: null, + api: { + onGet: { + isAvailable: true, + }, + onCreate: { + isAvailable: true, + }, + onUpdate: { + isAvailable: true, + }, + }, + ui: { + onTable: { + isSortable: false, + isVisible: true, + }, + onCreateForm: { + isVisible: false, + }, + onEditForm: { + isVisible: true, + }, + lookup: null, + }, + attributes: [], + configuration: {}, + defaultValue: null, + }, + AsOf: { + type: 'System.Date', + typeSimple: ePropType.Date, + displayName: { + name: 'Active as of', + resource: 'AbpIdentity', + }, + api: { + onGet: { + isAvailable: true, + }, + onCreate: { + isAvailable: true, + }, + onUpdate: { + isAvailable: true, + }, + }, + ui: { + onTable: { + isSortable: false, + isVisible: false, + }, + onCreateForm: { + isVisible: false, + }, + onEditForm: { + isVisible: false, + }, + lookup: null, + }, + attributes: [], + configuration: {}, + defaultValue: null, + }, + MyEnum: { + type: 'MyCompanyName.MyProjectName.MyEnum', + typeSimple: ePropType.Enum, + displayName: null, + api: { + onGet: { + isAvailable: true, + }, + onCreate: { + isAvailable: true, + }, + onUpdate: { + isAvailable: true, + }, + }, + ui: { + onTable: { + isSortable: false, + isVisible: true, + }, + onCreateForm: { + isVisible: true, + }, + onEditForm: { + isVisible: false, + }, + lookup: null, + }, + attributes: [ + { + typeSimple: 'required', + config: { + allowEmptyStrings: false, + }, + }, + { + typeSimple: 'enumDataType', + config: { + enumType: 'MyCompanyName.MyProjectName.MyEnum', + dataType: 'Custom', + customDataType: 'Enumeration', + }, + }, + ], + configuration: {}, + defaultValue: 2, + }, + Foo: { + type: 'System.String', + typeSimple: ePropType.String, + displayName: null, + api: { + onGet: { + isAvailable: false, + }, + onCreate: { + isAvailable: true, + }, + onUpdate: { + isAvailable: true, + }, + }, + ui: { + onTable: { + isVisible: false, + }, + onCreateForm: { + isVisible: true, + }, + onEditForm: { + isVisible: true, + }, + lookup: { + url: '/api/identity/roles', + resultListPropertyName: 'items', + displayPropertyName: 'text', + valuePropertyName: 'id', + filterParamName: 'filter', + }, + }, + attributes: [], + configuration: {}, + defaultValue: null, + }, + Foo_Text: { + type: 'System.String', + typeSimple: ePropType.String, + displayName: { + name: 'Foo', + resource: '_', + }, + api: { + onGet: { + isAvailable: true, + }, + onCreate: { + isAvailable: true, + }, + onUpdate: { + isAvailable: true, + }, + }, + ui: { + onTable: { + isVisible: true, + }, + onCreateForm: { + isVisible: true, + }, + onEditForm: { + isVisible: true, + }, + lookup: { + url: null, + resultListPropertyName: 'items', + displayPropertyName: 'text', + valuePropertyName: 'id', + filterParamName: 'filter', + }, + }, + attributes: [], + configuration: {}, + defaultValue: null, + }, + }, + configuration: {}, + }, + User: { + properties: null, + configuration: {}, + }, + ClaimType: null, + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/time.adapter.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/time.adapter.spec.ts new file mode 100644 index 0000000000..40d7fab58b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/time.adapter.spec.ts @@ -0,0 +1,36 @@ +import { TimeAdapter } from '../lib/adapters/time.adapter'; + +describe('Time Adapter', () => { + const adapter = new TimeAdapter(); + + describe('#fromModel', () => { + const date = new Date(); + const hour = date.getHours(); + const minute = date.getMinutes(); + const second = date.getSeconds(); + + test.each` + param | expected + ${undefined} | ${null} + ${null} | ${null} + ${'x'} | ${null} + ${'13:30:45'} | ${{ hour: 13, minute: 30, second: 45 }} + ${'13:30'} | ${{ hour: 13, minute: 30, second: 0 }} + ${date} | ${{ hour, minute, second }} + `('should return $expected when $param is given', ({ param, expected }) => { + expect(adapter.fromModel(param)).toEqual(expected); + }); + }); + + describe('#toModel', () => { + test.each` + param | expected + ${undefined} | ${''} + ${null} | ${''} + ${{ hour: 13, minute: 30, second: 0 }} | ${'13:30'} + ${{ hour: 13, minute: 30, second: 45 }} | ${'13:30'} + `('should return $expected when $param is given', ({ param, expected }) => { + expect(adapter.toModel(param)).toEqual(expected); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/toolbar-actions.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/toolbar-actions.spec.ts new file mode 100644 index 0000000000..a966628a56 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/toolbar-actions.spec.ts @@ -0,0 +1,216 @@ +import { LinkedList } from '@abp/utils'; +import { + ToolbarAction, + ToolbarActionContributorCallback, + ToolbarActionList, + ToolbarActions, + ToolbarActionsFactory, + ToolbarComponent, +} from '../lib/models/toolbar-actions'; + +describe('ToolbarActionList', () => { + it('should inherit from LinkedList', () => { + expect(new ToolbarActionList() instanceof LinkedList).toBe(true); + }); +}); + +describe('ToolbarActions', () => { + const add1toTail: ToolbarActionContributorCallback = actionList => actionList.addTail(1 as any); + const add2toTail: ToolbarActionContributorCallback = actionList => actionList.addTail(2 as any); + const add3toTail: ToolbarActionContributorCallback = actionList => actionList.addTail(3 as any); + const dropIndex1: ToolbarActionContributorCallback = actionList => actionList.dropByIndex(1); + + describe('#actions', () => { + test.each` + callbackList | expected + ${[]} | ${''} + ${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} + ${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} + ${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} + `( + 'should return $expected when given callbackList is $callbackList', + ({ callbackList, expected }) => { + const creator = new ToolbarActions(callbackList); + expect(creator.actions.toString()).toBe(expected); + }, + ); + }); + + describe('#addContributor', () => { + const creator = new ToolbarActions([]); + + test.each` + callbackList | callback | expected + ${[]} | ${add1toTail} | ${'1'} + ${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} + ${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} + ${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} + `( + 'should set actions to $expected when callbackList is $callbackList and given callback is $callback', + ({ callback, expected }) => { + creator.addContributor(callback); + expect(creator.actions.toString()).toBe(expected); + }, + ); + }); +}); + +describe('ToolbarActionsFactory', () => { + describe('#get', () => { + it('should create and return an ToolbarActions instance', () => { + const entityActions = new ToolbarActionsFactory(); + const creator = entityActions.get(''); + + expect(creator).toBeInstanceOf(ToolbarActions); + }); + + it('should store and pass contributorCallbacks to ToolbarActionsCreator instance it returns', () => { + const entityActions = new ToolbarActionsFactory(); + const creatorX1 = entityActions.get('X'); + + expect(creatorX1).toBeInstanceOf(ToolbarActions); + expect(creatorX1.actions.toArray()).toHaveLength(0); + + creatorX1.addContributor(actionList => actionList.addTail(1 as any)); + + const creatorX2 = entityActions.get('X'); + expect(creatorX2.actions.toArray()).toHaveLength(1); + }); + }); +}); + +describe('ToolbarAction', () => { + it('should be created when options object is passed as argument', () => { + const options = { + text: 'TEXT', + action: () => 'ACTION', + permission: 'PERMISSION', + visible: () => false, + icon: 'ICON', + }; + + const action = new ToolbarAction(options); + + expect(action.text).toBe(options.text); + expect(action.action(null)).toBe(options.action()); + expect(action.permission).toBe(options.permission); + expect(action.visible(null)).toBe(options.visible()); + expect(action.icon).toBe(options.icon); + }); + + it('should be created when only required options are passed', () => { + const options = { + text: 'TEXT', + action: () => 'ACTION', + }; + + const action = new ToolbarAction(options); + + expect(action.text).toBe(options.text); + expect(action.action).toBe(options.action); + expect(action.permission).toBeUndefined(); + expect(action.visible(null)).toBe(true); + expect(action.icon).toBe(''); + }); + + describe('#create', () => { + it('should return a new instance from given options', () => { + const options = { + text: 'TEXT', + action: () => 'ACTION', + }; + + const action = ToolbarAction.create(options); + + expect(action).toBeInstanceOf(ToolbarAction); + expect(action.text).toBe(options.text); + expect(action.action).toBe(options.action); + }); + }); + + describe('#createMany', () => { + it('should return multiple instances from given options array', () => { + const options1 = { + text: 'TEXT 1', + action: () => 'ACTION 1', + }; + const options2 = { + text: 'TEXT 2', + action: () => 'ACTION 2', + }; + + const [action1, action2] = ToolbarAction.createMany([options1, options2]); + + expect(action1).toBeInstanceOf(ToolbarAction); + expect(action1.text).toBe(options1.text); + expect(action1.action).toBe(options1.action); + expect(action2).toBeInstanceOf(ToolbarAction); + expect(action2.text).toBe(options2.text); + expect(action2.action).toBe(options2.action); + }); + }); +}); +class MockComponent1 {} +class MockComponent2 {} + +describe('ToolbarComponent', () => { + it('should be created when options object is passed as argument', () => { + const options = { + component: MockComponent1, + action: () => 'ACTION', + permission: 'PERMISSION', + visible: () => false, + }; + + const action = new ToolbarComponent(options); + + expect(action.component).toBe(options.component); + expect(action.action).toBe(options.action); + expect(action.permission).toBe(options.permission); + expect(action.visible).toBe(options.visible); + }); + + it('should be created when only required options are passed', () => { + const options = { + component: MockComponent1, + }; + + const action = new ToolbarComponent(options); + + expect(action.component).toBe(options.component); + expect(action.action()).toBeUndefined(); + expect(action.permission).toBeUndefined(); + expect(action.visible()).toBe(true); + }); + + describe('#create', () => { + it('should return a new instance from given options', () => { + const options = { + component: MockComponent1, + }; + + const action = ToolbarComponent.create(options); + + expect(action).toBeInstanceOf(ToolbarComponent); + expect(action.component).toBe(options.component); + }); + }); + + describe('#createMany', () => { + it('should return multiple instances from given options array', () => { + const options1 = { + component: MockComponent1, + }; + const options2 = { + component: MockComponent2, + }; + + const [action1, action2] = ToolbarComponent.createMany([options1, options2]); + + expect(action1).toBeInstanceOf(ToolbarComponent); + expect(action1.component).toBe(options1.component); + expect(action2).toBeInstanceOf(ToolbarComponent); + expect(action2.component).toBe(options2.component); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/typeahead.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/typeahead.util.spec.ts new file mode 100644 index 0000000000..479b1d4fcf --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/typeahead.util.spec.ts @@ -0,0 +1,72 @@ +import { ExtensionPropertyUiLookupDto } from '@abp/ng.core'; +import { of } from 'rxjs'; +import { createTypeaheadOptions } from '../lib/utils/typeahead.util'; + +const lookup: ExtensionPropertyUiLookupDto = { + url: 'url', + resultListPropertyName: 'list', + displayPropertyName: 'text', + valuePropertyName: 'id', + filterParamName: 'filter', +}; + +describe('Typeahead Utils', () => { + describe('#createTypeaheadOptions', () => { + it('should return observable empty array when search text does not exist', async () => { + const list = await createTypeaheadOptions(null)(null, null).toPromise(); + expect(list).toEqual([]); + }); + + it('should call request method of RestService with lookup url, filter param and search text', async () => { + const data = createData([]); + const service = data.getInjected(); + await createTypeaheadOptions(lookup)(data, 'x').toPromise(); + expect(service.request).toHaveBeenCalledTimes(1); + expect(service.request).toHaveBeenCalledWith( + { + method: 'GET', + url: 'url', + params: { + filter: 'x', + }, + }, + { apiName: 'Default' }, + ); + }); + + it('should return options based on given lookup data', async () => { + const data = createData([ + { + text: 'foo', + id: 'bar', + }, + { + text: 'baz', + id: 'qux', + }, + ]); + + const options = await createTypeaheadOptions(lookup)(data, 'x').toPromise(); + expect(options).toEqual([ + { + key: 'foo', + value: 'bar', + }, + { + key: 'baz', + value: 'qux', + }, + ]); + }); + }); +}); + +function createData(list: { text: string; id: string }[]): any { + const service = { request: jest.fn(() => of({ list })) }; + + return { + getInjected: () => service, + index: 0, + record: null, + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/validation.util.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/validation.util.spec.ts new file mode 100644 index 0000000000..abf4a6c486 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/src/tests/validation.util.spec.ts @@ -0,0 +1,20 @@ +import { Validators } from '@angular/forms'; +import { ObjectExtensions } from '../lib/models/object-extensions'; +import { getValidatorsFromProperty } from '../lib/utils/validation.util'; + +describe('Validation Utils', () => { + describe('#getValidatorsFromProperty', () => { + it('should return a list of validators derived from property attributes', () => { + const property = { + attributes: [ + { + typeSimple: 'emailAddress', + config: {}, + }, + ], + } as ObjectExtensions.ExtensionPropertyDto; + + expect(getValidatorsFromProperty(property)[0]).toBe(Validators.email); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/lib/ui-extensions-testing.module.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/lib/ui-extensions-testing.module.ts new file mode 100644 index 0000000000..593cce3733 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/lib/ui-extensions-testing.module.ts @@ -0,0 +1,8 @@ +import { BaseUiExtensionsModule } from '@abp/ng.theme.shared/extensions'; +import { NgModule } from '@angular/core'; + +@NgModule({ + exports: [BaseUiExtensionsModule], + imports: [BaseUiExtensionsModule], +}) +export class UiExtensionsTestingModule {} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/public-api.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/public-api.ts new file mode 100644 index 0000000000..d524c9cd0c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/src/public-api.ts @@ -0,0 +1 @@ +export * from './lib/ui-extensions-testing.module'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/ssng-package.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/ssng-package.json new file mode 100644 index 0000000000..10757e7f04 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/extensions/testing/ssng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/theme-shared/extensions/testing", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/jest.config.js b/npm/ng-packs/nx/ng-packs/libs/theme-shared/jest.config.js new file mode 100644 index 0000000000..04cdf90d4a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/jest.config.js @@ -0,0 +1,20 @@ +module.exports = { + displayName: 'theme-shared', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + }, + coverageDirectory: '../../coverage/libs/theme-shared', + transform: { + '^.+\\.(ts|js|html)$': 'jest-preset-angular', + }, + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/ng-package.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/ng-package.json new file mode 100644 index 0000000000..69a7ee91b9 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/ng-package.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/libs/theme-shared", + "lib": { + "entryFile": "src/public-api.ts" + }, + "allowedNonPeerDependencies": [ + "@abp/ng.core", + "@fortawesome/fontawesome-free", + "@ng-bootstrap/ng-bootstrap", + "@ngx-validate/core", + "@swimlane/ngx-datatable", + "bootstrap", + "chart.js" + ] +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/package.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/package.json new file mode 100644 index 0000000000..557782119a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/package.json @@ -0,0 +1,22 @@ +{ + "name": "@abp/ng.theme.shared", + "version": "4.4.0", + "homepage": "https://abp.io", + "repository": { + "type": "git", + "url": "https://github.com/abpframework/abp.git" + }, + "dependencies": { + "@abp/ng.core": "~4.4.0", + "@fortawesome/fontawesome-free": "^5.14.0", + "@ng-bootstrap/ng-bootstrap": "^7.0.0", + "@ngx-validate/core": "^0.0.13", + "@swimlane/ngx-datatable": "^17.1.0", + "bootstrap": "~4.6.0", + "chart.js": "^2.9.3", + "tslib": "^2.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/bounce.animations.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/bounce.animations.ts new file mode 100644 index 0000000000..c539725a0a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/bounce.animations.ts @@ -0,0 +1,23 @@ +import { animate, animation, keyframes, style } from '@angular/animations'; + +export const bounceIn = animation( + [ + style({ opacity: '0', display: '{{ display }}' }), + animate( + '{{ time}} {{ easing }}', + keyframes([ + style({ opacity: '0', transform: '{{ transform }} scale(0.0)', offset: 0 }), + style({ opacity: '0', transform: '{{ transform }} scale(0.8)', offset: 0.5 }), + style({ opacity: '1', transform: '{{ transform }} scale(1.0)', offset: 1 }) + ]) + ) + ], + { + params: { + time: '350ms', + easing: 'cubic-bezier(.7,.31,.72,1.47)', + display: 'block', + transform: 'translate(-50%, -50%)' + } + } +); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/collapse.animations.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/collapse.animations.ts new file mode 100644 index 0000000000..945ca6ab08 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/collapse.animations.ts @@ -0,0 +1,90 @@ +import { + animate, + animation, + trigger, + state, + style, + transition, + useAnimation, +} from '@angular/animations'; + +export const collapseY = animation( + [ + style({ height: '*', overflow: 'hidden', 'box-sizing': 'border-box' }), + animate('{{ time }} {{ easing }}', style({ height: '0', padding: '0px' })), + ], + { params: { time: '350ms', easing: 'ease' } }, +); + +export const collapseYWithMargin = animation( + [ + style({ 'margin-top': '0' }), + animate('{{ time }} {{ easing }}', style({ 'margin-left': '-100%' })), + ], + { + params: { time: '500ms', easing: 'ease' }, + }, +); + +export const collapseX = animation( + [ + style({ width: '*', overflow: 'hidden', 'box-sizing': 'border-box' }), + animate('{{ time }} {{ easing }}', style({ width: '0', padding: '0px' })), + ], + { params: { time: '350ms', easing: 'ease' } }, +); + +export const expandY = animation( + [ + style({ height: '0', overflow: 'hidden', 'box-sizing': 'border-box' }), + animate('{{ time }} {{ easing }}', style({ height: '*', padding: '*' })), + ], + { params: { time: '350ms', easing: 'ease' } }, +); + +export const expandYWithMargin = animation( + [ + style({ 'margin-top': '-100%' }), + animate('{{ time }} {{ easing }}', style({ 'margin-top': '0' })), + ], + { + params: { time: '500ms', easing: 'ease' }, + }, +); + +export const expandX = animation( + [ + style({ width: '0', overflow: 'hidden', 'box-sizing': 'border-box' }), + animate('{{ time }} {{ easing }}', style({ width: '*', padding: '*' })), + ], + { params: { time: '350ms', easing: 'ease' } }, +); + +export const collapse = trigger('collapse', [ + state('collapsed', style({ height: '0', overflow: 'hidden' })), + state('expanded', style({ height: '*', overflow: 'hidden' })), + transition('expanded => collapsed', useAnimation(collapseY)), + transition('collapsed => expanded', useAnimation(expandY)), +]); + +export const collapseWithMargin = trigger('collapseWithMargin', [ + state('collapsed', style({ 'margin-top': '-100%' })), + state('expanded', style({ 'margin-top': '0' })), + transition('expanded => collapsed', useAnimation(collapseYWithMargin), { + params: { time: '400ms', easing: 'linear' }, + }), + transition('collapsed => expanded', useAnimation(expandYWithMargin)), +]); + +export const collapseLinearWithMargin = trigger('collapseLinearWithMargin', [ + state('collapsed', style({ 'margin-top': '-100vh' })), + state('expanded', style({ 'margin-top': '0' })), + transition( + 'expanded => collapsed', + useAnimation(collapseYWithMargin, { params: { time: '200ms', easing: 'linear' } }), + ), + transition( + 'collapsed => expanded', + useAnimation(expandYWithMargin, { params: { time: '250ms', easing: 'linear' } }), + ), +]); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/fade.animations.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/fade.animations.ts new file mode 100644 index 0000000000..3321974a77 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/fade.animations.ts @@ -0,0 +1,74 @@ +import { animate, animation, style } from '@angular/animations'; + +export const fadeIn = animation([style({ opacity: '0' }), animate('{{ time}} {{ easing }}', style({ opacity: '1' }))], { + params: { time: '350ms', easing: 'ease' }, +}); + +export const fadeOut = animation( + [style({ opacity: '1' }), animate('{{ time}} {{ easing }}', style({ opacity: '0' }))], + { params: { time: '350ms', easing: 'ease' } }, +); + +export const fadeInDown = animation( + [ + style({ opacity: '0', transform: '{{ transform }} translateY(-20px)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateY(0)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeInUp = animation( + [ + style({ opacity: '0', transform: '{{ transform }} translateY(20px)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateY(0)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeInLeft = animation( + [ + style({ opacity: '0', transform: '{{ transform }} translateX(20px)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateX(0)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeInRight = animation( + [ + style({ opacity: '0', transform: '{{ transform }} translateX(-20px)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateX(0)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeOutDown = animation( + [ + style({ opacity: '1', transform: '{{ transform }} translateY(0)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateY(20px)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeOutUp = animation( + [ + style({ opacity: '1', transform: '{{ transform }} translateY(0)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateY(-20px)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeOutLeft = animation( + [ + style({ opacity: '1', transform: '{{ transform }} translateX(0)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateX(20px)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); + +export const fadeOutRight = animation( + [ + style({ opacity: '1', transform: '{{ transform }} translateX(0)' }), + animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateX(-20px)' })), + ], + { params: { time: '350ms', easing: 'ease', transform: '' } }, +); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/index.ts new file mode 100644 index 0000000000..d625d76d8e --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/index.ts @@ -0,0 +1,6 @@ +export * from './bounce.animations'; +export * from './collapse.animations'; +export * from './fade.animations'; +export * from './modal.animations'; +export * from './slide.animations'; +export * from './toast.animations'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/modal.animations.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/modal.animations.ts new file mode 100644 index 0000000000..da979f331b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/modal.animations.ts @@ -0,0 +1,12 @@ +import { transition, trigger, useAnimation } from '@angular/animations'; +import { fadeIn, fadeInDown, fadeOut } from './fade.animations'; + +export const fadeAnimation = trigger('fade', [ + transition(':enter', useAnimation(fadeIn)), + transition(':leave', useAnimation(fadeOut)), +]); + +export const dialogAnimation = trigger('dialog', [ + transition(':enter', useAnimation(fadeInDown)), + transition(':leave', useAnimation(fadeOut)), +]); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/slide.animations.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/slide.animations.ts new file mode 100644 index 0000000000..32ff155e55 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/slide.animations.ts @@ -0,0 +1,7 @@ +import { animate, state, style, transition, trigger, query } from '@angular/animations'; +export const slideFromBottom = trigger('slideFromBottom', [ + transition('* <=> *', [ + style({ 'margin-top': '20px', opacity: '0' }), + animate('0.2s ease-out', style({ opacity: '1', 'margin-top': '0px' })), + ]), +]); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/toast.animations.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/toast.animations.ts new file mode 100644 index 0000000000..7ac6a7519b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/animations/toast.animations.ts @@ -0,0 +1,17 @@ +import { animate, query, style, transition, trigger } from '@angular/animations'; + +export const toastInOut = trigger('toastInOut', [ + transition('* <=> *', [ + query( + ':enter', + [ + style({ opacity: 0, transform: 'translateY(20px)' }), + animate('350ms ease', style({ opacity: 1, transform: 'translateY(0)' })), + ], + { optional: true }, + ), + query(':leave', animate('450ms ease', style({ opacity: 0 })), { + optional: true, + }), + ]), +]); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html new file mode 100644 index 0000000000..da17512ea1 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html @@ -0,0 +1,13 @@ + diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000000..65c3c0a0af --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,58 @@ +import { + ABP, + getRoutePath, + RouterEvents, + RoutesService, + SubscriptionService, + TreeNode, +} from '@abp/ng.core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { map, startWith } from 'rxjs/operators'; +import { eThemeSharedRouteNames } from '../../enums'; + +@Component({ + selector: 'abp-breadcrumb', + templateUrl: './breadcrumb.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SubscriptionService], +}) +export class BreadcrumbComponent implements OnInit { + segments: Partial[] = []; + + constructor( + public readonly cdRef: ChangeDetectorRef, + private router: Router, + private routes: RoutesService, + private subscription: SubscriptionService, + private routerEvents: RouterEvents, + ) {} + + ngOnInit(): void { + this.subscription.addOne( + this.routerEvents.getNavigationEvents('End').pipe( + // tslint:disable-next-line:deprecation + startWith(null), + map(() => this.routes.search({ path: getRoutePath(this.router) })), + ), + route => { + this.segments = []; + if (route) { + let node = { parent: route } as TreeNode; + + while (node.parent) { + node = node.parent; + const { parent, children, isLeaf, ...segment } = node; + if (!isAdministration(segment)) this.segments.unshift(segment); + } + + this.cdRef.detectChanges(); + } + }, + ); + } +} + +function isAdministration(route: ABP.Route) { + return route.name === eThemeSharedRouteNames.Administration; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/button/button.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/button/button.component.ts new file mode 100644 index 0000000000..83bef06f26 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/button/button.component.ts @@ -0,0 +1,82 @@ +import { + Component, + EventEmitter, + Input, + Output, + ViewChild, + ElementRef, + Renderer2, + OnInit, +} from '@angular/core'; +import { ABP } from '@abp/ng.core'; + +@Component({ + selector: 'abp-button', + template: ` + + `, +}) +export class ButtonComponent implements OnInit { + @Input() + buttonId = ''; + + @Input() + buttonClass = 'btn btn-primary'; + + @Input() + buttonType = 'button'; + + @Input() + iconClass: string; + + @Input() + loading = false; + + @Input() + disabled = false; + + @Input() + attributes: ABP.Dictionary; + + // tslint:disable + @Output() readonly click = new EventEmitter(); + + @Output() readonly focus = new EventEmitter(); + + @Output() readonly blur = new EventEmitter(); + // tslint:enable + + @Output() readonly abpClick = new EventEmitter(); + + @Output() readonly abpFocus = new EventEmitter(); + + @Output() readonly abpBlur = new EventEmitter(); + + @ViewChild('button', { static: true }) + buttonRef: ElementRef; + + get icon(): string { + return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`; + } + + constructor(private renderer: Renderer2) {} + + ngOnInit() { + if (this.attributes) { + Object.keys(this.attributes).forEach(key => { + this.renderer.setAttribute(this.buttonRef.nativeElement, key, this.attributes[key]); + }); + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.html new file mode 100644 index 0000000000..211614c90d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.html @@ -0,0 +1,11 @@ +
+ +
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.ts new file mode 100644 index 0000000000..e019840b7c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/chart/chart.component.ts @@ -0,0 +1,142 @@ +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + Output, + ChangeDetectorRef, +} from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { chartJsLoaded$ } from '../../utils/widget-utils'; +declare const Chart: any; + +@Component({ + selector: 'abp-chart', + templateUrl: './chart.component.html', +}) +export class ChartComponent implements AfterViewInit, OnDestroy { + @Input() type: string; + + @Input() options: any = {}; + + @Input() plugins: any[] = []; + + @Input() width: string; + + @Input() height: string; + + @Input() responsive = true; + + // tslint:disable-next-line: no-output-on-prefix + @Output() readonly onDataSelect: EventEmitter = new EventEmitter(); + + @Output() readonly initialized = new BehaviorSubject(this); + + private _initialized: boolean; + + _data: any; + + chart: any; + + constructor(public el: ElementRef, private cdRef: ChangeDetectorRef) {} + + @Input() get data(): any { + return this._data; + } + + set data(val: any) { + this._data = val; + this.reinit(); + } + + get canvas() { + return this.el.nativeElement.children[0].children[0]; + } + + get base64Image() { + return this.chart.toBase64Image(); + } + + ngAfterViewInit() { + chartJsLoaded$.subscribe(() => { + this.testChartJs(); + + this.initChart(); + this._initialized = true; + }); + } + + testChartJs() { + try { + // tslint:disable-next-line: no-unused-expression + Chart; + } catch (error) { + throw new Error(`Chart is not found. Import the Chart from app.module like shown below: + import('chart.js'); + `); + } + } + + onCanvasClick = event => { + if (this.chart) { + const element = this.chart.getElementAtEvent(event); + const dataset = this.chart.getDatasetAtEvent(event); + if (element && element.length && dataset) { + this.onDataSelect.emit({ + originalEvent: event, + element: element[0], + dataset, + }); + } + } + }; + + initChart = () => { + const opts = this.options || {}; + opts.responsive = this.responsive; + + // allows chart to resize in responsive mode + if (opts.responsive && (this.height || this.width)) { + opts.maintainAspectRatio = false; + } + + this.chart = new Chart(this.canvas, { + type: this.type, + data: this.data, + options: this.options, + plugins: this.plugins, + }); + + this.cdRef.detectChanges(); + }; + + generateLegend = () => { + if (this.chart) { + return this.chart.generateLegend(); + } + }; + + refresh = () => { + if (this.chart) { + this.chart.update(); + this.cdRef.detectChanges(); + } + }; + + reinit = () => { + if (this.chart) { + this.chart.destroy(); + this.initChart(); + } + }; + + ngOnDestroy() { + if (this.chart) { + this.chart.destroy(); + this._initialized = false; + this.chart = null; + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.html new file mode 100644 index 0000000000..47c555a16b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.html @@ -0,0 +1,39 @@ +
+
+
+
+ +
+
+

+

+
+ +
+
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.scss new file mode 100644 index 0000000000..a6d25a4946 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.scss @@ -0,0 +1,82 @@ +.confirmation { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1060; + .confirmation-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1061 !important; + } + .confirmation-dialog { + display: flex; + flex-direction: column; + margin: 20px auto; + padding: 0; + width: 450px; + min-height: 300px; + z-index: 1062 !important; + @media screen and (max-width: 500px) { + width: 90vw; + } + .icon-container { + display: flex; + align-items: center; + justify-content: center; + margin: 0 0 10px 0; + padding: 20px; + .icon { + width: 100px; + height: 100px; + stroke-width: 1; + font-size: 80px; + text-align: center; + } + } + .content { + flex-grow: 1; + display: block; + .title { + display: block; + margin: 0; + padding: 0; + font-size: 27px; + font-weight: 600; + text-align: center; + } + .message { + display: block; + margin: 10px auto; + padding: 20px; + font-size: 16px; + font-weight: 400; + text-align: center; + } + } + .footer { + display: flex; + align-items: center; + justify-content: flex-end; + margin: 10px 0 0 0; + padding: 20px; + width: 100%; + .confirmation-button { + display: inline-block; + margin: 0px 5px; + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + } + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.ts new file mode 100644 index 0000000000..13462612e6 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/confirmation/confirmation.component.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import { Confirmation } from '../../models/confirmation'; + +@Component({ + selector: 'abp-confirmation', + templateUrl: './confirmation.component.html', + styleUrls: ['./confirmation.component.scss'], +}) +export class ConfirmationComponent { + confirm = Confirmation.Status.confirm; + reject = Confirmation.Status.reject; + dismiss = Confirmation.Status.dismiss; + + confirmation$: ReplaySubject; + + clear: (status: Confirmation.Status) => void; + + close(status: Confirmation.Status) { + this.clear(status); + } + + getIconClass({ severity }: Confirmation.DialogData): string { + switch (severity) { + case 'info': + return 'fa-info-circle'; + case 'success': + return 'fa-check-circle'; + case 'warning': + return 'fa-exclamation-triangle'; + case 'error': + return 'fa-times-circle'; + default: + return 'fa-question-circle'; + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.html new file mode 100644 index 0000000000..046e8bb67a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.html @@ -0,0 +1,37 @@ +
+ + +
+
+
+

{{ statusText }} {{ title | abpLocalization }}

+
+ {{ details | abpLocalization }} +
+ +
+
+
+
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss new file mode 100644 index 0000000000..b9f7690a5b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss @@ -0,0 +1,14 @@ +.error { + position: fixed; + top: 0; + width: 100vw; + height: 100vh; + z-index: 999999; +} + +.centered { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts new file mode 100644 index 0000000000..fb1b2d4a6c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts @@ -0,0 +1,90 @@ +import { Config, SubscriptionService } from '@abp/ng.core'; +import { + AfterViewInit, + ApplicationRef, + Component, + ComponentFactoryResolver, + ElementRef, + EmbeddedViewRef, + Injector, + OnDestroy, + OnInit, + Type, + ViewChild, +} from '@angular/core'; +import { fromEvent, Subject } from 'rxjs'; +import { debounceTime, filter } from 'rxjs/operators'; +import snq from 'snq'; + +@Component({ + selector: 'abp-http-error-wrapper', + templateUrl: './http-error-wrapper.component.html', + styleUrls: ['http-error-wrapper.component.scss'], + providers: [SubscriptionService], +}) +export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit { + appRef: ApplicationRef; + + cfRes: ComponentFactoryResolver; + + injector: Injector; + + status = 0; + + title: Config.LocalizationParam = 'Oops!'; + + details: Config.LocalizationParam = 'Sorry, an error has occured.'; + + customComponent: Type = null; + + destroy$: Subject; + + hideCloseIcon = false; + + backgroundColor: string; + + isHomeShow = true; + + @ViewChild('container', { static: false }) + containerRef: ElementRef; + + get statusText(): string { + return this.status ? `[${this.status}]` : ''; + } + + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.backgroundColor = + snq(() => window.getComputedStyle(document.body).getPropertyValue('background-color')) || + '#fff'; + } + + ngAfterViewInit() { + if (this.customComponent) { + const customComponentRef = this.cfRes + .resolveComponentFactory(this.customComponent) + .create(this.injector); + customComponentRef.instance.errorStatus = this.status; + customComponentRef.instance.destroy$ = this.destroy$; + this.appRef.attachView(customComponentRef.hostView); + this.containerRef.nativeElement.appendChild( + (customComponentRef.hostView as EmbeddedViewRef).rootNodes[0], + ); + customComponentRef.changeDetectorRef.detectChanges(); + } + + const keyup$ = fromEvent(document, 'keyup').pipe( + debounceTime(150), + filter((key: KeyboardEvent) => key && key.key === 'Escape'), + ); + this.subscription.addOne(keyup$, () => this.destroy()); + } + + ngOnDestroy() {} + + destroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/index.ts new file mode 100644 index 0000000000..45036680be --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/index.ts @@ -0,0 +1,15 @@ +export * from './breadcrumb/breadcrumb.component'; +export * from './button/button.component'; +export * from './chart/chart.component'; +export * from './confirmation/confirmation.component'; +export * from './http-error-wrapper/http-error-wrapper.component'; +export * from './loader-bar/loader-bar.component'; +export * from './loading/loading.component'; +export * from './modal/modal.component'; +export * from './modal/modal-close.directive'; +export * from './modal/modal-ref.service'; +export * from './sort-order-icon/sort-order-icon.component'; +export * from './table-empty-message/table-empty-message.component'; +export * from './table/table.component'; +export * from './toast-container/toast-container.component'; +export * from './toast/toast.component'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss new file mode 100644 index 0000000000..19a4f2ee8f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.scss @@ -0,0 +1,24 @@ +.abp-loader-bar { + left: 0; + opacity: 0; + position: fixed; + top: 0; + transition: opacity 0.4s linear 0.4s; + z-index: 99999; + + &.is-loading { + opacity: 1; + transition: none; + } + + .abp-progress { + height: 3px; + left: 0; + position: fixed; + top: 0; + + &.progressing { + transition: width 0.4s ease; + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts new file mode 100644 index 0000000000..3fd01d1b4d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -0,0 +1,120 @@ +import { HttpWaitService, RouterWaitService, SubscriptionService } from '@abp/ng.core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { combineLatest, Subscription, timer } from 'rxjs'; + +@Component({ + selector: 'abp-loader-bar', + template: ` +
+
+
+ `, + styleUrls: ['./loader-bar.component.scss'], + providers: [SubscriptionService], +}) +export class LoaderBarComponent implements OnDestroy, OnInit { + protected _isLoading: boolean; + + @Input() + set isLoading(value: boolean) { + this._isLoading = value; + this.cdRef.detectChanges(); + } + get isLoading(): boolean { + return this._isLoading; + } + + @Input() + containerClass = 'abp-loader-bar'; + + @Input() + color = '#77b6ff'; + + progressLevel = 0; + + interval = new Subscription(); + + timer = new Subscription(); + + intervalPeriod = 350; + + stopDelay = 800; + + private readonly clearProgress = () => { + this.progressLevel = 0; + this.cdRef.detectChanges(); + }; + + private readonly reportProgress = () => { + if (this.progressLevel < 75) { + this.progressLevel += 1 + Math.random() * 9; + } else if (this.progressLevel < 90) { + this.progressLevel += 0.4; + } else if (this.progressLevel < 100) { + this.progressLevel += 0.1; + } else { + this.interval.unsubscribe(); + } + this.cdRef.detectChanges(); + }; + + get boxShadow(): string { + return `0 0 10px rgba(${this.color}, 0.5)`; + } + + constructor( + private router: Router, + private cdRef: ChangeDetectorRef, + private subscription: SubscriptionService, + private httpWaitService: HttpWaitService, + private routerWaitService: RouterWaitService, + ) {} + + ngOnInit() { + this.subscribeLoading(); + } + + subscribeLoading() { + this.subscription.addOne( + combineLatest([this.httpWaitService.getLoading$(), this.routerWaitService.getLoading$()]), + ([httpLoading, routerLoading]) => { + if (httpLoading || routerLoading) this.startLoading(); + else this.stopLoading(); + }, + ); + } + + ngOnDestroy() { + this.interval.unsubscribe(); + } + + startLoading() { + if (this.isLoading || !this.interval.closed) return; + + this.isLoading = true; + this.progressLevel = 0; + this.cdRef.detectChanges(); + this.interval = timer(0, this.intervalPeriod).subscribe(this.reportProgress); + this.timer.unsubscribe(); + } + + stopLoading() { + this.interval.unsubscribe(); + + this.progressLevel = 100; + this.isLoading = false; + + if (!this.timer.closed) return; + + this.timer = timer(this.stopDelay).subscribe(this.clearProgress); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loading/loading.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loading/loading.component.ts new file mode 100644 index 0000000000..c68757b4a0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/loading/loading.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'abp-loading', + template: ` +
+ +
+ `, + encapsulation: ViewEncapsulation.None, + styles: [ + ` + .abp-loading { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1040; + } + + .abp-loading .abp-spinner { + position: absolute; + top: 50%; + left: 50%; + font-size: 14px; + -moz-transform: translateX(-50%) translateY(-50%); + -o-transform: translateX(-50%) translateY(-50%); + -ms-transform: translateX(-50%) translateY(-50%); + -webkit-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + `, + ], +}) +export class LoadingComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-close.directive.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-close.directive.ts new file mode 100644 index 0000000000..a20c8d8a34 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-close.directive.ts @@ -0,0 +1,16 @@ +import { Directive, HostListener, Optional } from '@angular/core'; +import { ModalComponent } from './modal.component'; + +@Directive({ selector: '[abpClose]' }) +export class ModalCloseDirective { + constructor(@Optional() private modal: ModalComponent) { + if (!modal) { + console.error('Please use abpClose within an abp-modal'); + } + } + + @HostListener('click') + onClick() { + this.modal?.close(); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-container.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-container.component.ts new file mode 100644 index 0000000000..1b07955270 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-container.component.ts @@ -0,0 +1,13 @@ +import { Component, ViewChild, ViewContainerRef } from '@angular/core'; + +/** + * @deprecated To be removed in v5.0 + */ +@Component({ + selector: 'abp-modal-container', + template: '', +}) +export class ModalContainerComponent { + @ViewChild('container', { static: true, read: ViewContainerRef }) + container: ViewContainerRef; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-ref.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-ref.service.ts new file mode 100644 index 0000000000..c9f7ea41e2 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal-ref.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; + +export type ModalDismissMode = 'hard' | 'soft'; + +export interface DismissableModal { + dismiss(mode: ModalDismissMode); +} + +@Injectable({ providedIn: 'root' }) +export class ModalRefService { + modalRefs: DismissableModal[] = []; + + register(modal: DismissableModal) { + this.modalRefs.push(modal); + } + unregister(modal: DismissableModal) { + const index = this.modalRefs.indexOf(modal); + if (index > -1) { + this.modalRefs.splice(index, 1); + } + } + + dismissAll(mode: ModalDismissMode) { + this.modalRefs.forEach(modal => modal.dismiss(mode)); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.html new file mode 100644 index 0000000000..ee0d99b7cb --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.html @@ -0,0 +1,23 @@ + + + + + + + diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.scss new file mode 100644 index 0000000000..321bf9342d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.scss @@ -0,0 +1,25 @@ +.modal { + &.show { + display: block !important; + } + + &-backdrop { + opacity: 0.8; + } + + &::-webkit-scrollbar { + width: 7px; + } + + &::-webkit-scrollbar-track { + background: #ddd; + } + + &::-webkit-scrollbar-thumb { + background: #8a8686; + } + + &-dialog { + z-index: 1050; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.ts new file mode 100644 index 0000000000..feb4ba5fd8 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/modal/modal.component.ts @@ -0,0 +1,253 @@ +import { SubscriptionService, uuid } from '@abp/ng.core'; +import { + Component, + ContentChild, + ElementRef, + EventEmitter, + Inject, + Input, + isDevMode, + OnDestroy, + OnInit, + Optional, + Output, + TemplateRef, + ViewChild +} from '@angular/core'; +import { NgbModal, NgbModalOptions, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { fromEvent, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, filter, takeUntil } from 'rxjs/operators'; +import { Confirmation } from '../../models/confirmation'; +import { ConfirmationService } from '../../services/confirmation.service'; +import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '../../tokens/suppress-unsaved-changes-warning.token'; +import { ButtonComponent } from '../button/button.component'; +import { DismissableModal, ModalDismissMode, ModalRefService } from './modal-ref.service'; + +export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'; + +@Component({ + selector: 'abp-modal', + templateUrl: './modal.component.html', + styleUrls: ['./modal.component.scss'], + providers: [SubscriptionService], +}) +export class ModalComponent implements OnInit, OnDestroy, DismissableModal { + /** + * @deprecated Use centered property of options input instead. To be deleted in v5.0. + */ + @Input() centered = false; + /** + * @deprecated Use windowClass property of options input instead. To be deleted in v5.0. + */ + @Input() modalClass = ''; + /** + * @deprecated Use size property of options input instead. To be deleted in v5.0. + */ + @Input() size: ModalSize = 'lg'; + + @Input() + get visible(): boolean { + return this._visible; + } + set visible(value: boolean) { + if (typeof value !== 'boolean') return; + this.toggle$.next(value); + } + + @Input() + get busy(): boolean { + return this._busy; + } + set busy(value: boolean) { + if (this.abpSubmit && this.abpSubmit instanceof ButtonComponent) { + this.abpSubmit.loading = value; + } + + this._busy = value; + } + + @Input() options: NgbModalOptions = {}; + + @Input() suppressUnsavedChangesWarning = this.suppressUnsavedChangesWarningToken; + + @ViewChild('modalContent') modalContent: TemplateRef; + + @ContentChild('abpHeader', { static: false }) abpHeader: TemplateRef; + + @ContentChild('abpBody', { static: false }) abpBody: TemplateRef; + + @ContentChild('abpFooter', { static: false }) abpFooter: TemplateRef; + + @ContentChild(ButtonComponent, { static: false, read: ButtonComponent }) + abpSubmit: ButtonComponent; + + /** + * @deprecated will be removed in v5.0 + */ + @ContentChild('abpClose', { static: false, read: ElementRef }) + abpClose: ElementRef; + + @Output() readonly visibleChange = new EventEmitter(); + + @Output() readonly init = new EventEmitter(); + + @Output() readonly appear = new EventEmitter(); + + @Output() readonly disappear = new EventEmitter(); + + _visible = false; + + _busy = false; + + modalRef: NgbModalRef; + + isConfirmationOpen = false; + + destroy$ = new Subject(); + + modalIdentifier = `modal-${uuid()}`; + + private toggle$ = new Subject(); + + get modalWindowRef() { + return document.querySelector(`ngb-modal-window.${this.modalIdentifier}`); + } + + get isFormDirty(): boolean { + return Boolean(this.modalWindowRef.querySelector('.ng-dirty')); + } + + constructor( + private confirmationService: ConfirmationService, + private subscription: SubscriptionService, + @Optional() + @Inject(SUPPRESS_UNSAVED_CHANGES_WARNING) + private suppressUnsavedChangesWarningToken: boolean, + private modal: NgbModal, + private modalRefService: ModalRefService, + ) { + this.initToggleStream(); + } + ngOnInit(): void { + this.modalRefService.register(this); + } + + dismiss(mode: ModalDismissMode) { + switch (mode) { + case 'hard': + this.visible = false; + break; + case 'soft': + this.close(); + break; + default: + break; + } + } + + private initToggleStream() { + this.subscription.addOne(this.toggle$.pipe(debounceTime(0), distinctUntilChanged()), value => + this.toggle(value), + ); + } + + private toggle(value: boolean) { + this._visible = value; + this.visibleChange.emit(value); + + if (!value) { + this.modalRef?.dismiss(); + this.disappear.emit(); + this.destroy$.next(); + return; + } + + setTimeout(() => this.listen(), 0); + this.modalRef = this.modal.open(this.modalContent, { + // TODO: set size to 'lg' when removed the size variable + size: this.size, + centered: this.centered, + keyboard: false, + scrollable: true, + beforeDismiss: () => { + if (!this.visible) return true; + + this.close(); + return !this.visible; + }, + ...this.options, + windowClass: `${this.modalClass} ${this.options.windowClass || ''} ${this.modalIdentifier}`, + }); + + this.appear.emit(); + } + + ngOnDestroy(): void { + this.modalRefService.unregister(this); + this.toggle(false); + this.destroy$.next(); + } + + close() { + if (this.busy) return; + + if (this.isFormDirty && !this.suppressUnsavedChangesWarning) { + if (this.isConfirmationOpen) return; + + this.isConfirmationOpen = true; + this.confirmationService + .warn( + 'AbpAccount::AreYouSureYouWantToCancelEditingWarningMessage', + 'AbpAccount::AreYouSure', + { dismissible: false }, + ) + .subscribe((status: Confirmation.Status) => { + this.isConfirmationOpen = false; + if (status === Confirmation.Status.confirm) { + this.visible = false; + } + }); + } else { + this.visible = false; + } + } + + listen() { + fromEvent(this.modalWindowRef, 'keyup') + .pipe( + takeUntil(this.destroy$), + debounceTime(150), + filter((key: KeyboardEvent) => key && key.key === 'Escape'), + ) + .subscribe(() => this.close()); + + fromEvent(window, 'beforeunload') + .pipe(takeUntil(this.destroy$)) + .subscribe(event => { + event.preventDefault(); + if (this.isFormDirty && !this.suppressUnsavedChangesWarning) { + event.returnValue = true; + } else { + delete event.returnValue; + } + }); + + setTimeout(() => { + if (!this.abpClose) return; + this.warnForDeprecatedClose(); + fromEvent(this.abpClose.nativeElement, 'click') + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.close()); + }, 0); + + this.init.emit(); + } + + private warnForDeprecatedClose() { + if (isDevMode()) { + console.warn( + 'Please use abpClose directive instead of #abpClose template variable. #abpClose will be removed in v5.0', + ); + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.html new file mode 100644 index 0000000000..a3a142cc01 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.ts new file mode 100644 index 0000000000..439989ef73 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.ts @@ -0,0 +1,63 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +/** + * @deprecated To be deleted in v5.0. Use ngx-datatale instead. + */ +@Component({ + selector: 'abp-sort-order-icon', + templateUrl: './sort-order-icon.component.html', +}) +export class SortOrderIconComponent { + private _order: 'asc' | 'desc' | ''; + private _selectedSortKey: string; + + @Input() + sortKey: string; + + @Input() + set selectedSortKey(value: string) { + this._selectedSortKey = value; + this.selectedSortKeyChange.emit(value); + } + get selectedSortKey(): string { + return this._selectedSortKey; + } + + @Input() + set order(value: 'asc' | 'desc' | '') { + this._order = value; + this.orderChange.emit(value); + } + get order(): 'asc' | 'desc' | '' { + return this._order; + } + + @Output() readonly orderChange = new EventEmitter(); + @Output() readonly selectedSortKeyChange = new EventEmitter(); + + @Input() + iconClass: string; + + get icon(): string { + if (this.selectedSortKey === this.sortKey) return `sorting_${this.order}`; + else return 'sorting'; + } + + sort(key: string) { + this.selectedSortKey = key; + switch (this.order) { + case '': + this.order = 'asc'; + this.orderChange.emit('asc'); + break; + case 'asc': + this.order = 'desc'; + this.orderChange.emit('desc'); + break; + case 'desc': + this.order = ''; + this.orderChange.emit(''); + break; + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table-empty-message/table-empty-message.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table-empty-message/table-empty-message.component.ts new file mode 100644 index 0000000000..5924a9f187 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table-empty-message/table-empty-message.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit, Input } from '@angular/core'; + +@Component({ + // tslint:disable-next-line: component-selector + selector: '[abp-table-empty-message]', + template: ` + + {{ emptyMessage | abpLocalization }} + + ` +}) +export class TableEmptyMessageComponent { + @Input() + colspan = 2; + + @Input() + message: string; + + @Input() + localizationResource = 'AbpAccount'; + + @Input() + localizationProp = 'NoDataAvailableInDatatable'; + + get emptyMessage(): string { + return this.message || `${this.localizationResource}::${this.localizationProp}`; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.html new file mode 100644 index 0000000000..6cbea56304 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.html @@ -0,0 +1,81 @@ +
+
+ +
+ +
+
+
+ + +
+
+
+
+ + + + +
+
+
+
+ + + +
+
+
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + {{ + emptyMessage | abpLocalization + }} + + diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.scss new file mode 100644 index 0000000000..98d4eb5811 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.scss @@ -0,0 +1,337 @@ +.ui-table { + position: relative; + + .ui-table-tbody > tr:nth-child(even):hover, + .ui-table-tbody > tr:hover { + filter: brightness(90%); + } + + .ui-table-empty { + padding: 20px 0; + text-align: center; + border: 1px solid #e0e0e0; + border-top-width: 0; + } + + .ui-table-caption, + .ui-table-summary { + background-color: #f4f4f4; + color: #333333; + border: 1px solid #c8c8c8; + padding: 0.571em 1em; + text-align: center; + } + .ui-table-caption { + border-bottom: 0 none; + font-weight: 700; + } + .ui-table-summary { + border-top: 0 none; + font-weight: 700; + } + .ui-table-thead > tr > th { + padding: 0.571em 0.857em; + border: 1px solid #c8c8c8; + font-weight: 700; + color: #333333; + background-color: #f4f4f4; + } + .ui-table-tbody > tr > td { + padding: 0.571em 0.857em; + } + .ui-table-tfoot > tr > td { + padding: 0.571em 0.857em; + border: 1px solid #c8c8c8; + font-weight: 700; + color: #333333; + background-color: #ffffff; + } + .ui-sortable-column { + -moz-transition: box-shadow 0.2s; + -o-transition: box-shadow 0.2s; + -webkit-transition: box-shadow 0.2s; + transition: box-shadow 0.2s; + } + .ui-sortable-column:focus { + outline: 0 none; + outline-offset: 0; + -webkit-box-shadow: inset 0 0 0 0.2em #8dcdff; + -moz-box-shadow: inset 0 0 0 0.2em #8dcdff; + box-shadow: inset 0 0 0 0.2em #8dcdff; + } + .ui-sortable-column .ui-sortable-column-icon { + color: #848484; + } + .ui-sortable-column:not(.ui-state-highlight):hover { + background-color: #e0e0e0; + color: #333333; + } + .ui-sortable-column:not(.ui-state-highlight):hover .ui-sortable-column-icon { + color: #333333; + } + .ui-sortable-column.ui-state-highlight { + background-color: #007ad9; + color: #ffffff; + } + .ui-sortable-column.ui-state-highlight .ui-sortable-column-icon { + color: #ffffff; + } + .ui-editable-column input { + font-size: 14px; + font-family: 'Open Sans', 'Helvetica Neue', sans-serif; + } + .ui-editable-column input:focus { + outline: 1px solid #007ad9; + outline-offset: 2px; + } + .ui-table-tbody > tr { + background-color: #ffffff; + color: #333333; + } + .ui-table-tbody > tr > td { + background-color: inherit; + border: 1px solid #c8c8c8; + } + .ui-table-tbody > tr.ui-state-highlight { + background-color: #007ad9; + color: #ffffff; + } + .ui-table-tbody > tr.ui-state-highlight a { + color: #ffffff; + } + .ui-table-tbody > tr.ui-contextmenu-selected { + background-color: #007ad9; + color: #ffffff; + } + .ui-table-tbody > tr.ui-table-dragpoint-top > td { + -webkit-box-shadow: inset 0 2px 0 0 #007ad9; + -moz-box-shadow: inset 0 2px 0 0 #007ad9; + box-shadow: inset 0 2px 0 0 #007ad9; + } + .ui-table-tbody > tr.ui-table-dragpoint-bottom > td { + -webkit-box-shadow: inset 0 -2px 0 0 #007ad9; + -moz-box-shadow: inset 0 -2px 0 0 #007ad9; + box-shadow: inset 0 -2px 0 0 #007ad9; + } + .ui-table-tbody > tr:nth-child(even) { + background-color: #f9f9f9; + } + .ui-table-tbody > tr:nth-child(even).ui-state-highlight { + background-color: #007ad9; + color: #ffffff; + } + .ui-table-tbody > tr:nth-child(even).ui-state-highlight a { + color: #ffffff; + } + .ui-table-tbody > tr:nth-child(even).ui-contextmenu-selected { + background-color: #007ad9; + color: #ffffff; + } + + &.ui-table-hoverable-rows + .ui-table-tbody + > tr.ui-selectable-row:not(.ui-state-highlight):not(.ui-contextmenu-selected):hover { + cursor: pointer; + background-color: #eaeaea; + color: #333333; + } + .ui-column-resizer-helper { + background-color: #007ad9; + } + @media screen and (max-width: 40em) { + &.ui-table-responsive .ui-table-tbody > tr > td { + border: 0 none; + } + } + + table { + border-collapse: collapse; + width: 100%; + table-layout: fixed; + } + + .ui-table-tbody > tr > td, + .ui-table-tfoot > tr > td, + .ui-table-thead > tr > th { + padding: 0.571em 0.857em; + } + + .ui-sortable-column { + cursor: pointer; + } + + p-sorticon { + vertical-align: middle; + } + + .ui-table-auto-layout > .ui-table-wrapper { + overflow-x: auto; + } + + .ui-table-auto-layout > .ui-table-wrapper > table { + table-layout: auto; + } + + .ui-table-caption, + .ui-table-summary { + padding: 0.25em 0.5em; + text-align: center; + font-weight: 700; + } + + .ui-table-caption { + border-bottom: 0; + } + + .ui-table-summary { + border-top: 0; + } + + .ui-table-scrollable-wrapper { + position: relative; + } + + .ui-table-scrollable-footer, + .ui-table-scrollable-header { + overflow: hidden; + border: 0; + } + + .ui-table-scrollable-body { + overflow: auto; + position: relative; + } + + .ui-table-virtual-table { + position: absolute; + } + + .ui-table-loading-virtual-table { + display: none; + } + + .ui-table-frozen-view .ui-table-scrollable-body { + overflow: hidden; + } + + .ui-table-frozen-view > .ui-table-scrollable-body > table > .ui-table-tbody > tr > td:last-child { + border-right: 0; + } + + .ui-table-unfrozen-view { + position: absolute; + top: 0; + } + + .ui-table-resizable > .ui-table-wrapper { + overflow-x: auto; + } + + .ui-table-resizable .ui-table-tbody > tr > td, + .ui-table-resizable .ui-table-tfoot > tr > td, + .ui-table-resizable .ui-table-thead > tr > th { + overflow: hidden; + } + + .ui-table-resizable .ui-resizable-column { + background-clip: padding-box; + position: relative; + } + + .ui-table-resizable-fit .ui-resizable-column:last-child .ui-column-resizer { + display: none; + } + + .ui-column-resizer { + display: block; + position: absolute !important; + top: 0; + right: 0; + margin: 0; + width: 0.5em; + height: 100%; + padding: 0; + cursor: col-resize; + border: 1px solid rgba(0, 0, 0, 0); + } + + .ui-column-resizer-helper { + width: 1px; + position: absolute; + z-index: 10; + display: none; + } + + .ui-table-tbody > tr > td.ui-editing-cell { + padding: 0; + } + + .ui-table-tbody > tr > td.ui-editing-cell p-celleditor > * { + width: 100%; + } + + .ui-table-reorder-indicator-down, + .ui-table-reorder-indicator-up { + position: absolute; + display: none; + } + + .ui-table-responsive .ui-table-tbody > tr > td .ui-column-title { + display: none; + } + + @media screen and (max-width: 40em) { + .ui-table-responsive .ui-table-tfoot > tr > td, + .ui-table-responsive .ui-table-thead > tr > th, + .ui-table-responsive colgroup { + display: none !important; + } + .ui-table-responsive .ui-table-tbody > tr > td { + text-align: left; + display: block; + border: 0; + width: 100% !important; + box-sizing: border-box; + float: left; + clear: left; + } + .ui-table-responsive .ui-table-tbody > tr > td .ui-column-title { + padding: 0.4em; + min-width: 30%; + display: inline-block; + margin: -0.4em 1em -0.4em -0.4em; + font-weight: 700; + } + } + + .ui-widget { + font-family: 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 14px; + text-decoration: none; + } + + .page-item.disabled .page-link, + .page-link { + background-color: transparent; + border: none; + } + + .page-item.disabled .page-link { + box-shadow: none; + } + + .pagination { + margin-bottom: 0; + } + + .pagination-wrapper { + display: flex; + justify-content: center; + border-top: 0; + padding: 0; + } + + .op-0 { + opacity: 0; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.ts new file mode 100644 index 0000000000..0baf78f883 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/table/table.component.ts @@ -0,0 +1,110 @@ +import { + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + TemplateRef, + TrackByFunction, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +/** + * + * @deprecated To be deleted in v5.0. Use ngx-datatale instead. + */ +@Component({ + selector: 'abp-table', + templateUrl: 'table.component.html', + styleUrls: ['table.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class TableComponent implements OnInit { + private _totalRecords: number; + bodyScrollLeft = 0; + + @Input() + value: any[]; + + @Input() + headerTemplate: TemplateRef; + + @Input() + bodyTemplate: TemplateRef; + + @Input() + colgroupTemplate: TemplateRef; + + @Input() + scrollHeight: string; + + @Input() + scrollable: boolean; + + @Input() + rows: number; + + @Input() + page = 1; + + @Input() + trackingProp = 'id'; + + @Input() + emptyMessage = 'AbpAccount::NoDataAvailableInDatatable'; + + @Output() + readonly pageChange = new EventEmitter(); + + @ViewChild('wrapper', { read: ElementRef }) + wrapperRef: ElementRef; + + @Input() + get totalRecords(): number { + return this._totalRecords || this.value.length; + } + set totalRecords(newValue: number) { + if (newValue < 0) this._totalRecords = 0; + + this._totalRecords = newValue; + } + + get totalPages(): number { + if (!this.rows) { + return; + } + + return Math.ceil(this.totalRecords / this.rows); + } + + get slicedValue(): any[] { + if (!this.rows || this.rows >= this.value.length) { + return this.value; + } + + const start = (this.page - 1) * this.rows; + return this.value.slice(start, start + this.rows); + } + + marginCalculator: MarginCalculator; + + trackByFn: TrackByFunction = (_, value) => { + return typeof value === 'object' ? value[this.trackingProp] || value : value; + }; + + ngOnInit() { + this.marginCalculator = document.body.dir === 'rtl' ? rtlCalculator : ltrCalculator; + } +} + +function ltrCalculator(div: HTMLDivElement): string { + return `0 auto 0 -${div.scrollLeft}px`; +} + +function rtlCalculator(div: HTMLDivElement): string { + return `0 ${-(div.scrollWidth - div.clientWidth - div.scrollLeft)}px 0 auto`; +} + +type MarginCalculator = (div: HTMLDivElement) => string; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.html new file mode 100644 index 0000000000..3dc2427f92 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.html @@ -0,0 +1,11 @@ +
+ +
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.scss new file mode 100644 index 0000000000..281e08998d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.scss @@ -0,0 +1,13 @@ +.abp-toast-container { + position: fixed; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + min-width: 350px; + min-height: 80px; + z-index: 1900; + &.new-on-top { + flex-direction: column-reverse; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.ts new file mode 100644 index 0000000000..28d66123a9 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast-container/toast-container.component.ts @@ -0,0 +1,46 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { toastInOut } from '../../animations/toast.animations'; +import { Toaster } from '../../models/toaster'; +import { ReplaySubject } from 'rxjs'; + +@Component({ + selector: 'abp-toast-container', + templateUrl: './toast-container.component.html', + styleUrls: ['./toast-container.component.scss'], + animations: [toastInOut], +}) +export class ToastContainerComponent implements OnInit { + toasts$: ReplaySubject; + + toasts = [] as Toaster.Toast[]; + + @Input() + top: string; + + @Input() + right = '30px'; + + @Input() + bottom = '30px'; + + @Input() + left: string; + + @Input() + toastKey: string; + + ngOnInit() { + this.toasts$.subscribe(toasts => { + this.toasts = this.toastKey + ? toasts.filter(t => { + return t.options && t.options.containerKey !== this.toastKey; + }) + : toasts; + }); + } + + trackByFunc(index, toast) { + if (!toast) return null; + return toast.options.id; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.html b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.html new file mode 100644 index 0000000000..8af75bdfc4 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.html @@ -0,0 +1,17 @@ +
+
+ +
+
+ +
+ {{ toast.title | abpLocalization: toast.options?.titleLocalizationParams }} +
+

+
+
diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.scss b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.scss new file mode 100644 index 0000000000..64bf8a6068 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.scss @@ -0,0 +1,81 @@ +@mixin fillColor($background, $color) { + border: 2px solid $background; + background-color: $background; + color: $color; + box-shadow: 0 0 10px -5px rgba(#000, 0.4); + &:hover { + border: 2px solid darken($background, 5); + background-color: darken($background, 5); + box-shadow: 0 0 15px -5px rgba(#000, 0.4); + } +} + +$toastClass: abp-toast; + +.#{$toastClass} { + display: grid; + grid-template-columns: 50px 1fr; + gap: 10px; + margin: 5px 0; + padding: 10px; + border-radius: 0px; + width: 350px; + user-select: none; + box-shadow: 0 0 10px -5px rgba(#000, 0.4); + z-index: 9999; + @include fillColor(#f0f0f0, #000); + opacity: 1; + &.#{$toastClass}-success { + @include fillColor(#51a351, #fff); + } + &.#{$toastClass}-info { + @include fillColor(#2f96b4, #fff); + } + &.#{$toastClass}-warning { + @include fillColor(#f89406, #fff); + } + &.#{$toastClass}-error { + @include fillColor(#bd362f, #fff); + } + .#{$toastClass}-icon { + display: flex; + align-items: center; + justify-content: center; + .icon { + font-size: 36px; + } + } + .#{$toastClass}-content { + position: relative; + .#{$toastClass}-close-button { + position: absolute; + top: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 0px 5px 0 0; + width: 25px; + height: 25px; + border: none; + border-radius: 50%; + background: transparent; + color: inherit; + &:focus { + outline: none; + } + } + .#{$toastClass}-title { + margin: 0; + padding: 0; + font-size: 1rem; + font-weight: 600; + } + .#{$toastClass}-message { + margin: 0; + padding: 0; + max-width: 240px; + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.ts new file mode 100644 index 0000000000..48298e74a0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/components/toast/toast.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Toaster } from '../../models/toaster'; +import { ToasterService } from '../../services/toaster.service'; +import snq from 'snq'; + +@Component({ + selector: 'abp-toast', + templateUrl: './toast.component.html', + styleUrls: ['./toast.component.scss'], +}) +export class ToastComponent implements OnInit { + @Input() + toast: Toaster.Toast; + + get severityClass(): string { + if (!this.toast || !this.toast.severity) return ''; + return `abp-toast-${this.toast.severity}`; + } + + get iconClass(): string { + switch (this.toast.severity) { + case 'success': + return 'fa-check-circle'; + case 'info': + return 'fa-info-circle'; + case 'warning': + return 'fa-exclamation-triangle'; + case 'error': + return 'fa-times-circle'; + default: + return 'fa-exclamation-circle'; + } + } + + constructor(private toasterService: ToasterService) {} + + ngOnInit() { + if (snq(() => this.toast.options.sticky)) return; + const timeout = snq(() => this.toast.options.life) || 5000; + setTimeout(() => { + this.close(); + }, timeout); + } + + close() { + this.toasterService.remove(this.toast.options.id); + } + + tap() { + if (this.toast.options?.tapToDismiss) this.close(); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/scripts.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/scripts.ts new file mode 100644 index 0000000000..08d725cd4e --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/scripts.ts @@ -0,0 +1 @@ +export default ''; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/styles.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/styles.ts new file mode 100644 index 0000000000..669fc2cbae --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/constants/styles.ts @@ -0,0 +1,181 @@ +export default ` +.is-invalid .form-control { + border-color: #dc3545; + border-style: solid !important; + padding-right: calc(1.5em + .75rem); + background-image: url(data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e); + background-repeat: no-repeat; + background-position: right calc(.375em + .1875rem) center; + background-size: calc(.75em + .375rem) calc(.75em + .375rem); +} + +.is-invalid .invalid-feedback, +.is-invalid + * .invalid-feedback { + display: block; +} + +.data-tables-filter { + text-align: right; +} + +[dir=rtl] .data-tables-filter { + text-align: left; +} + +.pointer { + cursor: pointer; +} + +.navbar .dropdown-submenu a::after { + transform: rotate(-90deg); + position: absolute; + right: 16px; + top: 18px; +} + +.navbar .dropdown-menu { + min-width: 215px; +} + +.datatable-scroll { + margin-bottom: 5px !important; + width: unset !important; +} + +.ui-table-scrollable-body::-webkit-scrollbar { + height: 5px !important; + width: 5px !important; +} + +.ui-table-scrollable-body::-webkit-scrollbar-track { + background: #ddd; +} + +.ui-table-scrollable-body::-webkit-scrollbar-thumb { + background: #8a8686; +} + +.abp-ellipsis-inline { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.abp-ellipsis { + overflow: hidden !important; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ui-widget-overlay { + z-index: 1000; +} + +.color-white { + color: #FFF !important; +} + +.custom-checkbox > label { + cursor: pointer; +} + +/* { + if (!newValue && this.timerSubscription) { + this.timerSubscription.unsubscribe(); + this.timerSubscription = null; + this._loading = newValue; + + if (this.rootNode) { + this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); + this.rootNode = null; + } + return; + } + + this.timerSubscription = timer(this.delay) + .pipe(take(1)) + .subscribe(() => { + if (!this.componentRef) { + this.componentRef = this.cdRes + .resolveComponentFactory(LoadingComponent) + .create(this.injector); + } + + if (newValue && !this.rootNode) { + this.rootNode = (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0]; + this.targetElement.appendChild(this.rootNode); + } else if (this.rootNode) { + this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); + this.rootNode = null; + } + + this._loading = newValue; + this.timerSubscription = null; + }); + }, 0); + } + + @Input('abpLoadingTargetElement') + targetElement: HTMLElement; + + @Input('abpLoadingDelay') + delay = 0; + + componentRef: ComponentRef; + rootNode: HTMLDivElement; + timerSubscription: Subscription; + + constructor( + private elRef: ElementRef, + private vcRef: ViewContainerRef, + private cdRes: ComponentFactoryResolver, + private injector: Injector, + private renderer: Renderer2, + ) {} + + ngOnInit() { + if (!this.targetElement) { + const { offsetHeight, offsetWidth } = this.elRef.nativeElement; + if (!offsetHeight && !offsetWidth && this.elRef.nativeElement.children.length) { + this.targetElement = this.elRef.nativeElement.children[0] as HTMLElement; + } else { + this.targetElement = this.elRef.nativeElement; + } + } + } + + ngOnDestroy() { + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts new file mode 100644 index 0000000000..e8b9ffac85 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts @@ -0,0 +1,84 @@ +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Directive, HostBinding, Inject, Input, OnDestroy } from '@angular/core'; +import { ColumnMode, DatatableComponent, ScrollerComponent } from '@swimlane/ngx-datatable'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Directive({ + // tslint:disable-next-line + selector: 'ngx-datatable[default]', + exportAs: 'ngxDatatableDefault', +}) +export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy { + private subscription = new Subscription(); + + private resizeDiff = 0; + + @Input() class = 'material bordered'; + + @HostBinding('class') + get classes(): string { + return `ngx-datatable ${this.class}`; + } + + constructor(private table: DatatableComponent, @Inject(DOCUMENT) private document: MockDocument) { + this.table.columnMode = ColumnMode.force; + this.table.footerHeight = 50; + this.table.headerHeight = 50; + this.table.rowHeight = 'auto'; + this.table.scrollbarH = true; + this.table.virtualization = false; + } + + private fixHorizontalGap(scroller: ScrollerComponent) { + const { body, documentElement } = this.document; + + if (documentElement.scrollHeight !== documentElement.clientHeight) { + if (this.resizeDiff === 0) { + this.resizeDiff = window.innerWidth - body.offsetWidth; + scroller.scrollWidth -= this.resizeDiff; + } + } else { + scroller.scrollWidth += this.resizeDiff; + this.resizeDiff = 0; + } + } + + private fixStyleOnWindowResize() { + // avoided @HostListener('window:resize') in favor of performance + const subscription = fromEvent(window, 'resize') + .pipe(debounceTime(500)) + .subscribe(() => { + const { scroller } = this.table.bodyComponent; + + if (!scroller) return; + + this.fixHorizontalGap(scroller); + }); + + this.subscription.add(subscription); + } + + ngAfterViewInit() { + this.fixStyleOnWindowResize(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} + +// fix: https://github.com/angular/angular/issues/20351 +interface MockDocument { + body: MockBody; + documentElement: MockDocumentElement; +} + +interface MockBody { + offsetWidth: number; +} + +interface MockDocumentElement { + clientHeight: number; + scrollHeight: number; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts new file mode 100644 index 0000000000..cf800db556 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/ngx-datatable-list.directive.ts @@ -0,0 +1,106 @@ +import { ListService, LocalizationService } from '@abp/ng.core'; +import { + ChangeDetectorRef, + Directive, + Inject, + Input, + OnChanges, + OnDestroy, + OnInit, + Optional, + SimpleChanges, +} from '@angular/core'; +import { DatatableComponent } from '@swimlane/ngx-datatable'; +import { Subscription } from 'rxjs'; +import { + defaultNgxDatatableMessages, + NgxDatatableMessages, + NGX_DATATABLE_MESSAGES, +} from '../tokens/ngx-datatable-messages.token'; + +@Directive({ + // tslint:disable-next-line + selector: 'ngx-datatable[list]', + exportAs: 'ngxDatatableList', +}) +export class NgxDatatableListDirective implements OnChanges, OnDestroy, OnInit { + private subscription = new Subscription(); + private querySubscription = new Subscription(); + + @Input() list: ListService; + + constructor( + private table: DatatableComponent, + private cdRef: ChangeDetectorRef, + private localizationService: LocalizationService, + @Optional() @Inject(NGX_DATATABLE_MESSAGES) private ngxDatatableMessages: NgxDatatableMessages, + ) { + this.setInitialValues(); + } + + private setInitialValues() { + this.table.externalPaging = true; + this.table.externalSorting = true; + + const { emptyMessage, selectedMessage, totalMessage } = + this.ngxDatatableMessages || defaultNgxDatatableMessages; + + this.table.messages = { + emptyMessage: this.localizationService.instant(emptyMessage), + totalMessage: this.localizationService.instant(totalMessage), + selectedMessage: this.localizationService.instant(selectedMessage), + }; + } + + private subscribeToPage() { + const sub = this.table.page.subscribe(({ offset }) => { + this.list.page = offset; + this.table.offset = offset; + }); + this.subscription.add(sub); + } + + private subscribeToSort() { + const sub = this.table.sort.subscribe(({ sorts: [{ prop, dir }] }) => { + if (prop === this.list.sortKey && this.list.sortOrder === 'desc') { + this.list.sortKey = ''; + this.list.sortOrder = ''; + this.table.sorts = []; + this.cdRef.detectChanges(); + } else { + this.list.sortKey = prop; + this.list.sortOrder = dir; + } + }); + this.subscription.add(sub); + } + + private subscribeToQuery() { + if (!this.querySubscription.closed) this.querySubscription.unsubscribe(); + + this.querySubscription = this.list.query$.subscribe(() => { + const offset = this.list.page; + if (this.table.offset !== offset) this.table.offset = offset; + }); + } + + ngOnChanges({ list }: SimpleChanges) { + this.subscribeToQuery(); + + if (!list.firstChange) return; + + const { maxResultCount, page } = list.currentValue; + this.table.limit = maxResultCount; + this.table.offset = page; + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + this.querySubscription.unsubscribe(); + } + + ngOnInit() { + this.subscribeToPage(); + this.subscribeToSort(); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/table-sort.directive.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/table-sort.directive.ts new file mode 100644 index 0000000000..011768be7b --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/directives/table-sort.directive.ts @@ -0,0 +1,58 @@ +import { SortOrder, SortPipe } from '@abp/ng.core'; +import { + ChangeDetectorRef, + Directive, + Host, + Input, + OnChanges, + Optional, + Self, + SimpleChanges, +} from '@angular/core'; +import clone from 'just-clone'; +import snq from 'snq'; +import { TableComponent } from '../components/table/table.component'; + +export interface TableSortOptions { + key: string; + order: SortOrder; +} + +/** + * + * @deprecated To be deleted in v5.0 + */ +@Directive({ + selector: '[abpTableSort]', + providers: [SortPipe], +}) +export class TableSortDirective implements OnChanges { + @Input() + abpTableSort: TableSortOptions; + + @Input() + value: any[] = []; + + get table(): TableComponent | any { + return ( + this.abpTable || snq(() => this.cdRef['_view'].component) || snq(() => this.cdRef['context']) // 'context' for ivy + ); + } + + constructor( + @Host() @Optional() @Self() private abpTable: TableComponent, + private sortPipe: SortPipe, + private cdRef: ChangeDetectorRef, + ) {} + + ngOnChanges({ value, abpTableSort }: SimpleChanges) { + if (this.table && (value || abpTableSort)) { + this.abpTableSort = this.abpTableSort || ({} as TableSortOptions); + this.table.value = this.sortPipe.transform( + clone(this.value), + this.abpTableSort.order, + this.abpTableSort.key, + ); + } + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/index.ts new file mode 100644 index 0000000000..3bda94b078 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/index.ts @@ -0,0 +1 @@ +export * from './route-names'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/route-names.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/route-names.ts new file mode 100644 index 0000000000..0be2894fd3 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/enums/route-names.ts @@ -0,0 +1,3 @@ +export const enum eThemeSharedRouteNames { + Administration = 'AbpUiNavigation::Menu:Administration', +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/error.handler.ts new file mode 100644 index 0000000000..8d61c4c899 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/error.handler.ts @@ -0,0 +1,338 @@ +import { AuthService, LocalizationParam, RestOccurError, RouterEvents } from '@abp/ng.core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + EmbeddedViewRef, + Inject, + Injectable, + Injector, + RendererFactory2, +} from '@angular/core'; +import { NavigationError, ResolveEnd } from '@angular/router'; +import { Actions, ofActionSuccessful } from '@ngxs/store'; +import { Observable, of, Subject, throwError } from 'rxjs'; +import { catchError, filter, map, switchMap } from 'rxjs/operators'; +import snq from 'snq'; +import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; +import { ErrorScreenErrorCodes, HttpErrorConfig } from '../models/common'; +import { Confirmation } from '../models/confirmation'; +import { ConfirmationService } from '../services/confirmation.service'; +import { HTTP_ERROR_HANDLER } from '../tokens/http-error.token'; + +export const DEFAULT_ERROR_MESSAGES = { + defaultError: { + title: 'An error has occurred!', + details: 'Error detail not sent by server.', + }, + defaultError401: { + title: 'You are not authenticated!', + details: 'You should be authenticated (sign in) in order to perform this operation.', + }, + defaultError403: { + title: 'You are not authorized!', + details: 'You are not allowed to perform this operation.', + }, + defaultError404: { + title: 'Resource not found!', + details: 'The resource requested could not found on the server.', + }, + defaultError500: { + title: 'Internal server error', + details: 'Error detail not sent by server.', + }, +}; + +export const DEFAULT_ERROR_LOCALIZATIONS = { + defaultError: { + title: 'AbpUi::DefaultErrorMessage', + details: 'AbpUi::DefaultErrorMessageDetail', + }, + defaultError401: { + title: 'AbpUi::DefaultErrorMessage401', + details: 'AbpUi::DefaultErrorMessage401Detail', + }, + defaultError403: { + title: 'AbpUi::DefaultErrorMessage403', + details: 'AbpUi::DefaultErrorMessage403Detail', + }, + defaultError404: { + title: 'AbpUi::DefaultErrorMessage404', + details: 'AbpUi::DefaultErrorMessage404Detail', + }, + defaultError500: { + title: 'AbpUi::500Message', + details: 'AbpUi::DefaultErrorMessage', + }, +}; + +@Injectable({ providedIn: 'root' }) +export class ErrorHandler { + componentRef: ComponentRef; + + protected httpErrorHandler = this.injector.get(HTTP_ERROR_HANDLER, (_, err: HttpErrorResponse) => + throwError(err), + ); + + constructor( + protected actions: Actions, + protected routerEvents: RouterEvents, + protected confirmationService: ConfirmationService, + protected cfRes: ComponentFactoryResolver, + protected rendererFactory: RendererFactory2, + protected injector: Injector, + @Inject('HTTP_ERROR_CONFIG') protected httpErrorConfig: HttpErrorConfig, + ) { + this.listenToRestError(); + this.listenToRouterError(); + this.listenToRouterDataResolved(); + } + + protected listenToRouterError() { + this.routerEvents + .getNavigationEvents('Error') + .pipe(filter(this.filterRouteErrors)) + .subscribe(() => this.show404Page()); + } + + protected listenToRouterDataResolved() { + this.routerEvents + .getEvents(ResolveEnd) + .pipe(filter(() => !!this.componentRef)) + .subscribe(() => { + this.componentRef.destroy(); + this.componentRef = null; + }); + } + + protected listenToRestError() { + this.actions + .pipe( + ofActionSuccessful(RestOccurError), + map(action => action.payload), + filter(this.filterRestErrors), + switchMap(this.executeErrorHandler), + ) + .subscribe(); + } + + private executeErrorHandler = error => { + const returnValue = this.httpErrorHandler(this.injector, error); + + return (returnValue instanceof Observable ? returnValue : of(null)).pipe( + catchError(err => { + this.handleError(err); + return of(null); + }), + ); + }; + + private handleError(err: any) { + const body = snq(() => err.error.error, { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }); + + if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) { + const confirmation$ = this.showError(null, null, body); + + if (err.status === 401) { + confirmation$.subscribe(() => { + this.navigateToLogin(); + }); + } + } else { + switch (err.status) { + case 401: + this.canCreateCustomError(401) + ? this.show401Page() + : this.showError( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, + }, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details, + }, + ).subscribe(() => this.navigateToLogin()); + break; + case 403: + this.createErrorComponent({ + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title, + }, + details: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details, + }, + status: 403, + }); + break; + case 404: + this.canCreateCustomError(404) + ? this.show404Page() + : this.showError( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details, + }, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, + }, + ); + break; + case 500: + this.createErrorComponent({ + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title, + }, + details: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details, + }, + status: 500, + }); + break; + case 0: + if (err.statusText === 'Unknown Error') { + this.createErrorComponent({ + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }, + details: err.message, + isHomeShow: false, + }); + } + break; + default: + this.showError( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, + }, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }, + ); + break; + } + } + } + + protected show401Page() { + this.createErrorComponent({ + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, + }, + status: 401, + }); + } + + protected show404Page() { + this.createErrorComponent({ + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, + }, + status: 404, + }); + } + + protected showError( + message?: LocalizationParam, + title?: LocalizationParam, + body?: any, + ): Observable { + if (body) { + if (body.details) { + message = body.details; + title = body.message; + } else if (body.message) { + title = { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }; + message = body.message; + } else { + message = body.message || { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }; + } + } + + return this.confirmationService.error(message, title, { + hideCancelBtn: true, + yesText: 'AbpAccount::Close', + }); + } + + private navigateToLogin() { + this.injector.get(AuthService).navigateToLogin(); + } + + createErrorComponent(instance: Partial) { + const renderer = this.rendererFactory.createRenderer(null, null); + const host = renderer.selectRootElement(document.body, true); + + this.componentRef = this.cfRes + .resolveComponentFactory(HttpErrorWrapperComponent) + .create(this.injector); + + for (const key in instance) { + /* istanbul ignore else */ + if (this.componentRef.instance.hasOwnProperty(key)) { + this.componentRef.instance[key] = instance[key]; + } + } + + this.componentRef.instance.hideCloseIcon = this.httpErrorConfig.errorScreen.hideCloseIcon; + const appRef = this.injector.get(ApplicationRef); + + if (this.canCreateCustomError(instance.status as ErrorScreenErrorCodes)) { + this.componentRef.instance.cfRes = this.cfRes; + this.componentRef.instance.appRef = appRef; + this.componentRef.instance.injector = this.injector; + this.componentRef.instance.customComponent = this.httpErrorConfig.errorScreen.component; + } + + appRef.attachView(this.componentRef.hostView); + renderer.appendChild(host, (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0]); + + const destroy$ = new Subject(); + this.componentRef.instance.destroy$ = destroy$; + destroy$.subscribe(() => { + this.componentRef.destroy(); + this.componentRef = null; + }); + } + + canCreateCustomError(status: ErrorScreenErrorCodes): boolean { + return snq( + () => + this.httpErrorConfig.errorScreen.component && + this.httpErrorConfig.errorScreen.forWhichErrors.indexOf(status) > -1, + ); + } + + protected filterRestErrors = ({ status }: HttpErrorResponse): boolean => { + if (typeof status !== 'number') return false; + + return this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0; + }; + + protected filterRouteErrors = (navigationError: NavigationError): boolean => { + return ( + snq(() => navigationError.error.message.indexOf('Cannot match') > -1) && + this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0 + ); + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/index.ts new file mode 100644 index 0000000000..11eb07398e --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/index.ts @@ -0,0 +1,2 @@ +export * from './error.handler'; +export * from './lazy-style.handler'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/lazy-style.handler.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/lazy-style.handler.ts new file mode 100644 index 0000000000..79bed70563 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/handlers/lazy-style.handler.ts @@ -0,0 +1,111 @@ +import { + getLocaleDirection, + LazyLoadService, + LOADING_STRATEGY, + LocalizationService, +} from '@abp/ng.core'; +import { Injectable, Injector } from '@angular/core'; +import { map, startWith } from 'rxjs/operators'; +import { BOOTSTRAP } from '../constants/styles'; +import { LocaleDirection } from '../models/common'; +import { LAZY_STYLES } from '../tokens/lazy-styles.token'; + +@Injectable({ + providedIn: 'root', +}) +export class LazyStyleHandler { + private lazyLoad: LazyLoadService; + private styles: string[]; + private _dir: LocaleDirection = 'ltr'; + + readonly loaded = new Map(); + + set dir(dir: LocaleDirection) { + if (dir === this._dir) return; + + this.switchCSS(dir); + this.setBodyDir(dir); + this._dir = dir; + } + + get dir(): LocaleDirection { + return this._dir; + } + + constructor(injector: Injector) { + this.setStyles(injector); + this.setLazyLoad(injector); + this.listenToLanguageChanges(injector); + } + + private getHrefFromLink(link: HTMLLinkElement | null): string { + if (!link) return ''; + + const a = document.createElement('a'); + a.href = link.href; + return a.pathname.replace(/^\//, ''); + } + + private getLoadedBootstrap(): LoadedStyle { + const href = createLazyStyleHref(BOOTSTRAP, this.dir); + const selector = `[href*="${href.replace(/\.css$/, '')}"]`; + const link = document.querySelector(selector); + return { href, link }; + } + + private listenToLanguageChanges(injector: Injector) { + const l10n = injector.get(LocalizationService); + + // will always listen, no need to unsubscribe + l10n.languageChange$.pipe(startWith(l10n.currentLang)).subscribe(locale => { + this.dir = getLocaleDirection(locale); + }); + } + + private setBodyDir(dir: LocaleDirection) { + document.body.dir = dir; + } + + private setLazyLoad(injector: Injector) { + this.lazyLoad = injector.get(LazyLoadService); + const { href, link } = this.getLoadedBootstrap(); + this.lazyLoad.loaded.set(href, link); + } + + private setStyles(injector: Injector) { + this.styles = injector.get(LAZY_STYLES, [BOOTSTRAP]); + } + + private switchCSS(dir: LocaleDirection) { + this.styles.forEach(style => { + const oldHref = createLazyStyleHref(style, this.dir); + const newHref = createLazyStyleHref(style, dir); + const link = this.loaded.get(newHref); + const href = this.getHrefFromLink(link) || newHref; + + const strategy = LOADING_STRATEGY.PrependAnonymousStyleToHead(href); + this.lazyLoad.load(strategy).subscribe(() => { + const oldLink = this.lazyLoad.loaded.get(oldHref) as HTMLLinkElement; + this.loaded.delete(newHref); + this.loaded.set(oldHref, oldLink); + const newLink = this.lazyLoad.loaded.get(href) as HTMLLinkElement; + this.lazyLoad.loaded.delete(href); + this.lazyLoad.loaded.set(newHref, newLink); + this.lazyLoad.remove(oldHref); + }); + }); + } +} + +export function createLazyStyleHref(style: string, dir: string): string { + return style.replace(/{{\s*dir\s*}}/g, dir); +} + +export function initLazyStyleHandler(injector: Injector) { + return () => new LazyStyleHandler(injector); +} + +interface LoadedStyle { + href: string; + link: HTMLLinkElement; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/common.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/common.ts new file mode 100644 index 0000000000..0e8dd9e592 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/common.ts @@ -0,0 +1,27 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Injector, Type } from '@angular/core'; +import { Validation } from '@ngx-validate/core'; +import { Observable } from 'rxjs'; + +export interface RootParams { + httpErrorConfig: HttpErrorConfig; + validation?: Partial; +} + +export type ErrorScreenErrorCodes = 401 | 403 | 404 | 500; + +export interface HttpErrorConfig { + skipHandledErrorCodes?: ErrorScreenErrorCodes[] | number[]; + errorScreen?: { + component: Type; + forWhichErrors?: ErrorScreenErrorCodes[]; + hideCloseIcon?: boolean; + }; +} + +export type HttpErrorHandler = ( + injector: Injector, + httpError: HttpErrorResponse, +) => Observable; + +export type LocaleDirection = 'ltr' | 'rtl'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/confirmation.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/confirmation.ts new file mode 100644 index 0000000000..ad2ceb7545 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/confirmation.ts @@ -0,0 +1,29 @@ +import { Config, LocalizationParam } from '@abp/ng.core'; + +export namespace Confirmation { + export interface Options { + id?: any; + dismissible?: boolean; + messageLocalizationParams?: string[]; + titleLocalizationParams?: string[]; + hideCancelBtn?: boolean; + hideYesBtn?: boolean; + cancelText?: LocalizationParam; + yesText?: LocalizationParam; + } + + export interface DialogData { + message: Config.LocalizationParam; + title?: Config.LocalizationParam; + severity?: Severity; + options?: Partial; + } + + export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error'; + + export enum Status { + confirm = 'confirm', + reject = 'reject', + dismiss = 'dismiss', + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/index.ts new file mode 100644 index 0000000000..d1e2e71c5c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/index.ts @@ -0,0 +1,5 @@ +export * from './common'; +export * from './confirmation'; +export * from './statistics'; +export * from './toaster'; +export * from './nav-item'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/nav-item.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/nav-item.ts new file mode 100644 index 0000000000..7fcb154cbf --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/nav-item.ts @@ -0,0 +1,15 @@ +import { Type } from '@angular/core'; + +export class NavItem { + id: string | number; + component?: Type; + html?: string; + action?: () => void; + order?: number; + requiredPolicy?: string; + visible?: () => boolean; + constructor(props: Partial) { + props = { ...props, visible: props.visible || (() => true) }; + Object.assign(this, props); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/statistics.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/statistics.ts new file mode 100644 index 0000000000..65bd5dfa8f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/statistics.ts @@ -0,0 +1,14 @@ +export namespace Statistics { + export interface Response { + data: Data; + } + + export interface Data { + [key: string]: number; + } + + export interface Filter { + startDate: string | Date; + endDate: string | Date; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/toaster.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/toaster.ts new file mode 100644 index 0000000000..1cd4e18bea --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/models/toaster.ts @@ -0,0 +1,55 @@ +import { Config, LocalizationParam } from '@abp/ng.core'; + +export namespace Toaster { + export interface ToastOptions { + life?: number; + sticky?: boolean; + closable?: boolean; + tapToDismiss?: boolean; + messageLocalizationParams?: string[]; + titleLocalizationParams?: string[]; + id: any; + containerKey?: string; + } + + export interface Toast { + message: LocalizationParam; + title?: LocalizationParam; + severity?: string; + options?: ToastOptions; + } + + export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error'; + export type ToasterId = string | number; + + export interface Service { + show: ( + message: LocalizationParam, + title: LocalizationParam, + severity: Toaster.Severity, + options: Partial, + ) => ToasterId; + remove: (id: number) => void; + clear: (containerKey?: string) => void; + info: ( + message: Config.LocalizationParam, + title?: Config.LocalizationParam, + options?: Partial, + ) => ToasterId; + success: ( + message: Config.LocalizationParam, + title?: Config.LocalizationParam, + options?: Partial, + ) => ToasterId; + warn: ( + message: Config.LocalizationParam, + title?: Config.LocalizationParam, + options?: Partial, + ) => ToasterId; + error: ( + message: Config.LocalizationParam, + title?: Config.LocalizationParam, + options?: Partial, + ) => ToasterId; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/index.ts new file mode 100644 index 0000000000..40efb9df39 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/index.ts @@ -0,0 +1,2 @@ +export * from './ng-bootstrap-config.provider'; +export * from './route.provider'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/ng-bootstrap-config.provider.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/ng-bootstrap-config.provider.ts new file mode 100644 index 0000000000..be60e9b2f6 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/ng-bootstrap-config.provider.ts @@ -0,0 +1,21 @@ +import { APP_INITIALIZER } from '@angular/core'; +import { NgbInputDatepickerConfig, NgbTypeaheadConfig } from '@ng-bootstrap/ng-bootstrap'; + +export const NG_BOOTSTRAP_CONFIG_PROVIDERS = [ + { + provide: APP_INITIALIZER, + useFactory: configureNgBootstrap, + deps: [NgbInputDatepickerConfig, NgbTypeaheadConfig], + multi: true, + }, +]; + +export function configureNgBootstrap( + datepicker: NgbInputDatepickerConfig, + typeahead: NgbTypeaheadConfig, +) { + return () => { + datepicker.container = 'body'; + typeahead.container = 'body'; + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/route.provider.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/route.provider.ts new file mode 100644 index 0000000000..577d688c4e --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/providers/route.provider.ts @@ -0,0 +1,20 @@ +import { RoutesService } from '@abp/ng.core'; +import { APP_INITIALIZER } from '@angular/core'; +import { eThemeSharedRouteNames } from '../enums/route-names'; + +export const THEME_SHARED_ROUTE_PROVIDERS = [ + { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, +]; + +export function configureRoutes(routesService: RoutesService) { + return () => { + routesService.add([ + { + path: undefined, + name: eThemeSharedRouteNames.Administration, + iconClass: 'fa fa-wrench', + order: 100, + }, + ]); + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/confirmation.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/confirmation.service.ts new file mode 100644 index 0000000000..e2556d311a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/confirmation.service.ts @@ -0,0 +1,100 @@ +import { ContentProjectionService, LocalizationParam, PROJECTION_STRATEGY } from '@abp/ng.core'; +import { ComponentRef, Injectable } from '@angular/core'; +import { fromEvent, Observable, ReplaySubject, Subject } from 'rxjs'; +import { debounceTime, filter, takeUntil } from 'rxjs/operators'; +import { ConfirmationComponent } from '../components/confirmation/confirmation.component'; +import { Confirmation } from '../models/confirmation'; + +@Injectable({ providedIn: 'root' }) +export class ConfirmationService { + status$: Subject; + confirmation$ = new ReplaySubject(1); + + private containerComponentRef: ComponentRef; + + clear = (status: Confirmation.Status = Confirmation.Status.dismiss) => { + this.confirmation$.next(); + this.status$.next(status); + }; + + constructor(private contentProjectionService: ContentProjectionService) {} + + private setContainer() { + this.containerComponentRef = this.contentProjectionService.projectContent( + PROJECTION_STRATEGY.AppendComponentToBody(ConfirmationComponent, { + confirmation$: this.confirmation$, + clear: this.clear, + }), + ); + + setTimeout(() => { + this.containerComponentRef.changeDetectorRef.detectChanges(); + }, 0); + } + + info( + message: LocalizationParam, + title: LocalizationParam, + options?: Partial, + ): Observable { + return this.show(message, title, 'info', options); + } + + success( + message: LocalizationParam, + title: LocalizationParam, + options?: Partial, + ): Observable { + return this.show(message, title, 'success', options); + } + + warn( + message: LocalizationParam, + title: LocalizationParam, + options?: Partial, + ): Observable { + return this.show(message, title, 'warning', options); + } + + error( + message: LocalizationParam, + title: LocalizationParam, + options?: Partial, + ): Observable { + return this.show(message, title, 'error', options); + } + + show( + message: LocalizationParam, + title: LocalizationParam, + severity?: Confirmation.Severity, + options = {} as Partial, + ): Observable { + if (!this.containerComponentRef) this.setContainer(); + + this.confirmation$.next({ + message, + title, + severity: severity || 'neutral', + options, + }); + + this.status$ = new Subject(); + const { dismissible = true } = options; + if (dismissible) this.listenToEscape(); + + return this.status$; + } + + private listenToEscape() { + fromEvent(document, 'keyup') + .pipe( + takeUntil(this.status$), + debounceTime(150), + filter((key: KeyboardEvent) => key && key.key === 'Escape'), + ) + .subscribe(_ => { + this.clear(); + }); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/index.ts new file mode 100644 index 0000000000..1b39fcc0f0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/index.ts @@ -0,0 +1,5 @@ +export * from './confirmation.service'; +export * from './modal.service'; +export * from './toaster.service'; +export * from './nav-items.service'; +export * from './page-alert.service'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/modal.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/modal.service.ts new file mode 100644 index 0000000000..a75cfe5089 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/modal.service.ts @@ -0,0 +1,54 @@ +import { ContentProjectionService, PROJECTION_STRATEGY } from '@abp/ng.core'; +import { ComponentRef, Injectable, TemplateRef, ViewContainerRef, OnDestroy } from '@angular/core'; +import { ModalContainerComponent } from '../components/modal/modal-container.component'; + +/** + * @deprecated Use ng-bootstrap modal. To be deleted in v5.0. + */ +@Injectable({ + providedIn: 'root', +}) +export class ModalService implements OnDestroy { + private containerComponentRef: ComponentRef; + + constructor(private contentProjectionService: ContentProjectionService) { + this.setContainer(); + } + + private setContainer() { + this.containerComponentRef = this.contentProjectionService.projectContent( + PROJECTION_STRATEGY.AppendComponentToBody(ModalContainerComponent), + ); + + this.containerComponentRef.changeDetectorRef.detectChanges(); + } + + clearModal() { + this.getContainer().clear(); + this.detectChanges(); + } + + detectChanges() { + this.containerComponentRef.changeDetectorRef.detectChanges(); + } + + getContainer(): ViewContainerRef { + return this.containerComponentRef.instance.container; + } + + renderTemplate(template: TemplateRef, context?: T) { + const containerRef = this.getContainer(); + + const strategy = PROJECTION_STRATEGY.ProjectTemplateToContainer( + template, + containerRef, + context, + ); + + this.contentProjectionService.projectContent(strategy); + } + + ngOnDestroy() { + this.containerComponentRef.destroy(); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/nav-items.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/nav-items.service.ts new file mode 100644 index 0000000000..ca361a2940 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/nav-items.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { NavItem } from '../models/nav-item'; + +@Injectable({ providedIn: 'root' }) +export class NavItemsService { + private _items$ = new BehaviorSubject([]); + + get items(): NavItem[] { + return this._items$.value; + } + + get items$(): Observable { + return this._items$.asObservable(); + } + + addItems(newItems: NavItem[]) { + const items = [...this.items]; + newItems.forEach(item => items.push(new NavItem(item))); + items.sort(sortItems); + this._items$.next(items); + } + + removeItem(id: string | number) { + const index = this.items.findIndex(item => item.id === id); + + if (index < 0) return; + + const items = [...this.items.slice(0, index), ...this.items.slice(index + 1)]; + this._items$.next(items); + } + + patchItem(id: string | number, item: Partial>) { + const index = this.items.findIndex(i => i.id === id); + + if (index < 0) return; + + const items = [...this.items]; + items[index] = new NavItem({ ...items[index], ...item }); + items.sort(sortItems); + this._items$.next(items); + } +} + +function sortItems(a: NavItem, b: NavItem) { + if (!a.order) return 1; + if (!b.order) return -1; + + return a.order - b.order; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/page-alert.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/page-alert.service.ts new file mode 100644 index 0000000000..c66d02c004 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/page-alert.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { InternalStore } from '@abp/ng.core'; + +export interface PageAlert { + type: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark'; + message: string; + dismissible?: boolean; + title?: string; + messageLocalizationParams?: string[]; + titleLocalizationParams?: string[]; +} + +@Injectable({ providedIn: 'root' }) +export class PageAlertService { + private alerts = new InternalStore([]); + + alerts$ = this.alerts.sliceState(state => state); + + constructor() {} + + show(alert: PageAlert) { + const newAlert: PageAlert = { + ...alert, + dismissible: alert.dismissible ?? true, + }; + this.alerts.set([newAlert, ...this.alerts.state]); + } + + remove(index: number) { + const alerts = [...this.alerts.state]; + alerts.splice(index, 1); + this.alerts.set(alerts); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/toaster.service.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/toaster.service.ts new file mode 100644 index 0000000000..efd04b6ba0 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/services/toaster.service.ts @@ -0,0 +1,138 @@ +import { + ContentProjectionService, + LocalizationParam, + PROJECTION_STRATEGY, + Strict, +} from '@abp/ng.core'; +import { ComponentRef, Injectable } from '@angular/core'; +import { ReplaySubject } from 'rxjs'; +import snq from 'snq'; +import { ToastContainerComponent } from '../components/toast-container/toast-container.component'; +import { Toaster } from '../models'; + +@Injectable({ + providedIn: 'root', +}) +export class ToasterService implements ToasterContract { + private toasts$ = new ReplaySubject(1); + + private lastId = -1; + + private toasts = [] as Toaster.Toast[]; + + private containerComponentRef: ComponentRef; + + constructor(private contentProjectionService: ContentProjectionService) {} + + private setContainer() { + this.containerComponentRef = this.contentProjectionService.projectContent( + PROJECTION_STRATEGY.AppendComponentToBody(ToastContainerComponent, { toasts$: this.toasts$ }), + ); + + this.containerComponentRef.changeDetectorRef.detectChanges(); + } + + /** + * Creates an info toast with given parameters. + * @param message Content of the toast + * @param title Title of the toast + * @param options Spesific style or structural options for individual toast + */ + info( + message: LocalizationParam, + title?: LocalizationParam, + options?: Partial, + ): Toaster.ToasterId { + return this.show(message, title, 'info', options); + } + + /** + * Creates a success toast with given parameters. + * @param message Content of the toast + * @param title Title of the toast + * @param options Spesific style or structural options for individual toast + */ + success( + message: LocalizationParam, + title?: LocalizationParam, + options?: Partial, + ): Toaster.ToasterId { + return this.show(message, title, 'success', options); + } + + /** + * Creates a warning toast with given parameters. + * @param message Content of the toast + * @param title Title of the toast + * @param options Spesific style or structural options for individual toast + */ + warn( + message: LocalizationParam, + title?: LocalizationParam, + options?: Partial, + ): Toaster.ToasterId { + return this.show(message, title, 'warning', options); + } + + /** + * Creates an error toast with given parameters. + * @param message Content of the toast + * @param title Title of the toast + * @param options Spesific style or structural options for individual toast + */ + error( + message: LocalizationParam, + title?: LocalizationParam, + options?: Partial, + ): Toaster.ToasterId { + return this.show(message, title, 'error', options); + } + + /** + * Creates a toast with given parameters. + * @param message Content of the toast + * @param title Title of the toast + * @param severity Sets color of the toast. "success", "warning" etc. + * @param options Spesific style or structural options for individual toast + */ + + show( + message: LocalizationParam, + title: LocalizationParam = null, + severity: Toaster.Severity = 'neutral', + options = {} as Partial, + ): Toaster.ToasterId { + if (!this.containerComponentRef) this.setContainer(); + + const id = ++this.lastId; + this.toasts.push({ + message, + title, + severity, + options: { closable: true, id, ...options }, + }); + this.toasts$.next(this.toasts); + return id; + } + + /** + * Removes the toast with given id. + * @param id ID of the toast to be removed. + */ + remove(id: number): void { + this.toasts = this.toasts.filter(toast => snq(() => toast.options.id) !== id); + this.toasts$.next(this.toasts); + } + + /** + * Removes all open toasts at once. + */ + clear(containerKey?: string): void { + this.toasts = !containerKey + ? [] + : this.toasts.filter(toast => snq(() => toast.options.containerKey) !== containerKey); + this.toasts$.next(this.toasts); + } +} + +export type ToasterContract = Strict; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/append-content.token.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/append-content.token.spec.ts new file mode 100644 index 0000000000..fe3b0ac37c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/append-content.token.spec.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator'; +import { THEME_SHARED_APPEND_CONTENT } from '../tokens/append-content.token'; +import { DomInsertionService } from '@abp/ng.core'; +import { chartJsLoaded$ } from '../utils'; +import styles from '../constants/styles'; + +@Component({ selector: 'abp-dummy', template: '' }) +class DummyComponent {} + +describe('AppendContentToken', () => { + let spectator: Spectator; + const createComponent = createComponentFactory(DummyComponent); + + beforeEach(() => (spectator = createComponent())); + + it('should insert a style element to the DOM', () => { + spectator.inject(THEME_SHARED_APPEND_CONTENT); + expect(spectator.inject(DomInsertionService).has(styles)).toBe(true); + }); + + it('should be loaded the chart.js', done => { + chartJsLoaded$.subscribe(loaded => { + expect(loaded).toBe(true); + done(); + }); + + spectator.inject(THEME_SHARED_APPEND_CONTENT); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/breadcrumb.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/breadcrumb.component.spec.ts new file mode 100644 index 0000000000..87487f9b04 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/breadcrumb.component.spec.ts @@ -0,0 +1,84 @@ +import { + ABP, + CORE_OPTIONS, + LocalizationPipe, + RouterOutletComponent, + RoutesService, +} from '@abp/ng.core'; +import { HttpClient } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; +import { createRoutingFactory, SpectatorRouting, SpyObject } from '@ngneat/spectator/jest'; +import { Store } from '@ngxs/store'; +import { mockRoutesService } from '../../../../core/src/lib/tests/routes.service.spec'; +import { BreadcrumbComponent } from '../components/breadcrumb/breadcrumb.component'; + +const mockRoutes: ABP.Route[] = [ + { name: 'Identity', path: '/identity' }, + { name: 'Users', path: '/identity/users', parentName: 'Identity' }, +]; + +describe('BreadcrumbComponent', () => { + let spectator: SpectatorRouting; + let routes: RoutesService; + let store: SpyObject; + + const createRouting = createRoutingFactory({ + component: RouterOutletComponent, + stubsEnabled: false, + detectChanges: false, + mocks: [Store, HttpClient], + providers: [ + { provide: CORE_OPTIONS, useValue: {} }, + { + provide: RoutesService, + useFactory: () => mockRoutesService(), + }, + ], + declarations: [LocalizationPipe, BreadcrumbComponent], + imports: [RouterModule], + routes: [ + { + path: '', + children: [ + { + path: 'identity', + children: [ + { + path: 'users', + component: BreadcrumbComponent, + }, + ], + }, + ], + }, + ], + }); + + beforeEach(() => { + spectator = createRouting(); + routes = spectator.inject(RoutesService); + store = spectator.inject(Store); + }); + + it('should display the breadcrumb', async () => { + routes.add(mockRoutes); + await spectator.router.navigateByUrl('/identity/users'); + // for abpLocalization + store.selectSnapshot.mockReturnValueOnce('Identity'); + store.selectSnapshot.mockReturnValueOnce('Users'); + spectator.detectChanges(); + + const elements = spectator.queryAll('li'); + expect(elements).toHaveLength(3); + expect(elements[1]).toHaveText('Identity'); + expect(elements[2]).toHaveText('Users'); + }); + + it('should not display the breadcrumb when empty', async () => { + routes.add([]); + await spectator.router.navigateByUrl('/identity/users'); + + spectator.detectChanges(); + expect(spectator.query('ol.breadcrumb')).toBeFalsy(); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/button.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/button.component.spec.ts new file mode 100644 index 0000000000..c272b665e7 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/button.component.spec.ts @@ -0,0 +1,56 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { ButtonComponent } from '../components'; + +describe('ButtonComponent', () => { + let spectator: SpectatorHost; + + const createHost = createHostFactory(ButtonComponent); + + beforeEach( + () => + (spectator = createHost('Button', { + hostProps: { attributes: { autofocus: '', name: 'abp-button' } }, + })), + ); + + it('should display the button', () => { + expect(spectator.query('button')).toBeTruthy(); + }); + + it('should equal the default classes to btn btn-primary', () => { + expect(spectator.query('button')).toHaveClass('btn btn-primary'); + }); + + it('should equal the default type to button', () => { + expect(spectator.query('button')).toHaveAttribute('type', 'button'); + }); + + it('should enabled', () => { + expect(spectator.query('[disabled]')).toBeFalsy(); + }); + + it('should have the text content', () => { + expect(spectator.query('button')).toHaveText('Button'); + }); + + it('should display the icon', () => { + expect(spectator.query('i.d-none')).toBeFalsy(); + expect(spectator.query('i')).toHaveClass('fa'); + }); + + it('should display the spinner icon', () => { + spectator.component.loading = true; + spectator.detectComponentChanges(); + expect(spectator.query('i')).toHaveClass('fa-spinner'); + }); + + it('should disabled when the loading input is true', () => { + spectator.component.loading = true; + spectator.detectComponentChanges(); + expect(spectator.query('[disabled]')).toBeTruthy(); + }); + + it('should disabled when the loading input is true', () => { + expect(spectator.query('[autofocus][name="abp-button"]')).toBeTruthy(); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/chart.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/chart.component.spec.ts new file mode 100644 index 0000000000..b1bd0fba16 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/chart.component.spec.ts @@ -0,0 +1,126 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { ChartComponent } from '../components'; +import { chartJsLoaded$ } from '../utils/widget-utils'; +import { ReplaySubject } from 'rxjs'; +import * as widgetUtils from '../utils/widget-utils'; +// import 'chart.js'; +declare const Chart; + +Object.defineProperty(window, 'getComputedStyle', { + value: () => ({ + getPropertyValue: prop => { + return ''; + }, + }), +}); + +describe('ChartComponent', () => { + let spectator: SpectatorHost; + const createHost = createHostFactory({ component: ChartComponent }); + + beforeEach(() => { + (widgetUtils as any).chartJsLoaded$ = new ReplaySubject(1); + spectator = createHost('', { + hostProps: { + data: { + datasets: [ + { + data: [11], + backgroundColor: ['#FF6384'], + label: 'My dataset', + }, + ], + labels: ['Red'], + }, + }, + }); + }); + + test('should throw error when chart.js is not loaded', () => { + try { + spectator.component.testChartJs(); + } catch (error) { + expect(error.message).toContain('Chart is not found'); + } + }); + + test('should have a success class by default', async done => { + await import('chart.js'); + + chartJsLoaded$.next(); + setTimeout(() => { + expect(spectator.component.chart).toBeTruthy(); + done(); + }, 0); + }); + + describe('#reinit', () => { + it('should call the destroy method', done => { + chartJsLoaded$.next(); + const spy = jest.spyOn(spectator.component.chart, 'destroy'); + spectator.setHostInput({ + data: { + datasets: [ + { + data: [12], + label: 'My dataset', + }, + ], + labels: ['Red'], + }, + }); + spectator.detectChanges(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('#refresh', () => { + it('should call the update method', done => { + chartJsLoaded$.next(); + const spy = jest.spyOn(spectator.component.chart, 'update'); + spectator.component.refresh(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('#generateLegend', () => { + it('should call the generateLegend method', done => { + chartJsLoaded$.next(); + const spy = jest.spyOn(spectator.component.chart, 'generateLegend'); + spectator.component.generateLegend(); + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }, 0); + }); + }); + + describe('#onCanvasClick', () => { + it('should emit the onDataSelect', done => { + spectator.component.onDataSelect.subscribe(() => { + done(); + }); + + chartJsLoaded$.next(); + jest.spyOn(spectator.component.chart, 'getElementAtEvent').mockReturnValue([document.createElement('div')]); + spectator.click('canvas'); + }); + }); + + describe('#base64Image', () => { + it('should return the base64 image', done => { + chartJsLoaded$.next(); + + setTimeout(() => { + expect(spectator.component.base64Image).toContain('base64'); + done(); + }, 0); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/confirmation.service.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/confirmation.service.spec.ts new file mode 100644 index 0000000000..ab63cb3cb5 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/confirmation.service.spec.ts @@ -0,0 +1,135 @@ +import { CoreTestingModule } from '@abp/ng.core/testing'; +import { NgModule } from '@angular/core'; +import { fakeAsync, tick } from '@angular/core/testing'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { NgxsModule } from '@ngxs/store'; +import { timer } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ConfirmationComponent } from '../components'; +import { Confirmation } from '../models'; +import { ConfirmationService } from '../services'; + +@NgModule({ + exports: [ConfirmationComponent], + entryComponents: [ConfirmationComponent], + declarations: [ConfirmationComponent], + imports: [CoreTestingModule.withConfig()], +}) +export class MockModule {} + +describe('ConfirmationService', () => { + let spectator: SpectatorService; + let service: ConfirmationService; + const createService = createServiceFactory({ + service: ConfirmationService, + imports: [NgxsModule.forRoot(), CoreTestingModule.withConfig(), MockModule], + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + }); + + afterEach(() => { + clearElements(); + }); + + test('should display a confirmation popup', fakeAsync(() => { + service.show('MESSAGE', 'TITLE'); + + tick(); + + expect(selectConfirmationContent('.title')).toBe('TITLE'); + expect(selectConfirmationContent('.message')).toBe('MESSAGE'); + })); + + test('should display HTML string in title, message, and buttons', fakeAsync(() => { + service.show( + 'MESSAGE', + 'TITLE', + 'neutral', + { + cancelText: 'CANCEL', + yesText: 'YES', + }, + ); + + tick(); + + expect(selectConfirmationContent('.custom-title')).toBe('TITLE'); + expect(selectConfirmationContent('.custom-message')).toBe('MESSAGE'); + expect(selectConfirmationContent('.custom-cancel')).toBe('CANCEL'); + expect(selectConfirmationContent('.custom-yes')).toBe('YES'); + })); + + test.each` + type | selector | icon + ${'info'} | ${'.info'} | ${'.fa-info-circle'} + ${'success'} | ${'.success'} | ${'.fa-check-circle'} + ${'warn'} | ${'.warning'} | ${'.fa-exclamation-triangle'} + ${'error'} | ${'.error'} | ${'.fa-times-circle'} + `('should display $type confirmation popup', async ({ type, selector, icon }) => { + service[type]('MESSAGE', 'TITLE'); + + await timer(0).toPromise(); + + expect(selectConfirmationContent('.title')).toBe('TITLE'); + expect(selectConfirmationContent('.message')).toBe('MESSAGE'); + expect(selectConfirmationElement(selector)).toBeTruthy(); + expect(selectConfirmationElement(icon)).toBeTruthy(); + }); + + test('should close with ESC key', done => { + service + .info('', '') + .pipe(take(1)) + .subscribe(status => { + expect(status).toBe(Confirmation.Status.dismiss); + done(); + }); + + const escape = new KeyboardEvent('keyup', { key: 'Escape' }); + document.dispatchEvent(escape); + }); + + test('should close when click cancel button', async done => { + service.info('', '', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(status => { + expect(status).toBe(Confirmation.Status.reject); + done(); + }); + + await timer(0).toPromise(); + + expect(selectConfirmationContent('button#cancel')).toBe('Exit'); + expect(selectConfirmationContent('button#confirm')).toBe('Sure'); + + selectConfirmationElement('button#cancel').click(); + }); + + test.each` + dismissible | count + ${true} | ${1} + ${false} | ${0} + `( + 'should call the listenToEscape method $count times when dismissible is $dismissible', + ({ dismissible, count }) => { + const spy = spyOn(service as any, 'listenToEscape'); + + service.info('', '', { dismissible }); + + expect(spy).toHaveBeenCalledTimes(count); + }, + ); +}); + +function clearElements(selector = '.confirmation') { + document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element)); +} + +function selectConfirmationContent(selector = '.confirmation'): string { + return selectConfirmationElement(selector).textContent.trim(); +} + +function selectConfirmationElement(selector = '.confirmation'): T { + return document.querySelector(selector); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/ellipsis.directive.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/ellipsis.directive.spec.ts new file mode 100644 index 0000000000..727a1c5312 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/ellipsis.directive.spec.ts @@ -0,0 +1,55 @@ +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { EllipsisDirective } from '../directives/ellipsis.directive'; + +describe('EllipsisDirective', () => { + let spectator: SpectatorDirective; + let directive: EllipsisDirective; + let el: HTMLDivElement; + const createDirective = createDirectiveFactory({ + directive: EllipsisDirective, + }); + + beforeEach(() => { + spectator = createDirective( + '
test content
', + { + hostProps: { + title: 'test title', + width: '100px', + }, + }, + ); + directive = spectator.directive; + el = spectator.query('div'); + }); + + test('should be created', () => { + expect(directive).toBeTruthy(); + }); + + test('should have 100px ellipsis width', () => { + expect(directive.width).toBe('100px'); + }); + + test('should be enabled if abpEllipsisEnabled input is true', () => { + expect(directive.enabled).toBe(true); + }); + + test('should have given title', () => { + expect(directive.title).toBe('test title'); + }); + + test('should have element innerText as title if not specified', () => { + spectator.setHostInput({ title: undefined }); + expect(directive.title).toBe(el.innerText); + }); + + test('should add abp-ellipsis-inline class to element if width is given', () => { + expect(el).toHaveClass('abp-ellipsis-inline'); + }); + + test('should add abp-ellipsis class to element if width is not given', () => { + spectator.setHostInput({ width: undefined }); + expect(el).toHaveClass('abp-ellipsis'); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.component.spec.ts new file mode 100644 index 0000000000..e86f620dad --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.component.spec.ts @@ -0,0 +1,44 @@ +import { SpectatorHost, createHostFactory } from '@ngneat/spectator/jest'; +import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; +import { CORE_OPTIONS, LocalizationPipe } from '@abp/ng.core'; +import { Store } from '@ngxs/store'; +import { Renderer2, ElementRef } from '@angular/core'; +import { Subject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +describe('ErrorComponent', () => { + let spectator: SpectatorHost; + const createHost = createHostFactory({ + component: HttpErrorWrapperComponent, + declarations: [LocalizationPipe], + mocks: [Store, HttpClient], + providers: [ + { provide: CORE_OPTIONS, useValue: {} }, + { provide: Renderer2, useValue: { removeChild: () => null } }, + { provide: ElementRef, useValue: { nativeElement: document.createElement('div') } }, + ], + }); + + beforeEach(() => { + spectator = createHost(''); + spectator.component.destroy$ = new Subject(); + }); + + describe('#destroy', () => { + it('should be call when pressed the esc key', done => { + spectator.component.destroy$.subscribe(res => { + done(); + }); + + spectator.keyboard.pressEscape(); + }); + + it('should be call when clicked the close button', done => { + spectator.component.destroy$.subscribe(res => { + done(); + }); + + spectator.click('#abp-close-button'); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.handler.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.handler.spec.ts new file mode 100644 index 0000000000..a6908e03bf --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/error.handler.spec.ts @@ -0,0 +1,328 @@ +import { RestOccurError } from '@abp/ng.core'; +import { CoreTestingModule } from '@abp/ng.core/testing'; +import { APP_BASE_HREF } from '@angular/common'; +import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { Component, NgModule } from '@angular/core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { NgxsModule, Store } from '@ngxs/store'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { of } from 'rxjs'; +import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; +import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers'; +import { ConfirmationService } from '../services'; +import { httpErrorConfigFactory } from '../tokens/http-error.token'; + +@NgModule({ + exports: [HttpErrorWrapperComponent], + declarations: [HttpErrorWrapperComponent], + entryComponents: [HttpErrorWrapperComponent], + imports: [CoreTestingModule], +}) +class MockModule {} + +let spectator: SpectatorService; +let service: ErrorHandler; +let store: Store; +const errorConfirmation: jest.Mock = jest.fn(() => of(null)); +const CONFIRMATION_BUTTONS = { + hideCancelBtn: true, + yesText: 'AbpAccount::Close', +}; +describe('ErrorHandler', () => { + const createService = createServiceFactory({ + service: ErrorHandler, + imports: [NgxsModule.forRoot([]), CoreTestingModule.withConfig(), MockModule], + mocks: [OAuthService], + providers: [ + { provide: APP_BASE_HREF, useValue: '/' }, + { + provide: 'HTTP_ERROR_CONFIG', + useFactory: httpErrorConfigFactory, + }, + { + provide: ConfirmationService, + useValue: { + error: errorConfirmation, + }, + }, + ], + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + store = spectator.inject(Store); + store.selectSnapshot = jest.fn(() => '/x'); + }); + + afterEach(() => { + errorConfirmation.mockClear(); + removeIfExistsInDom(selectHtmlErrorWrapper); + }); + + test('should display HttpErrorWrapperComponent when server error occurs', () => { + const createComponent = jest.spyOn(service, 'createErrorComponent'); + const error = new HttpErrorResponse({ status: 500 }); + const params = { + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title, + }, + details: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details, + }, + status: 500, + }; + + expect(selectHtmlErrorWrapper()).toBeNull(); + + store.dispatch(new RestOccurError(error)); + + expect(createComponent).toHaveBeenCalledWith(params); + }); + + test('should display HttpErrorWrapperComponent when authorize error occurs', () => { + const createComponent = jest.spyOn(service, 'createErrorComponent'); + const error = new HttpErrorResponse({ status: 403 }); + const params = { + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title, + }, + details: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details, + }, + status: 403, + }; + + expect(selectHtmlErrorWrapper()).toBeNull(); + + store.dispatch(new RestOccurError(error)); + + expect(createComponent).toHaveBeenCalledWith(params); + }); + + test('should display HttpErrorWrapperComponent when unknown error occurs', () => { + const createComponent = jest.spyOn(service, 'createErrorComponent'); + const error = new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' }); + const params = { + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }, + details: error.message, + isHomeShow: false, + }; + + expect(selectHtmlErrorWrapper()).toBeNull(); + + store.dispatch(new RestOccurError(error)); + + expect(createComponent).toHaveBeenCalledWith(params); + }); + + test('should call error method of ConfirmationService when not found error occurs', () => { + store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 }))); + + expect(errorConfirmation).toHaveBeenCalledWith( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details, + }, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, + }, + CONFIRMATION_BUTTONS, + ); + }); + + test('should call error method of ConfirmationService when default error occurs', () => { + store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 }))); + + expect(errorConfirmation).toHaveBeenCalledWith( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, + }, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }, + CONFIRMATION_BUTTONS, + ); + }); + + test('should call error method of ConfirmationService when authenticated error occurs', () => { + store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 }))); + + expect(errorConfirmation).toHaveBeenCalledWith( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, + }, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details, + }, + CONFIRMATION_BUTTONS, + ); + }); + + test('should call error method of ConfirmationService when authenticated error occurs with _AbpErrorFormat header', () => { + const headers: HttpHeaders = new HttpHeaders({ + _AbpErrorFormat: '_AbpErrorFormat', + }); + store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401, headers }))); + + expect(errorConfirmation).toHaveBeenCalledWith( + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }, + null, + CONFIRMATION_BUTTONS, + ); + }); + + test('should call error method of ConfirmationService when error occurs with _AbpErrorFormat header', () => { + let headers: HttpHeaders = new HttpHeaders(); + headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat'); + store.dispatch( + new RestOccurError( + new HttpErrorResponse({ + error: { error: { message: 'test message', details: 'test detail' } }, + status: 412, + headers, + }), + ), + ); + + expect(errorConfirmation).toHaveBeenCalledWith( + 'test detail', + 'test message', + CONFIRMATION_BUTTONS, + ); + }); +}); + +@Component({ + selector: 'abp-dummy-error', + template: '

{{errorStatus}}

', +}) +class DummyErrorComponent { + errorStatus; + destroy$; +} + +@NgModule({ + declarations: [DummyErrorComponent], + exports: [DummyErrorComponent], + entryComponents: [DummyErrorComponent], +}) +class ErrorModule {} + +// TODO: error component does not place to the DOM. +// describe('ErrorHandler with custom error component', () => { +// const createService = createServiceFactory({ +// service: ErrorHandler, +// imports: [ +// RouterModule.forRoot([], { relativeLinkResolution: 'legacy' }), +// NgxsModule.forRoot([]), +// CoreModule, +// MockModule, +// ErrorModule, +// ], +// mocks: [OAuthService, ConfirmationService], +// providers: [ +// { provide: APP_BASE_HREF, useValue: '/' }, +// { +// provide: 'HTTP_ERROR_CONFIG', +// useFactory: customHttpErrorConfigFactory, +// }, +// ], +// }); + +// beforeEach(() => { +// spectator = createService(); +// service = spectator.service; +// store = spectator.inject(Store); +// store.selectSnapshot = jest.fn(() => '/x'); +// }); + +// afterEach(() => { +// removeIfExistsInDom(selectCustomError); +// }); + +// describe('Custom error component', () => { +// test('should be created when 401 error is dispatched', () => { +// store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 }))); + +// expect(selectCustomErrorText()).toBe('401'); +// }); + +// test('should be created when 403 error is dispatched', () => { +// store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 }))); + +// expect(selectCustomErrorText()).toBe('403'); +// }); + +// test('should be created when 404 error is dispatched', () => { +// store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 }))); + +// expect(selectCustomErrorText()).toBe('404'); +// }); + +// test('should be created when RouterError is dispatched', () => { +// store.dispatch(new RouterError(null, null, new NavigationError(1, 'test', 'Cannot match'))); + +// expect(selectCustomErrorText()).toBe('404'); +// }); + +// test('should be created when 500 error is dispatched', () => { +// store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 }))); + +// expect(selectCustomErrorText()).toBe('500'); +// }); + +// test('should call destroy method of componentRef when destroy$ emits', () => { +// store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 }))); + +// expect(selectCustomErrorText()).toBe('401'); + +// const destroyComponent = jest.spyOn(service.componentRef, 'destroy'); + +// service.componentRef.instance.destroy$.next(); + +// expect(destroyComponent).toHaveBeenCalledTimes(1); +// }); +// }); +// }); + +export function customHttpErrorConfigFactory() { + return httpErrorConfigFactory({ + errorScreen: { + component: DummyErrorComponent, + forWhichErrors: [401, 403, 404, 500], + }, + }); +} + +function removeIfExistsInDom(errorSelector: () => HTMLDivElement | null) { + const abpError = errorSelector(); + if (abpError) abpError.parentNode.removeChild(abpError); +} + +function selectHtmlErrorWrapper(): HTMLDivElement | null { + return document.querySelector('abp-http-error-wrapper'); +} + +function selectCustomError(): HTMLDivElement | null { + return document.querySelector('abp-dummy-error'); +} + +function selectCustomErrorText(): string { + return selectCustomError().querySelector('p').textContent; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/lazy-style.handler.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/lazy-style.handler.spec.ts new file mode 100644 index 0000000000..1c120db08f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/lazy-style.handler.spec.ts @@ -0,0 +1,66 @@ +import { LazyLoadService, LOADING_STRATEGY, LocalizationService } from '@abp/ng.core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { EMPTY, of } from 'rxjs'; +import { BOOTSTRAP } from '../constants/styles'; +import { createLazyStyleHref, initLazyStyleHandler, LazyStyleHandler } from '../handlers'; + +const languageChange$ = of({ payload: 'en' }); + +describe('LazyStyleHandler', () => { + let spectator: SpectatorService; + let handler: LazyStyleHandler; + let lazyLoad: LazyLoadService; + + const createService = createServiceFactory({ + service: LazyStyleHandler, + providers: [ + { + provide: LocalizationService, + useValue: { currentLang: 'en', languageChange$ }, + }, + ], + }); + + beforeEach(() => { + spectator = createService(); + handler = spectator.service; + lazyLoad = handler['lazyLoad']; + }); + + describe('#dir', () => { + it('should initially be "ltr"', () => { + expect(handler.dir).toBe('ltr'); + }); + + it('should set bootstrap to rtl', () => { + const oldHref = createLazyStyleHref(BOOTSTRAP, 'ltr'); + const newHref = createLazyStyleHref(BOOTSTRAP, 'rtl'); + lazyLoad.loaded.set(newHref, null); // avoid actual loading + const load = jest.spyOn(lazyLoad, 'load'); + const remove = jest.spyOn(lazyLoad, 'remove'); + const strategy = LOADING_STRATEGY.PrependAnonymousStyleToHead(newHref); + + handler.dir = 'rtl'; + + expect(load).toHaveBeenCalledWith(strategy); + expect(remove).toHaveBeenCalledWith(oldHref); + }); + }); +}); + +describe('initLazyStyleHandler', () => { + it('should return a LazyStyleHandler factory', () => { + const generator = (function*() { + yield undefined; // LAZY_STYLES + yield { loaded: new Map() }; // LazyLoadService + yield { currentLang: 'en', languageChange$: EMPTY }; // LocalizationService + })(); + + const injector = { + get: () => generator.next().value as any, + }; + const factory = initLazyStyleHandler(injector); + + expect(factory()).toBeInstanceOf(LazyStyleHandler); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loader-bar.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loader-bar.component.spec.ts new file mode 100644 index 0000000000..2326009044 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loader-bar.component.spec.ts @@ -0,0 +1,94 @@ +import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; +import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator/jest'; +import { Subject, timer } from 'rxjs'; +import { LoaderBarComponent } from '../components/loader-bar/loader-bar.component'; +import { HttpWaitService, LOADER_DELAY, SubscriptionService } from '@abp/ng.core'; +import { HttpRequest } from '@angular/common/http'; + +describe('LoaderBarComponent', () => { + let spectator: Spectator; + let router: SpyObject; + const events$ = new Subject(); + + const createComponent = createComponentFactory({ + component: LoaderBarComponent, + detectChanges: false, + providers: [ + SubscriptionService, + { provide: Router, useValue: { events: events$ } }, + { provide: LOADER_DELAY, useValue: 0 }, + ], + }); + + beforeEach(() => { + spectator = createComponent({}); + spectator.component.intervalPeriod = 1; + spectator.component.stopDelay = 1; + router = spectator.inject(Router); + }); + + it('should initial variable values are correct', () => { + expect(spectator.component.containerClass).toBe('abp-loader-bar'); + expect(spectator.component.color).toBe('#77b6ff'); + }); + + it('should increase the progressLevel', done => { + spectator.detectChanges(); + const httpWaitService = spectator.inject(HttpWaitService); + httpWaitService.addRequest(new HttpRequest('GET', 'test')); + spectator.detectChanges(); + setTimeout(() => { + expect(spectator.component.progressLevel > 0).toBeTruthy(); + done(); + }, 10); + }); + + it('should be interval unsubscribed', done => { + spectator.detectChanges(); + const httpWaitService = spectator.inject(HttpWaitService); + httpWaitService.addRequest(new HttpRequest('GET', 'test')); + expect(spectator.component.interval.closed).toBe(false); + timer(400).subscribe(() => { + expect(spectator.component.interval.closed).toBe(true); + done(); + }); + }); + + it('should start and stop the loading with navigation', done => { + spectator.detectChanges(); + events$.next(new NavigationStart(1, 'test')); + expect(spectator.component.interval.closed).toBe(false); + + events$.next(new NavigationEnd(1, 'test', 'test')); + events$.next(new NavigationError(1, 'test', 'test')); + expect(spectator.component.progressLevel).toBe(100); + + timer(2).subscribe(() => { + expect(spectator.component.progressLevel).toBe(0); + done(); + }); + }); + + it('should stop the loading with navigation', done => { + spectator.detectChanges(); + events$.next(new NavigationStart(1, 'test')); + expect(spectator.component.interval.closed).toBe(false); + + events$.next(new NavigationEnd(1, 'testend', 'testend')); + expect(spectator.component.progressLevel).toBe(100); + + timer(2).subscribe(() => { + expect(spectator.component.progressLevel).toBe(0); + done(); + }); + }); + + describe('#startLoading', () => { + it('should return when isLoading is true', done => { + spectator.detectChanges(); + events$.next(new NavigationStart(1, 'test')); + events$.next(new NavigationStart(1, 'test')); + done(); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loading.directive.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loading.directive.spec.ts new file mode 100644 index 0000000000..b359a2000c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/loading.directive.spec.ts @@ -0,0 +1,93 @@ +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { LoadingDirective } from '../directives'; +import { LoadingComponent } from '../components'; + +import { Component } from '@angular/core'; + +@Component({ + selector: 'abp-dummy', + template: '
Testing Loading Directive
', +}) +export class DummyComponent {} + +describe('LoadingDirective', () => { + let spectator: SpectatorDirective; + const createDirective = createDirectiveFactory({ + directive: LoadingDirective, + declarations: [LoadingComponent, DummyComponent], + entryComponents: [LoadingComponent], + }); + + describe('default', () => { + beforeEach(() => { + spectator = createDirective('
Testing Loading Directive
', { + hostProps: { loading: true }, + }); + }); + + it('should create the loading component', done => { + setTimeout(() => { + expect(spectator.directive.rootNode).toBeTruthy(); + expect(spectator.directive.componentRef).toBeTruthy(); + done(); + }, 20); + }); + }); + + describe('with custom target', () => { + const mockTarget = document.createElement('div'); + const spy = jest.spyOn(mockTarget, 'appendChild'); + + beforeEach(() => { + spectator = createDirective( + '
Testing Loading Directive
', + { + hostProps: { loading: true, target: mockTarget, delay: 0 }, + }, + ); + }); + + it('should add the loading component to the DOM', done => { + setTimeout(() => { + expect(spy).toHaveBeenCalled(); + done(); + }, 20); + }); + + it('should remove the loading component to the DOM', done => { + const rendererSpy = jest.spyOn(spectator.directive['renderer'], 'removeChild'); + setTimeout(() => spectator.setHostInput({ loading: false }), 0); + setTimeout(() => { + expect(rendererSpy).toHaveBeenCalled(); + expect(spectator.directive.rootNode).toBeFalsy(); + done(); + }, 20); + }); + + it('should appear with delay', done => { + spectator.setHostInput({ loading: false, delay: 20 }); + spectator.detectChanges(); + setTimeout(() => spectator.setHostInput({ loading: true }), 0); + setTimeout(() => expect(spectator.directive.loading).toBe(false), 15); + setTimeout(() => { + expect(spectator.directive.loading).toBe(true); + done(); + }, 50); + }); + }); + + describe('with a component selector', () => { + beforeEach(() => { + spectator = createDirective('', { + hostProps: { loading: true }, + }); + }); + + it('should select the child element', done => { + setTimeout(() => { + expect(spectator.directive.targetElement.id).toBe('dummy'); + done(); + }, 20); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal-container.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal-container.component.spec.ts new file mode 100644 index 0000000000..9cb45127e5 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal-container.component.spec.ts @@ -0,0 +1,34 @@ +import { Component, ComponentFactoryResolver, ComponentRef } from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { ModalContainerComponent } from '../components/modal/modal-container.component'; + +describe('ModalContainerComponent', () => { + @Component({ template: '
bar
' }) + class TestComponent {} + + let componentRef: ComponentRef; + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: ModalContainerComponent, + entryComponents: [TestComponent], + }); + + beforeEach(() => (spectator = createComponent())); + + afterEach(() => componentRef.destroy()); + + describe('#container', () => { + it('should be a ViewContainerRef', () => { + let foo = document.querySelector('div.foo'); + expect(foo).toBeNull(); + + const cfResolver = spectator.inject(ComponentFactoryResolver); + const factory = cfResolver.resolveComponentFactory(TestComponent); + componentRef = spectator.component.container.createComponent(factory); + + foo = document.querySelector('div.foo'); + expect(foo.textContent).toBe('bar'); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.component.spec.ts new file mode 100644 index 0000000000..db6bcbc39c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.component.spec.ts @@ -0,0 +1,213 @@ +import { LocalizationPipe } from '@abp/ng.core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { Store } from '@ngxs/store'; +import { fromEvent, Subject, timer } from 'rxjs'; +import { delay, reduce, take } from 'rxjs/operators'; +import { ButtonComponent, ConfirmationComponent, ModalComponent } from '../components'; +import { Confirmation } from '../models'; +import { ConfirmationService } from '../services'; + +describe('ModalComponent', () => { + let spectator: SpectatorHost< + ModalComponent, + { visible: boolean; busy: boolean; ngDirty: boolean } + >; + let appearFn; + let disappearFn; + let mockConfirmation$: Subject; + const createHost = createHostFactory({ + component: ModalComponent, + imports: [RouterTestingModule, NgbModalModule], + declarations: [ConfirmationComponent, LocalizationPipe, ButtonComponent], + providers: [ + { + provide: ConfirmationService, + useValue: { + warn() { + mockConfirmation$ = new Subject(); + return mockConfirmation$; + }, + }, + }, + ], + mocks: [Store], + }); + + beforeEach(async () => { + appearFn = jest.fn(); + disappearFn = jest.fn(); + + spectator = createHost( + ` + +
+
+ + +
+
+ + + + +
+ `, + { + hostProps: { + visible: true, + busy: false, + ngDirty: false, + appearFn, + disappearFn, + }, + }, + ); + + await wait0ms(); + }); + + afterEach(() => { + const modalService = spectator.inject(NgbModal); + modalService.dismissAll(); + }); + + it('should open the ngb-modal with backdrop', () => { + const modal = selectModal(); + expect(modal).toBeTruthy(); + expect(document.querySelector('ngb-modal-backdrop')).toBeTruthy(); + }); + + it('should reflect its input properties to the template', () => { + const modal = selectModal('.test'); + expect(modal).toBeTruthy(); + expect(modal.querySelector('div.modal-sm')).toBeTruthy(); + expect(modal.querySelector('div.modal-dialog-centered')).toBeTruthy(); + }); + + it('should emit the appear output when made visible', () => { + expect(appearFn).toHaveBeenCalled(); + }); + + it('should emit the disappear output when made invisible', async () => { + spectator.hostComponent.visible = false; + spectator.detectChanges(); + + await wait0ms(); + + expect(disappearFn).toHaveBeenCalledTimes(1); + }); + + xit('should close with the abpClose', async () => { + await wait0ms(); + + spectator.dispatchMouseEvent(spectator.component.abpClose, 'click'); + + await wait0ms(); + + expect(disappearFn).toHaveBeenCalledTimes(1); + }); + + it('should open the confirmation popup and works correct', async () => { + const confirmationService = spectator.inject(ConfirmationService); + const warnSpy = jest.spyOn(confirmationService, 'warn'); + + await wait0ms(); + + spectator.hostComponent.ngDirty = true; + spectator.detectChanges(); + + expect(selectModal()).toBeTruthy(); + spectator.component.close(); // 1st try + + await wait0ms(); + + spectator.component.close(); // 2nd try + + await wait0ms(); + + expect(selectModal()).toBeTruthy(); + expect(warnSpy).toHaveBeenCalledTimes(1); + warnSpy.mockClear(); + + mockConfirmation$.next(Confirmation.Status.reject); + + await wait0ms(); + + expect(selectModal()).toBeTruthy(); + spectator.component.close(); + + await wait0ms(); + + expect(selectModal()).toBeTruthy(); + expect(warnSpy).toHaveBeenCalledTimes(1); + warnSpy.mockClear(); + + mockConfirmation$.next(Confirmation.Status.confirm); + await wait0ms(); + + // TODO: There is presumably a problem with change detection + // expect(selectModal()).toBeNull(); + expect(disappearFn).toHaveBeenCalledTimes(1); + }); + + it('should close with esc key', async () => { + await wait0ms(); + spectator.dispatchKeyboardEvent(spectator.component.modalWindowRef, 'keyup', 'Escape'); + + await wait300ms(); + + expect(spectator.component.visible).toBe(false); + }); + + it('should not close when busy is true', async () => { + spectator.hostComponent.busy = true; + spectator.detectChanges(); + + spectator.component.close(); + + await wait0ms(); + + expect(disappearFn).not.toHaveBeenCalled(); + }); + + xit('should not let window unload when form is dirty', async done => { + fromEvent(window, 'beforeunload') + .pipe( + take(2), + delay(0), + reduce((acc, v) => acc.concat(v), []), + ) + .subscribe(([event1, event2]) => { + expect(event1.returnValue).toBe(false); + expect(event2.returnValue).toBe(false); + done(); + }); + + spectator.hostComponent.ngDirty = true; + spectator.detectChanges(); + spectator.dispatchFakeEvent(window, 'beforeunload'); + + await wait0ms(); + + spectator.hostComponent.ngDirty = false; + spectator.detectChanges(); + spectator.dispatchFakeEvent(window, 'beforeunload'); + }); +}); + +function selectModal(modalSelector = ''): Element { + return document.querySelector(`ngb-modal-window.modal${modalSelector}`); +} + +async function wait0ms() { + await timer(0).toPromise(); +} + +async function wait300ms() { + await timer(300).toPromise(); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.service.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.service.spec.ts new file mode 100644 index 0000000000..0c383bc7cf --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/modal.service.spec.ts @@ -0,0 +1,87 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { ModalContainerComponent } from '../components/modal/modal-container.component'; +import { ModalService } from '../services'; + +describe('ModalContainerComponent', () => { + @Component({ + template: ` + +
bar
+
+ `, + }) + class TestComponent { + @ViewChild('ref', { static: true }) + template: TemplateRef; + + constructor(public modalService: ModalService) {} + } + + let spectator: Spectator; + let service: ModalService; + + const createComponent = createComponentFactory({ + component: TestComponent, + entryComponents: [ModalContainerComponent], + }); + + beforeEach(() => { + spectator = createComponent(); + service = spectator.component.modalService; + }); + + afterEach(() => { + service.getContainer().clear(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + service['containerComponentRef'].destroy(); + }); + + describe('#getContainer', () => { + it('should return the ViewContainerRef of ModalContainerComponent', () => { + let foo = document.querySelector('div.foo'); + expect(foo).toBeNull(); + + const containerRef = service.getContainer(); + const embeddedViewRef = containerRef.createEmbeddedView(spectator.component.template); + + foo = document.querySelector('div.foo'); + expect(foo).toBe(embeddedViewRef.rootNodes[0]); + expect(foo.textContent).toBe('bar'); + }); + }); + + describe('#renderTemplate', () => { + it('should render given template using the ViewContainerRef of ModalContainerComponent', () => { + let foo = document.querySelector('div.foo'); + expect(foo).toBeNull(); + + service.renderTemplate(spectator.component.template); + + foo = document.querySelector('div.foo'); + expect(foo.textContent).toBe('bar'); + }); + }); + + describe('#detectChanges', () => { + it('should call detectChanges on the containerComponentRef', () => { + const spy = jest.spyOn(service['containerComponentRef'].changeDetectorRef, 'detectChanges'); + + service.detectChanges(); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('#clearModal', () => { + it('should call clear on the ViewContainerRef and detectChanges', () => { + const clear = jest.spyOn(service.getContainer(), 'clear'); + const detectChanges = jest.spyOn(service, 'detectChanges'); + + service.clearModal(); + + expect(clear).toHaveBeenCalledTimes(1); + expect(detectChanges).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts new file mode 100644 index 0000000000..a91f5b76c1 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts @@ -0,0 +1,46 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { SortOrderIconComponent } from '../components/sort-order-icon/sort-order-icon.component'; + +describe('SortOrderIconComponent', () => { + let spectator: SpectatorHost; + let component: SortOrderIconComponent; + const createHost = createHostFactory(SortOrderIconComponent); + + beforeEach(() => { + spectator = createHost( + '', + { + hostProps: { + selectedSortKey: '', + order: '', + }, + }, + ); + component = spectator.component; + }); + + test('should have correct icon class when selectedSortKey and sortKey are the same', () => { + const newKey = 'testKey'; + component.sort(newKey); + expect(component.selectedSortKey).toBe(newKey); + expect(component.order).toBe('asc'); + expect(component.icon).toBe('sorting_asc'); + }); + + test("shouldn't have any icon class when sortKey and selectedSortKey are different", () => { + const newKey = 'otherKey'; + component.sort(newKey); + expect(component.selectedSortKey).toBe(newKey); + expect(component.order).toBe('asc'); + expect(component.icon).toBe('sorting'); + }); + + test('should change order correctly when sort function called', () => { + component.sort('testKey'); + expect(component.order).toBe('asc'); + component.sort('testKey'); + expect(component.order).toBe('desc'); + component.sort('testKey'); + expect(component.order).toBe(''); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table-sort.directive.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table-sort.directive.spec.ts new file mode 100644 index 0000000000..ad43a11b87 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table-sort.directive.spec.ts @@ -0,0 +1,32 @@ +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { TableSortDirective } from '../directives/table-sort.directive'; +import { TableComponent } from '../components/table/table.component'; +import { DummyLocalizationPipe } from './table.component.spec'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; + +describe('TableSortDirective', () => { + let spectator: SpectatorDirective; + let directive: TableSortDirective; + const createDirective = createDirectiveFactory({ + directive: TableSortDirective, + declarations: [TableComponent, DummyLocalizationPipe], + imports: [NgbPaginationModule], + }); + + beforeEach(() => { + spectator = createDirective( + ``, + ); + directive = spectator.directive; + }); + + test('should be created', () => { + expect(directive).toBeTruthy(); + }); + + test('should change table value', () => { + expect(directive.value).toEqual([1, 4, 2]); + const table = spectator.query(TableComponent); + expect(table.value).toEqual([1, 2, 4]); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table.component.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table.component.spec.ts new file mode 100644 index 0000000000..d456cebd34 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/table.component.spec.ts @@ -0,0 +1,75 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { TableComponent } from '../components'; + +@Pipe({ + name: 'abpLocalization', +}) +export class DummyLocalizationPipe implements PipeTransform { + transform(value: any, ...args: any[]): any { + return value; + } +} + +describe('TableComponent', () => { + let spectator: SpectatorHost; + const createHost = createHostFactory({ + component: TableComponent, + declarations: [DummyLocalizationPipe], + imports: [NgbPaginationModule], + }); + + describe('without value', () => { + beforeEach(() => { + spectator = createHost( + ` + + + name`, + { + hostProps: { + value: [], + }, + }, + ); + }); + + it('should display the empty message', () => { + expect(spectator.query('caption.ui-table-empty')).toHaveText( + 'AbpAccount::NoDataAvailableInDatatable', + ); + }); + + it('should display the header', () => { + expect(spectator.query('thead')).toBeTruthy(); + expect(spectator.query('th')).toHaveText('name'); + }); + + it('should place the colgroup template', () => { + expect(spectator.query('colgroup')).toBeTruthy(); + expect(spectator.query('col')).toBeTruthy(); + }); + }); + + describe('with value', () => { + // TODO + beforeEach(() => { + spectator = createHost( + ` + name + `, + { + hostProps: { + value: [], + }, + }, + ); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/toaster.service.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/toaster.service.spec.ts new file mode 100644 index 0000000000..ab885726de --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/toaster.service.spec.ts @@ -0,0 +1,122 @@ +import { CoreTestingModule } from '@abp/ng.core/testing'; +import { NgModule } from '@angular/core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { NgxsModule } from '@ngxs/store'; +import { timer } from 'rxjs'; +import { ToastContainerComponent } from '../components/toast-container/toast-container.component'; +import { ToastComponent } from '../components/toast/toast.component'; +import { ToasterService } from '../services/toaster.service'; + +@NgModule({ + exports: [ToastContainerComponent], + entryComponents: [ToastContainerComponent], + declarations: [ToastContainerComponent, ToastComponent], + imports: [CoreTestingModule.withConfig()], +}) +export class MockModule {} +const toastClassPrefix = 'abp-toast'; + +describe('ToasterService', () => { + let spectator: SpectatorService; + let service: ToasterService; + const createService = createServiceFactory({ + service: ToasterService, + imports: [NgxsModule.forRoot(), CoreTestingModule.withConfig(), MockModule], + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + }); + + afterEach(() => { + clearElements(); + }); + + test('should display a toast', async () => { + service.show('MESSAGE', 'TITLE'); + + await timer(0).toPromise(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + + expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy(); + expect(selectToasterContent(`.${toastClassPrefix}-title`)).toBe('TITLE'); + expect(selectToasterContent(`.${toastClassPrefix}-message`)).toBe('MESSAGE'); + }); + + test.each` + type | selector | icon + ${'info'} | ${`.${toastClassPrefix}-info`} | ${'.fa-info-circle'} + ${'success'} | ${`.${toastClassPrefix}-success`} | ${'.fa-check-circle'} + ${'warn'} | ${`.${toastClassPrefix}-warning`} | ${'.fa-exclamation-triangle'} + ${'error'} | ${`.${toastClassPrefix}-error`} | ${'.fa-times-circle'} + `('should display $type toast', async ({ type, selector, icon }) => { + service[type]('MESSAGE', 'TITLE'); + + await timer(0).toPromise(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + expect(selectToasterContent(`.${toastClassPrefix}-title`)).toBe('TITLE'); + expect(selectToasterContent(`.${toastClassPrefix}-message`)).toBe('MESSAGE'); + expect(selectToasterElement()).toBe(document.querySelector(selector)); + expect(selectToasterElement(icon)).toBeTruthy(); + }); + + test('should display multiple toasts', async () => { + service.show('MESSAGE_1', 'TITLE_1'); + service.show('MESSAGE_2', 'TITLE_2'); + + await timer(0).toPromise(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + + const titles = document.querySelectorAll(`.${toastClassPrefix}-title`); + expect(titles.length).toBe(2); + + const messages = document.querySelectorAll(`.${toastClassPrefix}-message`); + expect(messages.length).toBe(2); + }); + + test('should remove a toast when remove is called', async () => { + service.show('MESSAGE'); + service.remove(0); + + await timer(0).toPromise(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + + expect(selectToasterElement()).toBeNull(); + }); + + test('should remove toasts when clear is called', async () => { + service.show('MESSAGE'); + service.clear(); + + await timer(0).toPromise(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + + expect(selectToasterElement()).toBeNull(); + }); + + test('should remove toasts based on containerKey when clear is called with key', async () => { + service.show('MESSAGE_1', 'TITLE_1', 'neutral', { containerKey: 'x' }); + service.show('MESSAGE_2', 'TITLE_2', 'neutral', { containerKey: 'y' }); + service.clear('x'); + + await timer(0).toPromise(); + service['containerComponentRef'].changeDetectorRef.detectChanges(); + + expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy(); + expect(selectToasterContent(`.${toastClassPrefix}-title`)).toBe('TITLE_2'); + expect(selectToasterContent(`.${toastClassPrefix}-message`)).toBe('MESSAGE_2'); + }); +}); + +function clearElements(selector = `.${toastClassPrefix}`) { + document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element)); +} + +function selectToasterContent(selector = `.${toastClassPrefix}`): string { + return selectToasterElement(selector).textContent.trim(); +} + +function selectToasterElement(selector = `.${toastClassPrefix}`): T { + return document.querySelector(selector); +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/validation-utils.spec.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/validation-utils.spec.ts new file mode 100644 index 0000000000..19353dae28 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tests/validation-utils.spec.ts @@ -0,0 +1,52 @@ +import { ConfigState, ConfigStateService } from '@abp/ng.core'; +import { Component, Injector } from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator'; +import { NgxValidateCoreModule, validatePassword } from '@ngx-validate/core'; +import { NgxsModule, Store } from '@ngxs/store'; +import { HttpClient } from '@angular/common/http'; +import { getPasswordValidators } from '../utils'; +import { Validators } from '@angular/forms'; +import { OAuthService } from 'angular-oauth2-oidc'; + +@Component({ template: '', selector: 'abp-dummy' }) +class DummyComponent {} + +describe('ValidationUtils', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: DummyComponent, + imports: [NgxValidateCoreModule.forRoot()], + mocks: [HttpClient, OAuthService], + }); + + beforeEach(() => (spectator = createComponent())); + + describe('#getPasswordValidators', () => { + it('should return password valdiators', () => { + const configState = spectator.inject(ConfigStateService); + configState.setState({ + setting: { + values: { + 'Abp.Identity.Password.RequiredLength': '6', + 'Abp.Identity.Password.RequiredUniqueChars': '1', + 'Abp.Identity.Password.RequireNonAlphanumeric': 'True', + 'Abp.Identity.Password.RequireLowercase': 'True', + 'Abp.Identity.Password.RequireUppercase': 'True', + 'Abp.Identity.Password.RequireDigit': 'True', + }, + }, + }); + + const validators = getPasswordValidators(spectator.inject(Injector)); + const expectedValidators = [ + validatePassword(['number', 'small', 'capital', 'special']), + Validators.minLength(6), + Validators.maxLength(128), + ]; + + validators.forEach((validator, index) => { + expect(validator.toString()).toBe(expectedValidators[index].toString()); + }); + }); + }); +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/theme-shared.module.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/theme-shared.module.ts new file mode 100644 index 0000000000..11a2ce3493 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/theme-shared.module.ts @@ -0,0 +1,140 @@ +import { CoreModule, noop } from '@abp/ng.core'; +import { DatePipe } from '@angular/common'; +import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core'; +import { NgbDateParserFormatter, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { + defaultMapErrorsFn, + NgxValidateCoreModule, + VALIDATION_BLUEPRINTS, + VALIDATION_MAP_ERRORS_FN, + VALIDATION_VALIDATE_ON_SUBMIT, +} from '@ngx-validate/core'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component'; +import { ButtonComponent } from './components/button/button.component'; +import { ChartComponent } from './components/chart/chart.component'; +import { ConfirmationComponent } from './components/confirmation/confirmation.component'; +import { HttpErrorWrapperComponent } from './components/http-error-wrapper/http-error-wrapper.component'; +import { LoaderBarComponent } from './components/loader-bar/loader-bar.component'; +import { LoadingComponent } from './components/loading/loading.component'; +import { ModalCloseDirective } from './components/modal/modal-close.directive'; +import { ModalContainerComponent } from './components/modal/modal-container.component'; +import { ModalComponent } from './components/modal/modal.component'; +import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component'; +import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component'; +import { TableComponent } from './components/table/table.component'; +import { ToastContainerComponent } from './components/toast-container/toast-container.component'; +import { ToastComponent } from './components/toast/toast.component'; +import { DEFAULT_VALIDATION_BLUEPRINTS } from './constants/validation'; +import { EllipsisModule } from './directives/ellipsis.directive'; +import { LoadingDirective } from './directives/loading.directive'; +import { NgxDatatableDefaultDirective } from './directives/ngx-datatable-default.directive'; +import { NgxDatatableListDirective } from './directives/ngx-datatable-list.directive'; +import { TableSortDirective } from './directives/table-sort.directive'; +import { ErrorHandler } from './handlers/error.handler'; +import { initLazyStyleHandler } from './handlers/lazy-style.handler'; +import { RootParams } from './models/common'; +import { NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers'; +import { THEME_SHARED_ROUTE_PROVIDERS } from './providers/route.provider'; +import { THEME_SHARED_APPEND_CONTENT } from './tokens/append-content.token'; +import { httpErrorConfigFactory, HTTP_ERROR_CONFIG } from './tokens/http-error.token'; +import { DateParserFormatter } from './utils/date-parser-formatter'; + +const declarationsWithExports = [ + BreadcrumbComponent, + ButtonComponent, + ChartComponent, + ConfirmationComponent, + LoaderBarComponent, + LoadingComponent, + ModalComponent, + TableComponent, + TableEmptyMessageComponent, + ToastComponent, + ToastContainerComponent, + SortOrderIconComponent, + NgxDatatableDefaultDirective, + NgxDatatableListDirective, + LoadingDirective, + TableSortDirective, + ModalCloseDirective, +]; + +@NgModule({ + imports: [ + CoreModule, + NgxDatatableModule, + NgxValidateCoreModule, + NgbPaginationModule, + EllipsisModule, + ], + declarations: [...declarationsWithExports, HttpErrorWrapperComponent, ModalContainerComponent], + exports: [NgxDatatableModule, EllipsisModule, ...declarationsWithExports], + providers: [DatePipe], + entryComponents: [ + HttpErrorWrapperComponent, + LoadingComponent, + ModalContainerComponent, + ToastContainerComponent, + ConfirmationComponent, + ], +}) +export class BaseThemeSharedModule {} + +@NgModule({ + imports: [BaseThemeSharedModule], + exports: [BaseThemeSharedModule], +}) +export class ThemeSharedModule { + static forRoot( + { httpErrorConfig, validation = {} } = {} as RootParams, + ): ModuleWithProviders { + return { + ngModule: ThemeSharedModule, + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [ErrorHandler], + useFactory: noop, + }, + THEME_SHARED_ROUTE_PROVIDERS, + { + provide: APP_INITIALIZER, + multi: true, + deps: [THEME_SHARED_APPEND_CONTENT], + useFactory: noop, + }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [Injector], + useFactory: initLazyStyleHandler, + }, + { provide: HTTP_ERROR_CONFIG, useValue: httpErrorConfig }, + { + provide: 'HTTP_ERROR_CONFIG', + useFactory: httpErrorConfigFactory, + deps: [HTTP_ERROR_CONFIG], + }, + { provide: NgbDateParserFormatter, useClass: DateParserFormatter }, + NG_BOOTSTRAP_CONFIG_PROVIDERS, + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, + ...(validation.blueprints || {}), + }, + }, + { + provide: VALIDATION_MAP_ERRORS_FN, + useValue: validation.mapErrorsFn || defaultMapErrorsFn, + }, + { + provide: VALIDATION_VALIDATE_ON_SUBMIT, + useValue: validation.validateOnSubmit, + }, + ], + }; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/append-content.token.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/append-content.token.ts new file mode 100644 index 0000000000..2ba7898480 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/append-content.token.ts @@ -0,0 +1,15 @@ +import { CONTENT_STRATEGY, DomInsertionService } from '@abp/ng.core'; +import { inject, InjectionToken } from '@angular/core'; +import styles from '../constants/styles'; +import { chartJsLoaded$ } from '../utils/widget-utils'; + +export const THEME_SHARED_APPEND_CONTENT = new InjectionToken('THEME_SHARED_APPEND_CONTENT', { + providedIn: 'root', + factory: () => { + const domInsertion: DomInsertionService = inject(DomInsertionService); + + domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles)); + + import('chart.js').then(() => chartJsLoaded$.next(true)); + }, +}); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/http-error.token.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/http-error.token.ts new file mode 100644 index 0000000000..39cb43a350 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/http-error.token.ts @@ -0,0 +1,18 @@ +import { InjectionToken } from '@angular/core'; +import { HttpErrorConfig, HttpErrorHandler } from '../models/common'; + +export function httpErrorConfigFactory(config = {} as HttpErrorConfig) { + if (config.errorScreen && config.errorScreen.component && !config.errorScreen.forWhichErrors) { + config.errorScreen.forWhichErrors = [401, 403, 404, 500]; + } + + return { + skipHandledErrorCodes: [], + errorScreen: {}, + ...config, + } as HttpErrorConfig; +} + +export const HTTP_ERROR_CONFIG = new InjectionToken('HTTP_ERROR_CONFIG'); + +export const HTTP_ERROR_HANDLER = new InjectionToken('HTTP_ERROR_HANDLER'); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/index.ts new file mode 100644 index 0000000000..148c34084c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/index.ts @@ -0,0 +1,5 @@ +export * from './append-content.token'; +export * from './http-error.token'; +export * from './lazy-styles.token'; +export * from './ngx-datatable-messages.token'; +export * from './suppress-unsaved-changes-warning.token'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/lazy-styles.token.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/lazy-styles.token.ts new file mode 100644 index 0000000000..73ca3044fc --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/lazy-styles.token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const LAZY_STYLES = new InjectionToken('LAZY_STYLES'); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/ngx-datatable-messages.token.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/ngx-datatable-messages.token.ts new file mode 100644 index 0000000000..bf1a73ab09 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/ngx-datatable-messages.token.ts @@ -0,0 +1,17 @@ +import { InjectionToken } from '@angular/core'; + +export interface NgxDatatableMessages { + emptyMessage: string; + totalMessage: string; + selectedMessage: string; +} + +export const defaultNgxDatatableMessages = { + emptyMessage: 'AbpUi::NoDataAvailableInDatatable', + totalMessage: 'AbpUi::Total', + selectedMessage: 'AbpUi::Selected', +}; + +export const NGX_DATATABLE_MESSAGES = new InjectionToken>( + 'NGX_DATATABLE_MESSAGES', +); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts new file mode 100644 index 0000000000..af68c8130c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core'; + +// TODO: Should be documented +export const SUPPRESS_UNSAVED_CHANGES_WARNING = new InjectionToken( + 'SUPPRESS_UNSAVED_CHANGES_WARNING', +); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/date-parser-formatter.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/date-parser-formatter.ts new file mode 100644 index 0000000000..cf8565435a --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/date-parser-formatter.ts @@ -0,0 +1,56 @@ +import { ApplicationLocalizationConfigurationDto, ConfigStateService } from '@abp/ng.core'; +import { formatDate } from '@angular/common'; +import { Inject, Injectable, LOCALE_ID } from '@angular/core'; +import { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import snq from 'snq'; + +function isNumber(value: any): boolean { + return !isNaN(toInteger(value)); +} + +function toInteger(value: any): number { + return parseInt(`${value}`, 10); +} + +@Injectable() +export class DateParserFormatter extends NgbDateParserFormatter { + constructor(private configState: ConfigStateService, @Inject(LOCALE_ID) private locale: string) { + super(); + } + + parse(value: string): NgbDateStruct { + if (value) { + const dateParts = value.trim().split('-'); + if (dateParts.length === 1 && isNumber(dateParts[0])) { + return { year: toInteger(dateParts[0]), month: null, day: null }; + } else if (dateParts.length === 2 && isNumber(dateParts[0]) && isNumber(dateParts[1])) { + return { year: toInteger(dateParts[0]), month: toInteger(dateParts[1]), day: null }; + } else if ( + dateParts.length === 3 && + isNumber(dateParts[0]) && + isNumber(dateParts[1]) && + isNumber(dateParts[2]) + ) { + return { + year: toInteger(dateParts[0]), + month: toInteger(dateParts[1]), + day: toInteger(dateParts[2]), + }; + } + } + return null; + } + + format(date: NgbDateStruct): string { + if (!date) return ''; + + const localization: ApplicationLocalizationConfigurationDto = this.configState.getOne( + 'localization', + ); + + const dateFormat = + snq(() => localization.currentCulture.dateTimeFormat.shortDatePattern) || 'yyyy-MM-dd'; + + return formatDate(new Date(date.year, date.month - 1, date.day), dateFormat, this.locale); + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/index.ts new file mode 100644 index 0000000000..9f290b5ca3 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/index.ts @@ -0,0 +1,3 @@ +export * from './date-parser-formatter'; +export * from './validation-utils'; +export * from './widget-utils'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/validation-utils.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/validation-utils.ts new file mode 100644 index 0000000000..a9ec1d4270 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/validation-utils.ts @@ -0,0 +1,45 @@ +import { ABP, ConfigStateService } from '@abp/ng.core'; +import { Injector } from '@angular/core'; +import { ValidatorFn, Validators } from '@angular/forms'; +import { PasswordRules, validatePassword } from '@ngx-validate/core'; + +const { minLength, maxLength } = Validators; + +export function getPasswordValidators(injector: Injector): ValidatorFn[] { + const getRule = getRuleFn(injector); + + const passwordRulesArr = [] as PasswordRules; + let requiredLength = 1; + + if (getRule('RequireDigit') === 'true') { + passwordRulesArr.push('number'); + } + + if (getRule('RequireLowercase') === 'true') { + passwordRulesArr.push('small'); + } + + if (getRule('RequireUppercase') === 'true') { + passwordRulesArr.push('capital'); + } + + if (getRule('RequireNonAlphanumeric') === 'true') { + passwordRulesArr.push('special'); + } + + if (Number.isInteger(+getRule('RequiredLength'))) { + requiredLength = +getRule('RequiredLength'); + } + + return [validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(128)]; +} + +function getRuleFn(injector: Injector) { + const configState = injector.get(ConfigStateService); + + return (key: string) => { + const passwordRules: ABP.Dictionary = configState.getSettings('Identity.Password'); + + return (passwordRules[`Abp.Identity.Password.${key}`] || '').toLowerCase(); + }; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/widget-utils.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/widget-utils.ts new file mode 100644 index 0000000000..dfe81c973c --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/lib/utils/widget-utils.ts @@ -0,0 +1,16 @@ +import { ReplaySubject } from 'rxjs'; + +export function getRandomBackgroundColor(count) { + const colors = []; + + for (let i = 0; i < count; i++) { + const r = ((i + 5) * (i + 5) * 474) % 255; + const g = ((i + 5) * (i + 5) * 1600) % 255; + const b = ((i + 5) * (i + 5) * 84065) % 255; + colors.push('rgba(' + r + ', ' + g + ', ' + b + ', 0.7)'); + } + + return colors; +} + +export const chartJsLoaded$ = new ReplaySubject(1); diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/public-api.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/public-api.ts new file mode 100644 index 0000000000..845655402d --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/public-api.ts @@ -0,0 +1,17 @@ +/* + * Public API Surface of theme-shared + */ + +export * from './lib/animations'; +export * from './lib/components'; +export { BOOTSTRAP } from './lib/constants/styles'; +export * from './lib/constants/validation'; +export * from './lib/directives'; +export * from './lib/enums'; +export * from './lib/handlers'; +export * from './lib/models'; +export * from './lib/providers'; +export * from './lib/services'; +export * from './lib/theme-shared.module'; +export * from './lib/tokens'; +export * from './lib/utils'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/test-setup.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/test-setup.ts new file mode 100644 index 0000000000..1100b3e8a6 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular/setup-jest'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/styles/bootstrap-rtl.min.css b/npm/ng-packs/nx/ng-packs/libs/theme-shared/styles/bootstrap-rtl.min.css new file mode 100644 index 0000000000..710ef6b9e3 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/styles/bootstrap-rtl.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.5.2 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::after,*::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0, 0, 0, 0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:0.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}[type="button"],[type="reset"],[type="submit"],button{-webkit-appearance:button}[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled),button:not(:disabled){cursor:pointer}[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:0.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:0.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0, 0, 0, 0.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a > code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container,.container-sm{max-width:540px}}@media (min-width: 768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width: 992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width: 1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters > .col,.no-gutters > [class*="col-"]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-1 > *{flex:0 0 100%;max-width:100%}.row-cols-2 > *{flex:0 0 50%;max-width:50%}.row-cols-3 > *{flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4 > *{flex:0 0 25%;max-width:25%}.row-cols-5 > *{flex:0 0 20%;max-width:20%}.row-cols-6 > *{flex:0 0 16.666667%;max-width:16.666667%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.333333%;max-width:8.333333%}.col-2{flex:0 0 16.666667%;max-width:16.666667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.333333%;max-width:33.333333%}.col-5{flex:0 0 41.666667%;max-width:41.666667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.333333%;max-width:58.333333%}.col-8{flex:0 0 66.666667%;max-width:66.666667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.333333%;max-width:83.333333%}.col-11{flex:0 0 91.666667%;max-width:91.666667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width: 576px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-sm-1 > *{flex:0 0 100%;max-width:100%}.row-cols-sm-2 > *{flex:0 0 50%;max-width:50%}.row-cols-sm-3 > *{flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4 > *{flex:0 0 25%;max-width:25%}.row-cols-sm-5 > *{flex:0 0 20%;max-width:20%}.row-cols-sm-6 > *{flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width: 768px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-md-1 > *{flex:0 0 100%;max-width:100%}.row-cols-md-2 > *{flex:0 0 50%;max-width:50%}.row-cols-md-3 > *{flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4 > *{flex:0 0 25%;max-width:25%}.row-cols-md-5 > *{flex:0 0 20%;max-width:20%}.row-cols-md-6 > *{flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width: 992px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-lg-1 > *{flex:0 0 100%;max-width:100%}.row-cols-lg-2 > *{flex:0 0 50%;max-width:50%}.row-cols-lg-3 > *{flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4 > *{flex:0 0 25%;max-width:25%}.row-cols-lg-5 > *{flex:0 0 20%;max-width:20%}.row-cols-lg-6 > *{flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width: 1200px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.row-cols-xl-1 > *{flex:0 0 100%;max-width:100%}.row-cols-xl-2 > *{flex:0 0 50%;max-width:50%}.row-cols-xl-3 > *{flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4 > *{flex:0 0 25%;max-width:25%}.row-cols-xl-5 > *{flex:0 0 20%;max-width:20%}.row-cols-xl-6 > *{flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:0.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody + tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:0.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody + tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0, 0, 0, 0.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0, 0, 0, 0.075)}.table-primary,.table-primary > td,.table-primary > th{background-color:#b8daff}.table-primary tbody + tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover > td,.table-hover .table-primary:hover > th{background-color:#9fcdff}.table-secondary,.table-secondary > td,.table-secondary > th{background-color:#d6d8db}.table-secondary tbody + tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover > td,.table-hover .table-secondary:hover > th{background-color:#c8cbcf}.table-success,.table-success > td,.table-success > th{background-color:#c3e6cb}.table-success tbody + tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover > td,.table-hover .table-success:hover > th{background-color:#b1dfbb}.table-info,.table-info > td,.table-info > th{background-color:#bee5eb}.table-info tbody + tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover > td,.table-hover .table-info:hover > th{background-color:#abdde5}.table-warning,.table-warning > td,.table-warning > th{background-color:#ffeeba}.table-warning tbody + tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover > td,.table-hover .table-warning:hover > th{background-color:#ffe8a1}.table-danger,.table-danger > td,.table-danger > th{background-color:#f5c6cb}.table-danger tbody + tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover > td,.table-hover .table-danger:hover > th{background-color:#f1b0b7}.table-light,.table-light > td,.table-light > th{background-color:#fdfdfe}.table-light tbody + tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover > td,.table-hover .table-light:hover > th{background-color:#ececf6}.table-dark,.table-dark > td,.table-dark > th{background-color:#c6c8ca}.table-dark tbody + tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover > td,.table-hover .table-dark:hover > th{background-color:#b9bbbe}.table-active,.table-active > td,.table-active > th{background-color:rgba(0, 0, 0, 0.075)}.table-hover .table-active:hover{background-color:rgba(0, 0, 0, 0.075)}.table-hover .table-active:hover > td,.table-hover .table-active:hover > th{background-color:rgba(0, 0, 0, 0.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255, 255, 255, 0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255, 255, 255, 0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm > .table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md > .table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg > .table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl > .table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive > .table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:0.25rem;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:0.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + 0.5rem + 2px);padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5;border-radius:0.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:0.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:0.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row > .col,.form-row > [class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input:disabled ~ .form-check-label,.form-check-input[disabled] ~ .form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:0.1rem;font-size:0.875rem;line-height:1.5;color:#fff;background-color:rgba(40, 167, 69, 0.9);border-radius:0.25rem}.form-row > .col > .valid-tooltip,.form-row > [class*="col-"] > .valid-tooltip{left:5px}.is-valid ~ .valid-feedback,.is-valid ~ .valid-tooltip,.was-validated:valid ~ .valid-feedback,.was-validated:valid ~ .valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat, #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.25)}.form-check-input.is-valid ~ .form-check-label,.was-validated .form-check-input:valid ~ .form-check-label{color:#28a745}.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip,.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip{display:block}.custom-control-input.is-valid ~ .custom-control-label,.was-validated .custom-control-input:valid ~ .custom-control-label{color:#28a745}.custom-control-input.is-valid ~ .custom-control-label::before,.was-validated .custom-control-input:valid ~ .custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked ~ .custom-control-label::before,.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus ~ .custom-control-label::before,.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before{box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.25)}.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid ~ .custom-file-label,.was-validated .custom-file-input:valid ~ .custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus ~ .custom-file-label,.was-validated .custom-file-input:valid:focus ~ .custom-file-label{border-color:#28a745;box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:0.1rem;font-size:0.875rem;line-height:1.5;color:#fff;background-color:rgba(220, 53, 69, 0.9);border-radius:0.25rem}.form-row > .col > .invalid-tooltip,.form-row > [class*="col-"] > .invalid-tooltip{left:5px}.is-invalid ~ .invalid-feedback,.is-invalid ~ .invalid-tooltip,.was-validated:invalid ~ .invalid-feedback,.was-validated:invalid ~ .invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat, #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.25)}.form-check-input.is-invalid ~ .form-check-label,.was-validated .form-check-input:invalid ~ .form-check-label{color:#dc3545}.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip,.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip{display:block}.custom-control-input.is-invalid ~ .custom-control-label,.was-validated .custom-control-input:invalid ~ .custom-control-label{color:#dc3545}.custom-control-input.is-invalid ~ .custom-control-label::before,.was-validated .custom-control-input:invalid ~ .custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked ~ .custom-control-label::before,.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus ~ .custom-control-label::before,.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before{box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.25)}.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid ~ .custom-file-label,.was-validated .custom-file-input:invalid ~ .custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus ~ .custom-file-label,.was-validated .custom-file-input:invalid:focus ~ .custom-file-label{border-color:#dc3545;box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:1rem;line-height:1.5;border-radius:0.25rem;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.btn.disabled,.btn:disabled{opacity:0.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 0.2rem rgba(38, 143, 255, 0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show > .btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show > .btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(38, 143, 255, 0.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 0.2rem rgba(130, 138, 145, 0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show > .btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show > .btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(130, 138, 145, 0.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 0.2rem rgba(72, 180, 97, 0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show > .btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show > .btn-success.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(72, 180, 97, 0.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 0.2rem rgba(58, 176, 195, 0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show > .btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show > .btn-info.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(58, 176, 195, 0.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 0.2rem rgba(222, 170, 12, 0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show > .btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show > .btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(222, 170, 12, 0.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 0.2rem rgba(225, 83, 97, 0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show > .btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show > .btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(225, 83, 97, 0.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 0.2rem rgba(216, 217, 219, 0.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show > .btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show > .btn-light.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(216, 217, 219, 0.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 0.2rem rgba(82, 88, 93, 0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show > .btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show > .btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(82, 88, 93, 0.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show > .btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 0.2rem rgba(108, 117, 125, 0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show > .btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(108, 117, 125, 0.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show > .btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 0.2rem rgba(23, 162, 184, 0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show > .btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(23, 162, 184, 0.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 0.2rem rgba(255, 193, 7, 0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show > .btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(255, 193, 7, 0.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show > .btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 0.2rem rgba(248, 249, 250, 0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show > .btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(248, 249, 250, 0.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 0.2rem rgba(52, 58, 64, 0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show > .btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show > .btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 0.2rem rgba(52, 58, 64, 0.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg > .btn,.btn-lg{padding:0.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:0.3rem}.btn-group-sm > .btn,.btn-sm{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block + .btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0, 0, 0, 0.15);border-radius:0.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group > .btn,.btn-group-vertical > .btn{position:relative;flex:1 1 auto}.btn-group > .btn:hover,.btn-group-vertical > .btn:hover{z-index:1}.btn-group > .btn.active,.btn-group > .btn:active,.btn-group > .btn:focus,.btn-group-vertical > .btn.active,.btn-group-vertical > .btn:active,.btn-group-vertical > .btn:focus{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group > .btn-group:not(:first-child),.btn-group > .btn:not(:first-child){margin-left:-1px}.btn-group > .btn-group:not(:last-child) > .btn,.btn-group > .btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group > .btn-group:not(:first-child) > .btn,.btn-group > .btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm > .btn + .dropdown-toggle-split,.btn-sm + .dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-group-lg > .btn + .dropdown-toggle-split,.btn-lg + .dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical > .btn,.btn-group-vertical > .btn-group{width:100%}.btn-group-vertical > .btn-group:not(:first-child),.btn-group-vertical > .btn:not(:first-child){margin-top:-1px}.btn-group-vertical > .btn-group:not(:last-child) > .btn,.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical > .btn-group:not(:first-child) > .btn,.btn-group-vertical > .btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle > .btn,.btn-group-toggle > .btn-group > .btn{margin-bottom:0}.btn-group-toggle > .btn input[type="radio"],.btn-group-toggle > .btn input[type="checkbox"],.btn-group-toggle > .btn-group > .btn input[type="radio"],.btn-group-toggle > .btn-group > .btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group > .custom-file,.input-group > .custom-select,.input-group > .form-control,.input-group > .form-control-plaintext{position:relative;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group > .custom-file + .custom-file,.input-group > .custom-file + .custom-select,.input-group > .custom-file + .form-control,.input-group > .custom-select + .custom-file,.input-group > .custom-select + .custom-select,.input-group > .custom-select + .form-control,.input-group > .form-control + .custom-file,.input-group > .form-control + .custom-select,.input-group > .form-control + .form-control,.input-group > .form-control-plaintext + .custom-file,.input-group > .form-control-plaintext + .custom-select,.input-group > .form-control-plaintext + .form-control{margin-left:-1px}.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label,.input-group > .custom-select:focus,.input-group > .form-control:focus{z-index:3}.input-group > .custom-file .custom-file-input:focus{z-index:4}.input-group > .custom-select:not(:first-child),.input-group > .form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group > .custom-file{display:flex;align-items:center}.input-group > .custom-file:not(:first-child) .custom-file-label,.input-group > .custom-file:not(:last-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation) > .custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation) > .custom-select:not(:last-child),.input-group:not(.has-validation) > .form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation > .custom-file:nth-last-child(n + 3) .custom-file-label::after,.input-group.has-validation > .custom-select:nth-last-child(n + 3),.input-group.has-validation > .form-control:nth-last-child(n + 3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn + .btn,.input-group-append .btn + .input-group-text,.input-group-append .input-group-text + .btn,.input-group-append .input-group-text + .input-group-text,.input-group-prepend .btn + .btn,.input-group-prepend .btn + .input-group-text,.input-group-prepend .input-group-text + .btn,.input-group-prepend .input-group-text + .input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg > .custom-select,.input-group-lg > .form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg > .custom-select,.input-group-lg > .form-control,.input-group-lg > .input-group-append > .btn,.input-group-lg > .input-group-append > .input-group-text,.input-group-lg > .input-group-prepend > .btn,.input-group-lg > .input-group-prepend > .input-group-text{padding:0.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:0.3rem}.input-group-sm > .custom-select,.input-group-sm > .form-control:not(textarea){height:calc(1.5em + 0.5rem + 2px)}.input-group-sm > .custom-select,.input-group-sm > .form-control,.input-group-sm > .input-group-append > .btn,.input-group-sm > .input-group-append > .input-group-text,.input-group-sm > .input-group-prepend > .btn,.input-group-sm > .input-group-prepend > .input-group-text{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5;border-radius:0.2rem}.input-group-lg > .custom-select,.input-group-sm > .custom-select{padding-right:1.75rem}.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),.input-group > .input-group-append:last-child > .input-group-text:not(:last-child),.input-group > .input-group-prepend > .btn,.input-group > .input-group-prepend > .input-group-text,.input-group.has-validation > .input-group-append:nth-last-child(n + 3) > .btn,.input-group.has-validation > .input-group-append:nth-last-child(n + 3) > .input-group-text,.input-group:not(.has-validation) > .input-group-append:not(:last-child) > .btn,.input-group:not(.has-validation) > .input-group-append:not(:last-child) > .input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group > .input-group-append > .btn,.input-group > .input-group-append > .input-group-text,.input-group > .input-group-prepend:first-child > .btn:not(:first-child),.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child),.input-group > .input-group-prepend:not(:first-child) > .btn,.input-group > .input-group-prepend:not(:first-child) > .input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus ~ .custom-control-label::before{box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled ~ .custom-control-label,.custom-control-input[disabled] ~ .custom-control-label{color:#6c757d}.custom-control-input:disabled ~ .custom-control-label::before,.custom-control-input[disabled] ~ .custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:0.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:0.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50% / 50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(0, 123, 255, 0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(0, 123, 255, 0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(0, 123, 255, 0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:0.5rem}.custom-switch .custom-control-label::after{top:calc(0.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:0.5rem;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;transform:translateX(0.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(0, 123, 255, 0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right 0.75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + 0.5rem + 2px);padding-top:0.25rem;padding-bottom:0.25rem;padding-left:0.5rem;font-size:0.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:0.5rem;padding-bottom:0.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + 0.75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#80bdff;box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.custom-file-input:disabled ~ .custom-file-label,.custom-file-input[disabled] ~ .custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + 0.75rem);padding:0.375rem 0.75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0.2rem;margin-left:0.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show > .nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill > .nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified > .nav-link{flex-basis:0;flex-grow:1;text-align:center}.tab-content > .tab-pane{display:none}.tab-content > .active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:0.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.3125rem;padding-bottom:0.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50% / 100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width: 575.98px){.navbar-expand-sm > .container,.navbar-expand-sm > .container-fluid,.navbar-expand-sm > .container-lg,.navbar-expand-sm > .container-md,.navbar-expand-sm > .container-sm,.navbar-expand-sm > .container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm > .container,.navbar-expand-sm > .container-fluid,.navbar-expand-sm > .container-lg,.navbar-expand-sm > .container-md,.navbar-expand-sm > .container-sm,.navbar-expand-sm > .container-xl{flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md > .container,.navbar-expand-md > .container-fluid,.navbar-expand-md > .container-lg,.navbar-expand-md > .container-md,.navbar-expand-md > .container-sm,.navbar-expand-md > .container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md > .container,.navbar-expand-md > .container-fluid,.navbar-expand-md > .container-lg,.navbar-expand-md > .container-md,.navbar-expand-md > .container-sm,.navbar-expand-md > .container-xl{flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg > .container,.navbar-expand-lg > .container-fluid,.navbar-expand-lg > .container-lg,.navbar-expand-lg > .container-md,.navbar-expand-lg > .container-sm,.navbar-expand-lg > .container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg > .container,.navbar-expand-lg > .container-fluid,.navbar-expand-lg > .container-lg,.navbar-expand-lg > .container-md,.navbar-expand-lg > .container-sm,.navbar-expand-lg > .container-xl{flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl > .container,.navbar-expand-xl > .container-fluid,.navbar-expand-xl > .container-lg,.navbar-expand-xl > .container-md,.navbar-expand-xl > .container-sm,.navbar-expand-xl > .container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl > .container,.navbar-expand-xl > .container-fluid,.navbar-expand-xl > .container-lg,.navbar-expand-xl > .container-md,.navbar-expand-xl > .container-sm,.navbar-expand-xl > .container-xl{flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand > .container,.navbar-expand > .container-fluid,.navbar-expand > .container-lg,.navbar-expand > .container-md,.navbar-expand > .container-sm,.navbar-expand > .container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand > .container,.navbar-expand > .container-fluid,.navbar-expand > .container-lg,.navbar-expand > .container-md,.navbar-expand > .container-sm,.navbar-expand > .container-xl{flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0, 0, 0, 0.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0, 0, 0, 0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0, 0, 0, 0.3)}.navbar-light .navbar-nav .active > .nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show > .nav-link{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-toggler{color:rgba(0, 0, 0, 0.5);border-color:rgba(0, 0, 0, 0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0, 0, 0, 0.5)}.navbar-light .navbar-text a{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0, 0, 0, 0.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255, 255, 255, 0.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255, 255, 255, 0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255, 255, 255, 0.25)}.navbar-dark .navbar-nav .active > .nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show > .nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255, 255, 255, 0.5);border-color:rgba(255, 255, 255, 0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255, 255, 255, 0.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0, 0, 0, 0.125);border-radius:0.25rem}.card > hr{margin-right:0;margin-left:0}.card > .list-group{border-top:inherit;border-bottom:inherit}.card > .list-group:first-child{border-top-width:0;border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card > .list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card > .card-header + .list-group,.card > .list-group + .card-footer{border-top:0}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link + .card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:rgba(0, 0, 0, 0.03);border-bottom:1px solid rgba(0, 0, 0, 0.125)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-footer{padding:0.75rem 1.25rem;background-color:rgba(0, 0, 0, 0.03);border-top:1px solid rgba(0, 0, 0, 0.125)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(0.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{flex:1 0 0;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group > .card{margin-bottom:15px}@media (min-width: 576px){.card-group{display:flex;flex-flow:row wrap}.card-group > .card{flex:1 0 0;margin-bottom:0}.card-group > .card + .card{margin-left:0;border-left:0}.card-group > .card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group > .card:not(:last-child) .card-header,.card-group > .card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group > .card:not(:last-child) .card-footer,.card-group > .card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group > .card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group > .card:not(:first-child) .card-header,.card-group > .card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group > .card:not(:first-child) .card-footer,.card-group > .card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-moz-column-count:3;column-count:3;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion > .card{overflow:hidden}.accordion > .card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion > .card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion > .card > .card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:0.25rem}.breadcrumb-item + .breadcrumb-item{padding-left:0.5rem}.breadcrumb-item + .breadcrumb-item::before{float:left;padding-right:0.5rem;color:#6c757d;content:"/"}.breadcrumb-item + .breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item + .breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(0, 123, 255, 0.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(108, 117, 125, 0.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(40, 167, 69, 0.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(23, 162, 184, 0.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(255, 193, 7, 0.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(220, 53, 69, 0.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(248, 249, 250, 0.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(52, 58, 64, 0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;line-height:0;font-size:0.75rem;background-color:#e9ecef;border-radius:0.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:0.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0, 0, 0, 0.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item + .list-group-item{border-top-width:0}.list-group-item + .list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal > .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal > .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal > .list-group-item.active{margin-top:0}.list-group-horizontal > .list-group-item + .list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal > .list-group-item + .list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm > .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-sm > .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm > .list-group-item.active{margin-top:0}.list-group-horizontal-sm > .list-group-item + .list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm > .list-group-item + .list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md > .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-md > .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-md > .list-group-item.active{margin-top:0}.list-group-horizontal-md > .list-group-item + .list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md > .list-group-item + .list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg > .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-lg > .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg > .list-group-item.active{margin-top:0}.list-group-horizontal-lg > .list-group-item + .list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg > .list-group-item + .list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl > .list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-xl > .list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl > .list-group-item.active{margin-top:0}.list-group-horizontal-xl > .list-group-item + .list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl > .list-group-item + .list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush > .list-group-item{border-width:0 0 1px}.list-group-flush > .list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:0.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:0.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{flex-basis:350px;max-width:350px;font-size:0.875rem;background-color:rgba(255, 255, 255, 0.85);background-clip:padding-box;border:1px solid rgba(0, 0, 0, 0.1);box-shadow:0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);opacity:0;border-radius:0.25rem}.toast:not(:last-child){margin-bottom:0.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:0.25rem 0.75rem;color:#6c757d;background-color:rgba(255, 255, 255, 0.85);background-clip:padding-box;border-bottom:1px solid rgba(0, 0, 0, 0.05);border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.toast-body{padding:0.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform 0.3s ease-out;transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0, 0, 0, 0.2);border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:0.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(0.3rem - 1px);border-bottom-left-radius:calc(0.3rem - 1px)}.modal-footer > *{margin:0.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^="top"],.bs-tooltip-top{padding:0.4rem 0}.bs-tooltip-auto[x-placement^="top"] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^="top"] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^="right"],.bs-tooltip-right{padding:0 0.4rem}.bs-tooltip-auto[x-placement^="right"] .arrow,.bs-tooltip-right .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-auto[x-placement^="right"] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^="bottom"],.bs-tooltip-bottom{padding:0.4rem 0}.bs-tooltip-auto[x-placement^="bottom"] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^="bottom"] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^="left"],.bs-tooltip-left{padding:0 0.4rem}.bs-tooltip-auto[x-placement^="left"] .arrow,.bs-tooltip-left .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-auto[x-placement^="left"] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0, 0, 0, 0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^="top"],.bs-popover-top{margin-bottom:0.5rem}.bs-popover-auto[x-placement^="top"] > .arrow,.bs-popover-top > .arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-auto[x-placement^="top"] > .arrow::before,.bs-popover-top > .arrow::before{bottom:0;border-width:0.5rem 0.5rem 0;border-top-color:rgba(0, 0, 0, 0.25)}.bs-popover-auto[x-placement^="top"] > .arrow::after,.bs-popover-top > .arrow::after{bottom:1px;border-width:0.5rem 0.5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^="right"],.bs-popover-right{margin-left:0.5rem}.bs-popover-auto[x-placement^="right"] > .arrow,.bs-popover-right > .arrow{left:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-auto[x-placement^="right"] > .arrow::before,.bs-popover-right > .arrow::before{left:0;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:rgba(0, 0, 0, 0.25)}.bs-popover-auto[x-placement^="right"] > .arrow::after,.bs-popover-right > .arrow::after{left:1px;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^="bottom"],.bs-popover-bottom{margin-top:0.5rem}.bs-popover-auto[x-placement^="bottom"] > .arrow,.bs-popover-bottom > .arrow{top:calc(-0.5rem - 1px)}.bs-popover-auto[x-placement^="bottom"] > .arrow::before,.bs-popover-bottom > .arrow::before{top:0;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:rgba(0, 0, 0, 0.25)}.bs-popover-auto[x-placement^="bottom"] > .arrow::after,.bs-popover-bottom > .arrow::after{top:1px;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^="bottom"] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^="left"],.bs-popover-left{margin-right:0.5rem}.bs-popover-auto[x-placement^="left"] > .arrow,.bs-popover-left > .arrow{right:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-auto[x-placement^="left"] > .arrow::before,.bs-popover-left > .arrow::before{right:0;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:rgba(0, 0, 0, 0.25)}.bs-popover-auto[x-placement^="left"] > .arrow::after,.bs-popover-left > .arrow::after{right:1px;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:#fff}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform 0.6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s 0.6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:0.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50% / 100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:0.5;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:0.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:0.75s linear infinite spinner-border;animation:0.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:0.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:0.75s linear infinite spinner-grow;animation:0.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#007bff !important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc !important}.bg-secondary{background-color:#6c757d !important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62 !important}.bg-success{background-color:#28a745 !important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34 !important}.bg-info{background-color:#17a2b8 !important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b !important}.bg-warning{background-color:#ffc107 !important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00 !important}.bg-danger{background-color:#dc3545 !important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130 !important}.bg-light{background-color:#f8f9fa !important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5 !important}.bg-dark{background-color:#343a40 !important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#007bff !important}.border-secondary{border-color:#6c757d !important}.border-success{border-color:#28a745 !important}.border-info{border-color:#17a2b8 !important}.border-warning{border-color:#ffc107 !important}.border-danger{border-color:#dc3545 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#343a40 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:0.2rem !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-lg{border-radius:0.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-fill{flex:1 1 auto !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{-webkit-user-select:all !important;-moz-user-select:all !important;user-select:all !important}.user-select-auto{-webkit-user-select:auto !important;-moz-user-select:auto !important;-ms-user-select:auto !important;user-select:auto !important}.user-select-none{-webkit-user-select:none !important;-moz-user-select:none !important;-ms-user-select:none !important;user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position: -webkit-sticky) or (position: sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important}.shadow{box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0, 0, 0, 0.175) !important}.shadow-none{box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-0.25rem !important}.mt-n1,.my-n1{margin-top:-0.25rem !important}.mr-n1,.mx-n1{margin-right:-0.25rem !important}.mb-n1,.my-n1{margin-bottom:-0.25rem !important}.ml-n1,.mx-n1{margin-left:-0.25rem !important}.m-n2{margin:-0.5rem !important}.mt-n2,.my-n2{margin-top:-0.5rem !important}.mr-n2,.mx-n2{margin-right:-0.5rem !important}.mb-n2,.my-n2{margin-bottom:-0.5rem !important}.ml-n2,.mx-n2{margin-left:-0.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-0.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-0.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-0.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-0.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-0.25rem !important}.m-sm-n2{margin:-0.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-0.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-0.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-0.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-0.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-0.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-0.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-0.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-0.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-0.25rem !important}.m-md-n2{margin:-0.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-0.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-0.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-0.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-0.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-0.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-0.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-0.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-0.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-0.25rem !important}.m-lg-n2{margin:-0.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-0.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-0.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-0.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-0.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-0.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-0.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-0.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-0.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-0.25rem !important}.m-xl-n2{margin:-0.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-0.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-0.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-0.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-0.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0, 0, 0, 0)}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#007bff !important}a.text-primary:focus,a.text-primary:hover{color:#0056b3 !important}.text-secondary{color:#6c757d !important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54 !important}.text-success{color:#28a745 !important}a.text-success:focus,a.text-success:hover{color:#19692c !important}.text-info{color:#17a2b8 !important}a.text-info:focus,a.text-info:hover{color:#0f6674 !important}.text-warning{color:#ffc107 !important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00 !important}.text-danger{color:#dc3545 !important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a !important}.text-light{color:#f8f9fa !important}a.text-light:focus,a.text-light:hover{color:#cbd3da !important}.text-dark{color:#343a40 !important}a.text-dark:focus,a.text-dark:hover{color:#121416 !important}.text-body{color:#212529 !important}.text-muted{color:#6c757d !important}.text-black-50{color:rgba(0, 0, 0, 0.5) !important}.text-white-50{color:rgba(255, 255, 255, 0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::after,*::before{text-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark tbody + tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}.rtl,[dir="rtl"]{text-align:right;direction:rtl}.rtl .nav,[dir="rtl"] .nav{padding-right:0}.rtl .navbar-nav .nav-item,[dir="rtl"] .navbar-nav .nav-item{float:right}.rtl .navbar-nav .nav-item + .nav-item,[dir="rtl"] .navbar-nav .nav-item + .nav-item{margin-right:inherit;margin-left:1rem}.rtl th,[dir="rtl"] th{text-align:right}.rtl .alert-dismissible,[dir="rtl"] .alert-dismissible{padding-right:1.25rem;padding-left:4rem}.rtl .dropdown-menu,[dir="rtl"] .dropdown-menu{right:0;left:inherit;text-align:right}.rtl .checkbox label,[dir="rtl"] .checkbox label{padding-right:1.25rem;padding-left:inherit}.rtl .btn-group > .btn-group:not(:first-child),.rtl .btn-group > .btn:not(:first-child),[dir="rtl"] .btn-group > .btn-group:not(:first-child),[dir="rtl"] .btn-group > .btn:not(:first-child){margin-left:initial;margin-right:-1px}.rtl .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle),[dir="rtl"] .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle){border-radius:0 0.25rem 0.25rem 0}.rtl .btn-group > .btn:last-child:not(:first-child),.rtl .btn-group > .dropdown-toggle:not(:first-child),[dir="rtl"] .btn-group > .btn:last-child:not(:first-child),[dir="rtl"] .btn-group > .dropdown-toggle:not(:first-child){border-radius:0.25rem 0 0 0.25rem}.rtl .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child,[dir="rtl"] .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child{border-radius:0.25rem 0 0 0.25rem}.rtl .custom-control,[dir="rtl"] .custom-control{padding-right:1.5rem;padding-left:inherit;margin-right:inherit;margin-left:1rem}.rtl .custom-control-indicator,[dir="rtl"] .custom-control-indicator{right:0;left:inherit}.rtl .custom-file-label::after,[dir="rtl"] .custom-file-label::after{right:initial;left:-1px;border-radius:0.25rem 0 0 0.25rem}.rtl .custom-control-label::after,.rtl .custom-control-label::before,[dir="rtl"] .custom-control-label::after,[dir="rtl"] .custom-control-label::before{right:-1.5rem;left:inherit}.rtl .custom-select,[dir="rtl"] .custom-select{padding:0.375rem 0.75rem 0.375rem 1.75rem;background:#fff url("data:image/svg+xml,") no-repeat left 0.75rem center;background-size:8px 10px}.rtl .custom-switch,[dir="rtl"] .custom-switch{padding-right:2.25rem;padding-left:inherit}.rtl .custom-switch .custom-control-label::before,[dir="rtl"] .custom-switch .custom-control-label::before{right:-2.25rem}.rtl .custom-switch .custom-control-label::after,[dir="rtl"] .custom-switch .custom-control-label::after{right:calc(-2.25rem + 2px)}.rtl .custom-switch .custom-control-input:checked ~ .custom-control-label::after,[dir="rtl"] .custom-switch .custom-control-input:checked ~ .custom-control-label::after{transform:translateX(-0.75rem)}.rtl .input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),.rtl .input-group > .input-group-append:last-child > .input-group-text:not(:last-child),.rtl .input-group > .input-group-append:not(:last-child) > .btn,.rtl .input-group > .input-group-append:not(:last-child) > .input-group-text,.rtl .input-group > .input-group-prepend > .btn,.rtl .input-group > .input-group-prepend > .input-group-text,[dir="rtl"] .input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),[dir="rtl"] .input-group > .input-group-append:last-child > .input-group-text:not(:last-child),[dir="rtl"] .input-group > .input-group-append:not(:last-child) > .btn,[dir="rtl"] .input-group > .input-group-append:not(:last-child) > .input-group-text,[dir="rtl"] .input-group > .input-group-prepend > .btn,[dir="rtl"] .input-group > .input-group-prepend > .input-group-text{border-radius:0 0.25rem 0.25rem 0}.rtl .input-group > .input-group-append > .btn,.rtl .input-group > .input-group-append > .input-group-text,.rtl .input-group > .input-group-prepend:first-child > .btn:not(:first-child),.rtl .input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child),.rtl .input-group > .input-group-prepend:not(:first-child) > .btn,.rtl .input-group > .input-group-prepend:not(:first-child) > .input-group-text,[dir="rtl"] .input-group > .input-group-append > .btn,[dir="rtl"] .input-group > .input-group-append > .input-group-text,[dir="rtl"] .input-group > .input-group-prepend:first-child > .btn:not(:first-child),[dir="rtl"] .input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child),[dir="rtl"] .input-group > .input-group-prepend:not(:first-child) > .btn,[dir="rtl"] .input-group > .input-group-prepend:not(:first-child) > .input-group-text{border-radius:0.25rem 0 0 0.25rem}.rtl .input-group > .custom-select:not(:first-child),.rtl .input-group > .form-control:not(:first-child),[dir="rtl"] .input-group > .custom-select:not(:first-child),[dir="rtl"] .input-group > .form-control:not(:first-child){border-radius:0.25rem 0 0 0.25rem}.rtl .input-group > .custom-select:not(:last-child),.rtl .input-group > .form-control:not(:last-child),[dir="rtl"] .input-group > .custom-select:not(:last-child),[dir="rtl"] .input-group > .form-control:not(:last-child){border-radius:0 0.25rem 0.25rem 0}.rtl .input-group > .custom-select:not(:last-child):not(:first-child),.rtl .input-group > .form-control:not(:last-child):not(:first-child),[dir="rtl"] .input-group > .custom-select:not(:last-child):not(:first-child),[dir="rtl"] .input-group > .form-control:not(:last-child):not(:first-child){border-radius:0}.rtl .checkbox input,.rtl .checkbox-inline input,.rtl .radio input,.rtl .radio-inline,[dir="rtl"] .radio input,[dir="rtl"] .radio-inline,[dir="rtl"] .checkbox input,[dir="rtl"] .checkbox-inline input{margin-right:-1.25rem;margin-left:inherit}.rtl .breadcrumb-item + .breadcrumb-item,[dir="rtl"] .breadcrumb-item + .breadcrumb-item{padding-right:0.5rem;padding-left:0;color:#6c757d;content:"/"}.rtl .breadcrumb-item + .breadcrumb-item::before,[dir="rtl"] .breadcrumb-item + .breadcrumb-item::before{padding-right:0;padding-left:0.5rem}.rtl .list-group,[dir="rtl"] .list-group{padding-right:0;padding-left:40px}.rtl .close,[dir="rtl"] .close{float:left}.rtl .modal-header .close,[dir="rtl"] .modal-header .close{margin:-15px auto -15px -15px}.rtl .modal-footer > :not(:first-child),[dir="rtl"] .modal-footer > :not(:first-child){margin-right:0.25rem}.rtl .modal-footer > :not(:last-child),[dir="rtl"] .modal-footer > :not(:last-child){margin-left:0.25rem}.rtl .modal-footer > :first-child,[dir="rtl"] .modal-footer > :first-child{margin-right:0}.rtl .modal-footer > :last-child,[dir="rtl"] .modal-footer > :last-child{margin-left:0}.rtl .alert-dismissible .close,[dir="rtl"] .alert-dismissible .close{right:inherit;left:0}.rtl .dropdown-toggle::after,[dir="rtl"] .dropdown-toggle::after{margin-right:0.255em;margin-left:0}.rtl .form-check-input,[dir="rtl"] .form-check-input{margin-right:-1.25rem;margin-left:inherit}.rtl .form-check-label,[dir="rtl"] .form-check-label{padding-right:1.25rem;padding-left:inherit}.rtl .list-inline,.rtl .list-unstyled,.rtl .pagination,[dir="rtl"] .pagination,[dir="rtl"] .list-unstyled,[dir="rtl"] .list-inline{padding-right:0;padding-left:inherit}.rtl .pagination .page-item:first-child .page-link,[dir="rtl"] .pagination .page-item:first-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;border-top-left-radius:0;border-bottom-left-radius:0}.rtl .pagination .page-item:last-child .page-link,[dir="rtl"] .pagination .page-item:last-child .page-link{border-top-right-radius:0;border-bottom-right-radius:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.rtl .offset-1,[dir="rtl"] .offset-1{margin-right:8.333333%;margin-left:0}.rtl .offset-2,[dir="rtl"] .offset-2{margin-right:16.666667%;margin-left:0}.rtl .offset-3,[dir="rtl"] .offset-3{margin-right:25%;margin-left:0}.rtl .offset-4,[dir="rtl"] .offset-4{margin-right:33.333333%;margin-left:0}.rtl .offset-5,[dir="rtl"] .offset-5{margin-right:41.666667%;margin-left:0}.rtl .offset-6,[dir="rtl"] .offset-6{margin-right:50%;margin-left:0}.rtl .offset-7,[dir="rtl"] .offset-7{margin-right:58.333333%;margin-left:0}.rtl .offset-8,[dir="rtl"] .offset-8{margin-right:66.666667%;margin-left:0}.rtl .offset-9,[dir="rtl"] .offset-9{margin-right:75%;margin-left:0}.rtl .offset-10,[dir="rtl"] .offset-10{margin-right:83.333333%;margin-left:0}.rtl .offset-11,[dir="rtl"] .offset-11{margin-right:91.666667%;margin-left:0}@media (min-width: 576px){.rtl .offset-sm-0,[dir="rtl"] .offset-sm-0{margin-right:0;margin-left:0}.rtl .offset-sm-1,[dir="rtl"] .offset-sm-1{margin-right:8.333333%;margin-left:0}.rtl .offset-sm-2,[dir="rtl"] .offset-sm-2{margin-right:16.666667%;margin-left:0}.rtl .offset-sm-3,[dir="rtl"] .offset-sm-3{margin-right:25%;margin-left:0}.rtl .offset-sm-4,[dir="rtl"] .offset-sm-4{margin-right:33.333333%;margin-left:0}.rtl .offset-sm-5,[dir="rtl"] .offset-sm-5{margin-right:41.666667%;margin-left:0}.rtl .offset-sm-6,[dir="rtl"] .offset-sm-6{margin-right:50%;margin-left:0}.rtl .offset-sm-7,[dir="rtl"] .offset-sm-7{margin-right:58.333333%;margin-left:0}.rtl .offset-sm-8,[dir="rtl"] .offset-sm-8{margin-right:66.666667%;margin-left:0}.rtl .offset-sm-9,[dir="rtl"] .offset-sm-9{margin-right:75%;margin-left:0}.rtl .offset-sm-10,[dir="rtl"] .offset-sm-10{margin-right:83.333333%;margin-left:0}.rtl .offset-sm-11,[dir="rtl"] .offset-sm-11{margin-right:91.666667%;margin-left:0}}@media (min-width: 768px){.rtl .offset-md-0,[dir="rtl"] .offset-md-0{margin-right:0;margin-left:0}.rtl .offset-md-1,[dir="rtl"] .offset-md-1{margin-right:8.333333%;margin-left:0}.rtl .offset-md-2,[dir="rtl"] .offset-md-2{margin-right:16.666667%;margin-left:0}.rtl .offset-md-3,[dir="rtl"] .offset-md-3{margin-right:25%;margin-left:0}.rtl .offset-md-4,[dir="rtl"] .offset-md-4{margin-right:33.333333%;margin-left:0}.rtl .offset-md-5,[dir="rtl"] .offset-md-5{margin-right:41.666667%;margin-left:0}.rtl .offset-md-6,[dir="rtl"] .offset-md-6{margin-right:50%;margin-left:0}.rtl .offset-md-7,[dir="rtl"] .offset-md-7{margin-right:58.333333%;margin-left:0}.rtl .offset-md-8,[dir="rtl"] .offset-md-8{margin-right:66.666667%;margin-left:0}.rtl .offset-md-9,[dir="rtl"] .offset-md-9{margin-right:75%;margin-left:0}.rtl .offset-md-10,[dir="rtl"] .offset-md-10{margin-right:83.333333%;margin-left:0}.rtl .offset-md-11,[dir="rtl"] .offset-md-11{margin-right:91.666667%;margin-left:0}}@media (min-width: 992px){.rtl .offset-lg-0,[dir="rtl"] .offset-lg-0{margin-right:0;margin-left:0}.rtl .offset-lg-1,[dir="rtl"] .offset-lg-1{margin-right:8.333333%;margin-left:0}.rtl .offset-lg-2,[dir="rtl"] .offset-lg-2{margin-right:16.666667%;margin-left:0}.rtl .offset-lg-3,[dir="rtl"] .offset-lg-3{margin-right:25%;margin-left:0}.rtl .offset-lg-4,[dir="rtl"] .offset-lg-4{margin-right:33.333333%;margin-left:0}.rtl .offset-lg-5,[dir="rtl"] .offset-lg-5{margin-right:41.666667%;margin-left:0}.rtl .offset-lg-6,[dir="rtl"] .offset-lg-6{margin-right:50%;margin-left:0}.rtl .offset-lg-7,[dir="rtl"] .offset-lg-7{margin-right:58.333333%;margin-left:0}.rtl .offset-lg-8,[dir="rtl"] .offset-lg-8{margin-right:66.666667%;margin-left:0}.rtl .offset-lg-9,[dir="rtl"] .offset-lg-9{margin-right:75%;margin-left:0}.rtl .offset-lg-10,[dir="rtl"] .offset-lg-10{margin-right:83.333333%;margin-left:0}.rtl .offset-lg-11,[dir="rtl"] .offset-lg-11{margin-right:91.666667%;margin-left:0}}@media (min-width: 1200px){.rtl .offset-xl-0,[dir="rtl"] .offset-xl-0{margin-right:0;margin-left:0}.rtl .offset-xl-1,[dir="rtl"] .offset-xl-1{margin-right:8.333333%;margin-left:0}.rtl .offset-xl-2,[dir="rtl"] .offset-xl-2{margin-right:16.666667%;margin-left:0}.rtl .offset-xl-3,[dir="rtl"] .offset-xl-3{margin-right:25%;margin-left:0}.rtl .offset-xl-4,[dir="rtl"] .offset-xl-4{margin-right:33.333333%;margin-left:0}.rtl .offset-xl-5,[dir="rtl"] .offset-xl-5{margin-right:41.666667%;margin-left:0}.rtl .offset-xl-6,[dir="rtl"] .offset-xl-6{margin-right:50%;margin-left:0}.rtl .offset-xl-7,[dir="rtl"] .offset-xl-7{margin-right:58.333333%;margin-left:0}.rtl .offset-xl-8,[dir="rtl"] .offset-xl-8{margin-right:66.666667%;margin-left:0}.rtl .offset-xl-9,[dir="rtl"] .offset-xl-9{margin-right:75%;margin-left:0}.rtl .offset-xl-10,[dir="rtl"] .offset-xl-10{margin-right:83.333333%;margin-left:0}.rtl .offset-xl-11,[dir="rtl"] .offset-xl-11{margin-right:91.666667%;margin-left:0}}.rtl .mr-0,[dir="rtl"] .mr-0{margin-right:0 !important;margin-left:0 !important}.rtl .ml-0,[dir="rtl"] .ml-0{margin-left:0 !important;margin-right:0 !important}.rtl mx-0,[dir="rtl"] mx-0{margin-left:0 !important;margin-right:0 !important}.rtl .mr-1,[dir="rtl"] .mr-1{margin-right:0 !important;margin-left:0.25rem !important}.rtl .ml-1,[dir="rtl"] .ml-1{margin-left:0 !important;margin-right:0.25rem !important}.rtl mx-1,[dir="rtl"] mx-1{margin-left:0.25rem !important;margin-right:0.25rem !important}.rtl .mr-2,[dir="rtl"] .mr-2{margin-right:0 !important;margin-left:0.5rem !important}.rtl .ml-2,[dir="rtl"] .ml-2{margin-left:0 !important;margin-right:0.5rem !important}.rtl mx-2,[dir="rtl"] mx-2{margin-left:0.5rem !important;margin-right:0.5rem !important}.rtl .mr-3,[dir="rtl"] .mr-3{margin-right:0 !important;margin-left:1rem !important}.rtl .ml-3,[dir="rtl"] .ml-3{margin-left:0 !important;margin-right:1rem !important}.rtl mx-3,[dir="rtl"] mx-3{margin-left:1rem !important;margin-right:1rem !important}.rtl .mr-4,[dir="rtl"] .mr-4{margin-right:0 !important;margin-left:1.5rem !important}.rtl .ml-4,[dir="rtl"] .ml-4{margin-left:0 !important;margin-right:1.5rem !important}.rtl mx-4,[dir="rtl"] mx-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.rtl .mr-5,[dir="rtl"] .mr-5{margin-right:0 !important;margin-left:3rem !important}.rtl .ml-5,[dir="rtl"] .ml-5{margin-left:0 !important;margin-right:3rem !important}.rtl mx-5,[dir="rtl"] mx-5{margin-left:3rem !important;margin-right:3rem !important}.rtl .pr-0,[dir="rtl"] .pr-0{padding-right:0 !important;padding-left:0 !important}.rtl .pl-0,[dir="rtl"] .pl-0{padding-left:0 !important;padding-right:0 !important}.rtl px-0,[dir="rtl"] px-0{padding-left:0 !important;padding-right:0 !important}.rtl .pr-1,[dir="rtl"] .pr-1{padding-right:0 !important;padding-left:0.25rem !important}.rtl .pl-1,[dir="rtl"] .pl-1{padding-left:0 !important;padding-right:0.25rem !important}.rtl px-1,[dir="rtl"] px-1{padding-left:0.25rem !important;padding-right:0.25rem !important}.rtl .pr-2,[dir="rtl"] .pr-2{padding-right:0 !important;padding-left:0.5rem !important}.rtl .pl-2,[dir="rtl"] .pl-2{padding-left:0 !important;padding-right:0.5rem !important}.rtl px-2,[dir="rtl"] px-2{padding-left:0.5rem !important;padding-right:0.5rem !important}.rtl .pr-3,[dir="rtl"] .pr-3{padding-right:0 !important;padding-left:1rem !important}.rtl .pl-3,[dir="rtl"] .pl-3{padding-left:0 !important;padding-right:1rem !important}.rtl px-3,[dir="rtl"] px-3{padding-left:1rem !important;padding-right:1rem !important}.rtl .pr-4,[dir="rtl"] .pr-4{padding-right:0 !important;padding-left:1.5rem !important}.rtl .pl-4,[dir="rtl"] .pl-4{padding-left:0 !important;padding-right:1.5rem !important}.rtl px-4,[dir="rtl"] px-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.rtl .pr-5,[dir="rtl"] .pr-5{padding-right:0 !important;padding-left:3rem !important}.rtl .pl-5,[dir="rtl"] .pl-5{padding-left:0 !important;padding-right:3rem !important}.rtl px-5,[dir="rtl"] px-5{padding-left:3rem !important;padding-right:3rem !important}.rtl .mr-auto,[dir="rtl"] .mr-auto{margin-right:0 !important;margin-left:auto !important}.rtl .ml-auto,[dir="rtl"] .ml-auto{margin-right:auto !important;margin-left:0 !important}.rtl .mx-auto,[dir="rtl"] .mx-auto{margin-right:auto !important;margin-left:auto !important}@media (min-width: 576px){.rtl .mr-sm-0,[dir="rtl"] .mr-sm-0{margin-right:0 !important;margin-left:0 !important}.rtl .ml-sm-0,[dir="rtl"] .ml-sm-0{margin-left:0 !important;margin-right:0 !important}.rtl mx-sm-0,[dir="rtl"] mx-sm-0{margin-left:0 !important;margin-right:0 !important}.rtl .mr-sm-1,[dir="rtl"] .mr-sm-1{margin-right:0 !important;margin-left:0.25rem !important}.rtl .ml-sm-1,[dir="rtl"] .ml-sm-1{margin-left:0 !important;margin-right:0.25rem !important}.rtl mx-sm-1,[dir="rtl"] mx-sm-1{margin-left:0.25rem !important;margin-right:0.25rem !important}.rtl .mr-sm-2,[dir="rtl"] .mr-sm-2{margin-right:0 !important;margin-left:0.5rem !important}.rtl .ml-sm-2,[dir="rtl"] .ml-sm-2{margin-left:0 !important;margin-right:0.5rem !important}.rtl mx-sm-2,[dir="rtl"] mx-sm-2{margin-left:0.5rem !important;margin-right:0.5rem !important}.rtl .mr-sm-3,[dir="rtl"] .mr-sm-3{margin-right:0 !important;margin-left:1rem !important}.rtl .ml-sm-3,[dir="rtl"] .ml-sm-3{margin-left:0 !important;margin-right:1rem !important}.rtl mx-sm-3,[dir="rtl"] mx-sm-3{margin-left:1rem !important;margin-right:1rem !important}.rtl .mr-sm-4,[dir="rtl"] .mr-sm-4{margin-right:0 !important;margin-left:1.5rem !important}.rtl .ml-sm-4,[dir="rtl"] .ml-sm-4{margin-left:0 !important;margin-right:1.5rem !important}.rtl mx-sm-4,[dir="rtl"] mx-sm-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.rtl .mr-sm-5,[dir="rtl"] .mr-sm-5{margin-right:0 !important;margin-left:3rem !important}.rtl .ml-sm-5,[dir="rtl"] .ml-sm-5{margin-left:0 !important;margin-right:3rem !important}.rtl mx-sm-5,[dir="rtl"] mx-sm-5{margin-left:3rem !important;margin-right:3rem !important}.rtl .pr-sm-0,[dir="rtl"] .pr-sm-0{padding-right:0 !important;padding-left:0 !important}.rtl .pl-sm-0,[dir="rtl"] .pl-sm-0{padding-left:0 !important;padding-right:0 !important}.rtl px-sm-0,[dir="rtl"] px-sm-0{padding-left:0 !important;padding-right:0 !important}.rtl .pr-sm-1,[dir="rtl"] .pr-sm-1{padding-right:0 !important;padding-left:0.25rem !important}.rtl .pl-sm-1,[dir="rtl"] .pl-sm-1{padding-left:0 !important;padding-right:0.25rem !important}.rtl px-sm-1,[dir="rtl"] px-sm-1{padding-left:0.25rem !important;padding-right:0.25rem !important}.rtl .pr-sm-2,[dir="rtl"] .pr-sm-2{padding-right:0 !important;padding-left:0.5rem !important}.rtl .pl-sm-2,[dir="rtl"] .pl-sm-2{padding-left:0 !important;padding-right:0.5rem !important}.rtl px-sm-2,[dir="rtl"] px-sm-2{padding-left:0.5rem !important;padding-right:0.5rem !important}.rtl .pr-sm-3,[dir="rtl"] .pr-sm-3{padding-right:0 !important;padding-left:1rem !important}.rtl .pl-sm-3,[dir="rtl"] .pl-sm-3{padding-left:0 !important;padding-right:1rem !important}.rtl px-sm-3,[dir="rtl"] px-sm-3{padding-left:1rem !important;padding-right:1rem !important}.rtl .pr-sm-4,[dir="rtl"] .pr-sm-4{padding-right:0 !important;padding-left:1.5rem !important}.rtl .pl-sm-4,[dir="rtl"] .pl-sm-4{padding-left:0 !important;padding-right:1.5rem !important}.rtl px-sm-4,[dir="rtl"] px-sm-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.rtl .pr-sm-5,[dir="rtl"] .pr-sm-5{padding-right:0 !important;padding-left:3rem !important}.rtl .pl-sm-5,[dir="rtl"] .pl-sm-5{padding-left:0 !important;padding-right:3rem !important}.rtl px-sm-5,[dir="rtl"] px-sm-5{padding-left:3rem !important;padding-right:3rem !important}.rtl .mr-sm-auto,[dir="rtl"] .mr-sm-auto{margin-right:0 !important;margin-left:auto !important}.rtl .ml-sm-auto,[dir="rtl"] .ml-sm-auto{margin-right:auto !important;margin-left:0 !important}.rtl .mx-sm-auto,[dir="rtl"] .mx-sm-auto{margin-right:auto !important;margin-left:auto !important}}@media (min-width: 768px){.rtl .mr-md-0,[dir="rtl"] .mr-md-0{margin-right:0 !important;margin-left:0 !important}.rtl .ml-md-0,[dir="rtl"] .ml-md-0{margin-left:0 !important;margin-right:0 !important}.rtl mx-md-0,[dir="rtl"] mx-md-0{margin-left:0 !important;margin-right:0 !important}.rtl .mr-md-1,[dir="rtl"] .mr-md-1{margin-right:0 !important;margin-left:0.25rem !important}.rtl .ml-md-1,[dir="rtl"] .ml-md-1{margin-left:0 !important;margin-right:0.25rem !important}.rtl mx-md-1,[dir="rtl"] mx-md-1{margin-left:0.25rem !important;margin-right:0.25rem !important}.rtl .mr-md-2,[dir="rtl"] .mr-md-2{margin-right:0 !important;margin-left:0.5rem !important}.rtl .ml-md-2,[dir="rtl"] .ml-md-2{margin-left:0 !important;margin-right:0.5rem !important}.rtl mx-md-2,[dir="rtl"] mx-md-2{margin-left:0.5rem !important;margin-right:0.5rem !important}.rtl .mr-md-3,[dir="rtl"] .mr-md-3{margin-right:0 !important;margin-left:1rem !important}.rtl .ml-md-3,[dir="rtl"] .ml-md-3{margin-left:0 !important;margin-right:1rem !important}.rtl mx-md-3,[dir="rtl"] mx-md-3{margin-left:1rem !important;margin-right:1rem !important}.rtl .mr-md-4,[dir="rtl"] .mr-md-4{margin-right:0 !important;margin-left:1.5rem !important}.rtl .ml-md-4,[dir="rtl"] .ml-md-4{margin-left:0 !important;margin-right:1.5rem !important}.rtl mx-md-4,[dir="rtl"] mx-md-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.rtl .mr-md-5,[dir="rtl"] .mr-md-5{margin-right:0 !important;margin-left:3rem !important}.rtl .ml-md-5,[dir="rtl"] .ml-md-5{margin-left:0 !important;margin-right:3rem !important}.rtl mx-md-5,[dir="rtl"] mx-md-5{margin-left:3rem !important;margin-right:3rem !important}.rtl .pr-md-0,[dir="rtl"] .pr-md-0{padding-right:0 !important;padding-left:0 !important}.rtl .pl-md-0,[dir="rtl"] .pl-md-0{padding-left:0 !important;padding-right:0 !important}.rtl px-md-0,[dir="rtl"] px-md-0{padding-left:0 !important;padding-right:0 !important}.rtl .pr-md-1,[dir="rtl"] .pr-md-1{padding-right:0 !important;padding-left:0.25rem !important}.rtl .pl-md-1,[dir="rtl"] .pl-md-1{padding-left:0 !important;padding-right:0.25rem !important}.rtl px-md-1,[dir="rtl"] px-md-1{padding-left:0.25rem !important;padding-right:0.25rem !important}.rtl .pr-md-2,[dir="rtl"] .pr-md-2{padding-right:0 !important;padding-left:0.5rem !important}.rtl .pl-md-2,[dir="rtl"] .pl-md-2{padding-left:0 !important;padding-right:0.5rem !important}.rtl px-md-2,[dir="rtl"] px-md-2{padding-left:0.5rem !important;padding-right:0.5rem !important}.rtl .pr-md-3,[dir="rtl"] .pr-md-3{padding-right:0 !important;padding-left:1rem !important}.rtl .pl-md-3,[dir="rtl"] .pl-md-3{padding-left:0 !important;padding-right:1rem !important}.rtl px-md-3,[dir="rtl"] px-md-3{padding-left:1rem !important;padding-right:1rem !important}.rtl .pr-md-4,[dir="rtl"] .pr-md-4{padding-right:0 !important;padding-left:1.5rem !important}.rtl .pl-md-4,[dir="rtl"] .pl-md-4{padding-left:0 !important;padding-right:1.5rem !important}.rtl px-md-4,[dir="rtl"] px-md-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.rtl .pr-md-5,[dir="rtl"] .pr-md-5{padding-right:0 !important;padding-left:3rem !important}.rtl .pl-md-5,[dir="rtl"] .pl-md-5{padding-left:0 !important;padding-right:3rem !important}.rtl px-md-5,[dir="rtl"] px-md-5{padding-left:3rem !important;padding-right:3rem !important}.rtl .mr-md-auto,[dir="rtl"] .mr-md-auto{margin-right:0 !important;margin-left:auto !important}.rtl .ml-md-auto,[dir="rtl"] .ml-md-auto{margin-right:auto !important;margin-left:0 !important}.rtl .mx-md-auto,[dir="rtl"] .mx-md-auto{margin-right:auto !important;margin-left:auto !important}}@media (min-width: 992px){.rtl .mr-lg-0,[dir="rtl"] .mr-lg-0{margin-right:0 !important;margin-left:0 !important}.rtl .ml-lg-0,[dir="rtl"] .ml-lg-0{margin-left:0 !important;margin-right:0 !important}.rtl mx-lg-0,[dir="rtl"] mx-lg-0{margin-left:0 !important;margin-right:0 !important}.rtl .mr-lg-1,[dir="rtl"] .mr-lg-1{margin-right:0 !important;margin-left:0.25rem !important}.rtl .ml-lg-1,[dir="rtl"] .ml-lg-1{margin-left:0 !important;margin-right:0.25rem !important}.rtl mx-lg-1,[dir="rtl"] mx-lg-1{margin-left:0.25rem !important;margin-right:0.25rem !important}.rtl .mr-lg-2,[dir="rtl"] .mr-lg-2{margin-right:0 !important;margin-left:0.5rem !important}.rtl .ml-lg-2,[dir="rtl"] .ml-lg-2{margin-left:0 !important;margin-right:0.5rem !important}.rtl mx-lg-2,[dir="rtl"] mx-lg-2{margin-left:0.5rem !important;margin-right:0.5rem !important}.rtl .mr-lg-3,[dir="rtl"] .mr-lg-3{margin-right:0 !important;margin-left:1rem !important}.rtl .ml-lg-3,[dir="rtl"] .ml-lg-3{margin-left:0 !important;margin-right:1rem !important}.rtl mx-lg-3,[dir="rtl"] mx-lg-3{margin-left:1rem !important;margin-right:1rem !important}.rtl .mr-lg-4,[dir="rtl"] .mr-lg-4{margin-right:0 !important;margin-left:1.5rem !important}.rtl .ml-lg-4,[dir="rtl"] .ml-lg-4{margin-left:0 !important;margin-right:1.5rem !important}.rtl mx-lg-4,[dir="rtl"] mx-lg-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.rtl .mr-lg-5,[dir="rtl"] .mr-lg-5{margin-right:0 !important;margin-left:3rem !important}.rtl .ml-lg-5,[dir="rtl"] .ml-lg-5{margin-left:0 !important;margin-right:3rem !important}.rtl mx-lg-5,[dir="rtl"] mx-lg-5{margin-left:3rem !important;margin-right:3rem !important}.rtl .pr-lg-0,[dir="rtl"] .pr-lg-0{padding-right:0 !important;padding-left:0 !important}.rtl .pl-lg-0,[dir="rtl"] .pl-lg-0{padding-left:0 !important;padding-right:0 !important}.rtl px-lg-0,[dir="rtl"] px-lg-0{padding-left:0 !important;padding-right:0 !important}.rtl .pr-lg-1,[dir="rtl"] .pr-lg-1{padding-right:0 !important;padding-left:0.25rem !important}.rtl .pl-lg-1,[dir="rtl"] .pl-lg-1{padding-left:0 !important;padding-right:0.25rem !important}.rtl px-lg-1,[dir="rtl"] px-lg-1{padding-left:0.25rem !important;padding-right:0.25rem !important}.rtl .pr-lg-2,[dir="rtl"] .pr-lg-2{padding-right:0 !important;padding-left:0.5rem !important}.rtl .pl-lg-2,[dir="rtl"] .pl-lg-2{padding-left:0 !important;padding-right:0.5rem !important}.rtl px-lg-2,[dir="rtl"] px-lg-2{padding-left:0.5rem !important;padding-right:0.5rem !important}.rtl .pr-lg-3,[dir="rtl"] .pr-lg-3{padding-right:0 !important;padding-left:1rem !important}.rtl .pl-lg-3,[dir="rtl"] .pl-lg-3{padding-left:0 !important;padding-right:1rem !important}.rtl px-lg-3,[dir="rtl"] px-lg-3{padding-left:1rem !important;padding-right:1rem !important}.rtl .pr-lg-4,[dir="rtl"] .pr-lg-4{padding-right:0 !important;padding-left:1.5rem !important}.rtl .pl-lg-4,[dir="rtl"] .pl-lg-4{padding-left:0 !important;padding-right:1.5rem !important}.rtl px-lg-4,[dir="rtl"] px-lg-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.rtl .pr-lg-5,[dir="rtl"] .pr-lg-5{padding-right:0 !important;padding-left:3rem !important}.rtl .pl-lg-5,[dir="rtl"] .pl-lg-5{padding-left:0 !important;padding-right:3rem !important}.rtl px-lg-5,[dir="rtl"] px-lg-5{padding-left:3rem !important;padding-right:3rem !important}.rtl .mr-lg-auto,[dir="rtl"] .mr-lg-auto{margin-right:0 !important;margin-left:auto !important}.rtl .ml-lg-auto,[dir="rtl"] .ml-lg-auto{margin-right:auto !important;margin-left:0 !important}.rtl .mx-lg-auto,[dir="rtl"] .mx-lg-auto{margin-right:auto !important;margin-left:auto !important}}@media (min-width: 1200px){.rtl .mr-xl-0,[dir="rtl"] .mr-xl-0{margin-right:0 !important;margin-left:0 !important}.rtl .ml-xl-0,[dir="rtl"] .ml-xl-0{margin-left:0 !important;margin-right:0 !important}.rtl mx-xl-0,[dir="rtl"] mx-xl-0{margin-left:0 !important;margin-right:0 !important}.rtl .mr-xl-1,[dir="rtl"] .mr-xl-1{margin-right:0 !important;margin-left:0.25rem !important}.rtl .ml-xl-1,[dir="rtl"] .ml-xl-1{margin-left:0 !important;margin-right:0.25rem !important}.rtl mx-xl-1,[dir="rtl"] mx-xl-1{margin-left:0.25rem !important;margin-right:0.25rem !important}.rtl .mr-xl-2,[dir="rtl"] .mr-xl-2{margin-right:0 !important;margin-left:0.5rem !important}.rtl .ml-xl-2,[dir="rtl"] .ml-xl-2{margin-left:0 !important;margin-right:0.5rem !important}.rtl mx-xl-2,[dir="rtl"] mx-xl-2{margin-left:0.5rem !important;margin-right:0.5rem !important}.rtl .mr-xl-3,[dir="rtl"] .mr-xl-3{margin-right:0 !important;margin-left:1rem !important}.rtl .ml-xl-3,[dir="rtl"] .ml-xl-3{margin-left:0 !important;margin-right:1rem !important}.rtl mx-xl-3,[dir="rtl"] mx-xl-3{margin-left:1rem !important;margin-right:1rem !important}.rtl .mr-xl-4,[dir="rtl"] .mr-xl-4{margin-right:0 !important;margin-left:1.5rem !important}.rtl .ml-xl-4,[dir="rtl"] .ml-xl-4{margin-left:0 !important;margin-right:1.5rem !important}.rtl mx-xl-4,[dir="rtl"] mx-xl-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.rtl .mr-xl-5,[dir="rtl"] .mr-xl-5{margin-right:0 !important;margin-left:3rem !important}.rtl .ml-xl-5,[dir="rtl"] .ml-xl-5{margin-left:0 !important;margin-right:3rem !important}.rtl mx-xl-5,[dir="rtl"] mx-xl-5{margin-left:3rem !important;margin-right:3rem !important}.rtl .pr-xl-0,[dir="rtl"] .pr-xl-0{padding-right:0 !important;padding-left:0 !important}.rtl .pl-xl-0,[dir="rtl"] .pl-xl-0{padding-left:0 !important;padding-right:0 !important}.rtl px-xl-0,[dir="rtl"] px-xl-0{padding-left:0 !important;padding-right:0 !important}.rtl .pr-xl-1,[dir="rtl"] .pr-xl-1{padding-right:0 !important;padding-left:0.25rem !important}.rtl .pl-xl-1,[dir="rtl"] .pl-xl-1{padding-left:0 !important;padding-right:0.25rem !important}.rtl px-xl-1,[dir="rtl"] px-xl-1{padding-left:0.25rem !important;padding-right:0.25rem !important}.rtl .pr-xl-2,[dir="rtl"] .pr-xl-2{padding-right:0 !important;padding-left:0.5rem !important}.rtl .pl-xl-2,[dir="rtl"] .pl-xl-2{padding-left:0 !important;padding-right:0.5rem !important}.rtl px-xl-2,[dir="rtl"] px-xl-2{padding-left:0.5rem !important;padding-right:0.5rem !important}.rtl .pr-xl-3,[dir="rtl"] .pr-xl-3{padding-right:0 !important;padding-left:1rem !important}.rtl .pl-xl-3,[dir="rtl"] .pl-xl-3{padding-left:0 !important;padding-right:1rem !important}.rtl px-xl-3,[dir="rtl"] px-xl-3{padding-left:1rem !important;padding-right:1rem !important}.rtl .pr-xl-4,[dir="rtl"] .pr-xl-4{padding-right:0 !important;padding-left:1.5rem !important}.rtl .pl-xl-4,[dir="rtl"] .pl-xl-4{padding-left:0 !important;padding-right:1.5rem !important}.rtl px-xl-4,[dir="rtl"] px-xl-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.rtl .pr-xl-5,[dir="rtl"] .pr-xl-5{padding-right:0 !important;padding-left:3rem !important}.rtl .pl-xl-5,[dir="rtl"] .pl-xl-5{padding-left:0 !important;padding-right:3rem !important}.rtl px-xl-5,[dir="rtl"] px-xl-5{padding-left:3rem !important;padding-right:3rem !important}.rtl .mr-xl-auto,[dir="rtl"] .mr-xl-auto{margin-right:0 !important;margin-left:auto !important}.rtl .ml-xl-auto,[dir="rtl"] .ml-xl-auto{margin-right:auto !important;margin-left:0 !important}.rtl .mx-xl-auto,[dir="rtl"] .mx-xl-auto{margin-right:auto !important;margin-left:auto !important}}.rtl .text-right,[dir="rtl"] .text-right{text-align:left !important}.rtl .text-left,[dir="rtl"] .text-left{text-align:right !important}@media (min-width: 576px){.rtl .text-sm-right,[dir="rtl"] .text-sm-right{text-align:left !important}.rtl .text-sm-left,[dir="rtl"] .text-sm-left{text-align:right !important}}@media (min-width: 768px){.rtl .text-md-right,[dir="rtl"] .text-md-right{text-align:left !important}.rtl .text-md-left,[dir="rtl"] .text-md-left{text-align:right !important}}@media (min-width: 992px){.rtl .text-lg-right,[dir="rtl"] .text-lg-right{text-align:left !important}.rtl .text-lg-left,[dir="rtl"] .text-lg-left{text-align:right !important}}@media (min-width: 1200px){.rtl .text-xl-right,[dir="rtl"] .text-xl-right{text-align:left !important}.rtl .text-xl-left,[dir="rtl"] .text-xl-left{text-align:right !important}} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/ng-package.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/ng-package.json new file mode 100644 index 0000000000..962abb2d5f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/theme-shared/testing", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/config.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/config.ts new file mode 100644 index 0000000000..1b4825cf9f --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/config.ts @@ -0,0 +1,5 @@ +import { Validation } from '@ngx-validate/core'; + +export interface Config { + validation?: Partial; +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/index.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/index.ts new file mode 100644 index 0000000000..f03c2281a9 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/models/index.ts @@ -0,0 +1 @@ +export * from './config'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/theme-shared-testing.module.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/theme-shared-testing.module.ts new file mode 100644 index 0000000000..52abb12ffe --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/lib/theme-shared-testing.module.ts @@ -0,0 +1,52 @@ +import { + BaseThemeSharedModule, + DateParserFormatter, + DEFAULT_VALIDATION_BLUEPRINTS, + THEME_SHARED_ROUTE_PROVIDERS, +} from '@abp/ng.theme.shared'; +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap'; +import { + defaultMapErrorsFn, + VALIDATION_BLUEPRINTS, + VALIDATION_MAP_ERRORS_FN, + VALIDATION_VALIDATE_ON_SUBMIT, +} from '@ngx-validate/core'; +import { Config } from './models/config'; + +/** + * ThemeSharedTestingModule is the module that will be used in tests + */ +@NgModule({ + exports: [RouterTestingModule, BaseThemeSharedModule], + imports: [RouterTestingModule, BaseThemeSharedModule], +}) +export class ThemeSharedTestingModule { + static withConfig( + { validation = {} } = {} as Config, + ): ModuleWithProviders { + return { + ngModule: ThemeSharedTestingModule, + providers: [ + THEME_SHARED_ROUTE_PROVIDERS, + { provide: NgbDateParserFormatter, useClass: DateParserFormatter }, + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, + ...(validation.blueprints || {}), + }, + }, + { + provide: VALIDATION_MAP_ERRORS_FN, + useValue: validation.mapErrorsFn || defaultMapErrorsFn, + }, + { + provide: VALIDATION_VALIDATE_ON_SUBMIT, + useValue: validation.validateOnSubmit, + }, + ], + }; + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/public-api.ts b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/public-api.ts new file mode 100644 index 0000000000..14355a8a28 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/testing/src/public-api.ts @@ -0,0 +1 @@ +export * from './lib/theme-shared-testing.module'; diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.json new file mode 100644 index 0000000000..692cc17b32 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "allowSyntheticDefaultImports": true, + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.json new file mode 100644 index 0000000000..bbcc12b1c7 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.prod.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.prod.json new file mode 100644 index 0000000000..331d0ecb05 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.lib.prod.json @@ -0,0 +1,11 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial", + "enableIvy": false + } +} diff --git a/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.spec.json b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.spec.json new file mode 100644 index 0000000000..cfff29a544 --- /dev/null +++ b/npm/ng-packs/nx/ng-packs/libs/theme-shared/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/npm/ng-packs/nx/ng-packs/nx.json b/npm/ng-packs/nx/ng-packs/nx.json index f0d3529b9c..dbb5e5a85c 100644 --- a/npm/ng-packs/nx/ng-packs/nx.json +++ b/npm/ng-packs/nx/ng-packs/nx.json @@ -36,6 +36,12 @@ "dev-app-e2e": { "tags": [], "implicitDependencies": ["dev-app"] + }, + "theme-basic": { + "tags": [] + }, + "theme-shared": { + "tags": [] } } } diff --git a/npm/ng-packs/nx/ng-packs/package.json b/npm/ng-packs/nx/ng-packs/package.json index 28f5ee18dc..2c7fbd413b 100644 --- a/npm/ng-packs/nx/ng-packs/package.json +++ b/npm/ng-packs/nx/ng-packs/package.json @@ -39,22 +39,23 @@ "@abp/ng.theme.basic": "~4.4.0", "@abp/ng.theme.shared": "~4.4.0", "@abp/utils": "^4.4.0", - "@angular-devkit/build-angular": "~12.1.0", + "@angular-devkit/build-angular": "~12.2.0", + "@angular-devkit/build-ng-packagr": "^0.1002.0", "@angular-devkit/schematics-cli": "~12.2.0", - "@angular-eslint/eslint-plugin-template": "~12.3.0", "@angular-eslint/eslint-plugin": "~12.3.0", + "@angular-eslint/eslint-plugin-template": "~12.3.0", "@angular-eslint/template-parser": "~12.3.0", "@angular/animations": "~12.2.0", - "@angular/cli": "~12.1.0", + "@angular/cli": "~12.2.0", "@angular/common": "~12.2.0", - "@angular/compiler-cli": "^12.1.0", "@angular/compiler": "~12.2.0", + "@angular/compiler-cli": "^12.2.0", "@angular/core": "~12.2.0", "@angular/forms": "~12.2.0", - "@angular/language-service": "^12.1.0", + "@angular/language-service": "^12.2.0", "@angular/localize": "~12.2.0", - "@angular/platform-browser-dynamic": "~12.2.0", "@angular/platform-browser": "~12.2.0", + "@angular/platform-browser-dynamic": "~12.2.0", "@angular/router": "~12.2.0", "@fortawesome/fontawesome-free": "^5.14.0", "@ng-bootstrap/ng-bootstrap": "^7.0.0", @@ -80,17 +81,17 @@ "chart.js": "^2.9.3", "cypress": "^7.3.0", "dotenv": "~10.0.0", + "eslint": "7.22.0", "eslint-config-prettier": "8.1.0", "eslint-plugin-cypress": "^2.10.3", - "eslint": "7.22.0", "got": "^11.5.2", - "jest-preset-angular": "9.0.4", "jest": "27.0.3", + "jest-preset-angular": "9.0.4", "jsonc-parser": "^2.3.0", "just-clone": "^3.1.0", "just-compare": "^1.3.0", "lerna": "^3.19.0", - "ng-packagr": "^12.1.0", + "ng-packagr": "^12.2.0", "ng-zorro-antd": "^11.0.1", "ngxs-schematic": "^1.1.9", "prettier": "^2.3.1", diff --git a/npm/ng-packs/nx/ng-packs/tsconfig.base.json b/npm/ng-packs/nx/ng-packs/tsconfig.base.json index 39234d3ed6..ebd57b3a50 100644 --- a/npm/ng-packs/nx/ng-packs/tsconfig.base.json +++ b/npm/ng-packs/nx/ng-packs/tsconfig.base.json @@ -15,7 +15,10 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@ng-packs/core": ["libs/core/src/index.ts"] + // "@abp/ng.core": ["libs/core/src/public-api.ts"], + // "@abp/ng.theme.shared": ["libs/theme-shared/src/public-api.ts"] + "@abp/ng.core": ["dist/libs/core"], + "@abp/ng.theme.shared": ["dist/libs/theme-shared"] } }, "exclude": ["node_modules", "tmp"] diff --git a/npm/ng-packs/nx/ng-packs/yarn.lock b/npm/ng-packs/nx/ng-packs/yarn.lock index d0e453948e..d36ec011b8 100644 --- a/npm/ng-packs/nx/ng-packs/yarn.lock +++ b/npm/ng-packs/nx/ng-packs/yarn.lock @@ -117,6 +117,14 @@ dependencies: just-compare "^1.3.0" +"@angular-devkit/architect@0.1002.0": + version "0.1002.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1002.0.tgz#470b78aaf79308a23da6a0d3935f2d1f85dcb212" + integrity sha512-twM8V03ujBIGVpgV1PBlSDodUdxtUb7WakutfWafAvEHUsgwzfvQz2VtKWvjNZ9AiYjnCuwkQaclqVv0VHNo9w== + dependencies: + "@angular-devkit/core" "10.2.0" + rxjs "6.6.2" + "@angular-devkit/architect@0.1201.4": version "0.1201.4" resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1201.4.tgz#bdd4ed088845bc5412ac8ab9dd4355953f09ea70" @@ -200,6 +208,14 @@ webpack-merge "5.8.0" webpack-subresource-integrity "1.5.2" +"@angular-devkit/build-ng-packagr@^0.1002.0": + version "0.1002.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-ng-packagr/-/build-ng-packagr-0.1002.0.tgz#07fd264f98f0084c2897925e094cb9bd002d34e8" + integrity sha512-JngcuLuPpu2oOhi1qBPzco+ETjDbv90zyWZELO3qXeCelug1z83E0Qi2sLgnNYayzSHfrivrRamL8CbYYT+M9Q== + dependencies: + "@angular-devkit/architect" "0.1002.0" + rxjs "6.6.2" + "@angular-devkit/build-optimizer@0.1201.4": version "0.1201.4" resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1201.4.tgz#79bf972b5905d7d193c838c496febfff6923ec2c" @@ -217,6 +233,17 @@ "@angular-devkit/architect" "0.1201.4" rxjs "6.6.7" +"@angular-devkit/core@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-10.2.0.tgz#fcde160afc2786d2da0166526f065c6cf98684c0" + integrity sha512-XAszFhSF3mZw1VjoOsYGbArr5NJLcStjOvcCGjBPl1UBM2AKpuCQXHxI9XJGYKL3B93Vp5G58d8qkHvamT53OA== + dependencies: + ajv "6.12.4" + fast-json-stable-stringify "2.1.0" + magic-string "0.25.7" + rxjs "6.6.2" + source-map "0.7.3" + "@angular-devkit/core@11.0.7", "@angular-devkit/core@~11.0.2": version "11.0.7" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.0.7.tgz#05b82be7e11ddc2a7836c777297ee45eee20ff55" @@ -333,7 +360,7 @@ dependencies: eslint-scope "^5.1.0" -"@angular/animations@~12.2.0": +"@angular/animations@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-12.2.0.tgz#85e4e738bd72011067836b63bb8fc51a44646a7e" integrity sha512-BCta/HT0VaM0hzsyRyb24UIaV7vlpo2jJjSwnBhO5RosH+bAaA3Faa3ps3AjCDCEXo9z99dvxJFm8WqdunPYyw== @@ -374,7 +401,7 @@ symbol-observable "4.0.0" uuid "8.3.2" -"@angular/common@~12.2.0": +"@angular/common@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/common/-/common-12.2.0.tgz#b8bb889f310ae2af359120e110634cf356a6d142" integrity sha512-9OaqH5WhfJ/YpN9b4TmiGHdOar/3NiGjktwG1/5jV4mTO+tOP25yNO4zCd4RuaRURipWUEa8mSaesgUgTb3jmQ== @@ -401,21 +428,21 @@ tslib "^2.2.0" yargs "^17.0.0" -"@angular/compiler@~12.2.0": +"@angular/compiler@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-12.2.0.tgz#d1585f7b7a67b8393eef2aa4cd3ef47c9d7fc3c0" integrity sha512-24G1joE699gbAHRAIESVQBI797Q506MZ/LWyFHRjfdlfXgltDN63j5dpNJzb/rUK1/uHGGAJaEH+OE60ZK2q9Q== dependencies: tslib "^2.2.0" -"@angular/core@~12.2.0": +"@angular/core@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/core/-/core-12.2.0.tgz#29869aec3f624f6d4e1b5eedc25475cd5235a2e8" integrity sha512-zy/gqSlSEmi85HVbpoQ0nhC74xoL+aoofvbkKPgdgyEcnqzBt4IFzH++0EZ+GMeZ2pVoNfiXklUhAy2OBAiR/g== dependencies: tslib "^2.2.0" -"@angular/forms@~12.2.0": +"@angular/forms@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-12.2.0.tgz#ae43c044404b7cac3595ea2500abb2368889b6ef" integrity sha512-lhKin5IHElH5qk2Su/o0YMF9JYb84grz0WMIHYmt3EASw/EGvzZwUBd76anJWWwFig6GLw/ZdNC9zLW2Leh06Q== @@ -445,21 +472,21 @@ glob "7.1.7" yargs "^17.0.0" -"@angular/platform-browser-dynamic@~12.2.0": +"@angular/platform-browser-dynamic@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.2.0.tgz#d6d882129b5a0cf201c50f0839e5b434e822f0b0" integrity sha512-mvUo1WEI8PrJxIer0RLXSW9raYDrKarUQ9T2Q0/R3ZgS6Y1WvoJe+qSZ7K/hCbErH4zd+64TuJHlj+g2WHPLug== dependencies: tslib "^2.2.0" -"@angular/platform-browser@~12.2.0": +"@angular/platform-browser@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-12.2.0.tgz#d3d01602e331ace3752111e7e64efc9c4a10e561" integrity sha512-MIoWBhhMmPBFfuOPBgEG21uqCZVudTj4YMGxQH4iVlI9GD9ZAht8XgopN9Vw4cytPNfKWsspLoruhIWRNSJlNA== dependencies: tslib "^2.2.0" -"@angular/router@~12.2.0": +"@angular/router@^12.1.0": version "12.2.0" resolved "https://registry.yarnpkg.com/@angular/router/-/router-12.2.0.tgz#b833706e2d935da9dbc828e280f0f566458531bc" integrity sha512-C5S0bU8KtU/2wBQ76AZVFivWbGYJCDwkVg2Lk4ddjqs/pKWb0vg2Baa3DfJrIz3esqfYUJl00HMYQVM9+QtZQA== @@ -770,16 +797,7 @@ "@babel/traverse" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/helpers@^7.14.6", "@babel/helpers@^7.14.8": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.8.tgz#839f88f463025886cff7f85a35297007e2da1b77" - integrity sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw== - dependencies: - "@babel/template" "^7.14.5" - "@babel/traverse" "^7.14.8" - "@babel/types" "^7.14.8" - -"@babel/helpers@^7.8.3": +"@babel/helpers@^7.14.6", "@babel/helpers@^7.14.8", "@babel/helpers@^7.8.3": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.15.3.tgz#c96838b752b95dcd525b4e741ed40bb1dc2a1357" integrity sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g== @@ -797,12 +815,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.15.0", "@babel/parser@^7.7.2": - version "7.15.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.2.tgz#08d4ffcf90d211bf77e7cc7154c6f02d468d2b1d" - integrity sha512-bMJXql1Ss8lFnvr11TZDH4ArtwlAS5NG9qBmdiFW2UHHm6MVoR+GDc5XE2b9K938cyjc9O6/+vjjcffLDtfuDg== - -"@babel/parser@^7.8.3": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.14.6", "@babel/parser@^7.15.0", "@babel/parser@^7.7.2", "@babel/parser@^7.8.3": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862" integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA== @@ -1096,9 +1109,9 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-transform-block-scoping@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.5.tgz#8cc63e61e50f42e078e6f09be775a75f23ef9939" - integrity sha512-LBYm4ZocNgoCqyxMLoOnwpsmQ18HWTQvql64t3GvMUzLQrNoV1BDG0lNftC8QKYERkZgCCT/7J5xWGObGAyHDw== + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz#94c81a6e2fc230bcce6ef537ac96a1e4d2b3afaf" + integrity sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q== dependencies: "@babel/helper-plugin-utils" "^7.14.5" @@ -1502,9 +1515,9 @@ esutils "^2.0.2" "@babel/runtime-corejs3@^7.10.2": - version "7.14.9" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.14.9.tgz#fb21b1cf11650dcb8fcf4de2e6b3b8cf411da3f3" - integrity sha512-64RiH2ON4/y8qYtoa8rUiyam/tUVyGqRyNYhe+vCRGmjnV4bUlZvY+mwd0RrmLoCpJpdq3RsrNqKb7SJdw/4kw== + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz#28754263988198f2a928c09733ade2fb4d28089d" + integrity sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A== dependencies: core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" @@ -1516,14 +1529,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.8.4": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" - integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.10.3": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.8.4": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== @@ -1539,7 +1545,7 @@ "@babel/parser" "^7.14.5" "@babel/types" "^7.14.5" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8", "@babel/traverse@^7.15.0", "@babel/traverse@^7.7.2", "@babel/traverse@^7.8.3": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.15.0", "@babel/traverse@^7.7.2", "@babel/traverse@^7.8.3": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98" integrity sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw== @@ -3958,6 +3964,16 @@ ajv@6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@6.12.4: + version "6.12.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" + integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@6.12.6, ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -5297,9 +5313,9 @@ colord@^2.0.1: integrity sha512-8yMrtE20ZxH1YWvvSoeJFtvqY+GIAOfU+mZ3jx7ZSiEMasnAmNqD1BKUP3CuCWcy/XHgcXkLW6YU8C35nhOYVg== colorette@^1.2.1, colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + version "1.3.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" + integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== colors@^1.1.2: version "1.4.0" @@ -6466,9 +6482,9 @@ ejs@^3.1.5: jake "^10.6.1" electron-to-chromium@^1.3.793: - version "1.3.801" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.801.tgz#f41c588e408ad1a4f794f91f38aa94a89c492f51" - integrity sha512-xapG8ekC+IAHtJrGBMQSImNuN+dm+zl7UP1YbhvTkwQn8zf/yYuoxfTSAEiJ9VDD+kjvXaAhNDPSxJ+VImtAJA== + version "1.3.802" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.802.tgz#0afa989321de3e904ac653ee79e0d642883731a1" + integrity sha512-dXB0SGSypfm3iEDxrb5n/IVKeX4uuTnFHdve7v+yKJqNpEP0D4mjFJ8e1znmSR+OOVlVC+kDO6f2kAkTFXvJBg== elliptic@^6.5.3: version "6.5.4" @@ -10718,9 +10734,9 @@ node-modules-regexp@^1.0.0: integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-releases@^1.1.73: - version "1.1.73" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" - integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== + version "1.1.74" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.74.tgz#e5866488080ebaa70a93b91144ccde06f3c3463e" + integrity sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw== node-sass-tilde-importer@^1.0.2: version "1.0.2" @@ -13073,6 +13089,13 @@ rxjs@6.4.0: dependencies: tslib "^1.9.0" +rxjs@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" + integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== + dependencies: + tslib "^1.9.0" + rxjs@6.6.3: version "6.6.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" @@ -15212,9 +15235,9 @@ webpack@5.44.0: webpack-sources "^2.3.0" "webpack@^4.0.0 || ^5.30.0": - version "5.49.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.49.0.tgz#e250362b781a9fb614ba0a97ed67c66b9c5310cd" - integrity sha512-XarsANVf28A7Q3KPxSnX80EkCcuOer5hTOEJWJNvbskOZ+EK3pobHarGHceyUZMxpsTHBHhlV7hiQyLZzGosYw== + version "5.50.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.50.0.tgz#5562d75902a749eb4d75131f5627eac3a3192527" + integrity sha512-hqxI7t/KVygs0WRv/kTgUW8Kl3YC81uyWQSo/7WUs5LsuRw0htH/fCwbVBGCuiX/t4s7qzjXFcf41O8Reiypag== dependencies: "@types/eslint-scope" "^3.7.0" "@types/estree" "^0.0.50" diff --git a/npm/ng-packs/yarn.lock b/npm/ng-packs/yarn.lock index 395d999edc..cdcb211672 100644 --- a/npm/ng-packs/yarn.lock +++ b/npm/ng-packs/yarn.lock @@ -2632,9 +2632,9 @@ universal-user-agent "^6.0.0" "@octokit/openapi-types@^9.5.0": - version "9.6.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.6.0.tgz#0260a84f5c61c01755402ab5ac5b1ab46e7c0730" - integrity sha512-L+8x7DpcNtHkMbTxxCxg3cozvHUNP46rOIzFwoMs0piWwQzAGNXqlIQO2GLvnKTWLUh99DkY+UyHVrP4jXlowg== + version "9.7.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2" + integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" @@ -2680,9 +2680,9 @@ once "^1.4.0" "@octokit/request@^5.2.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.0.tgz#6084861b6e4fa21dc40c8e2a739ec5eff597e672" - integrity sha512-4cPp/N+NqmaGQwbh3vUsYqokQIzt7VjsgTYVXiwpUP2pxd5YiZB2XuTedbb0SPtv9XS7nzAKjAuQxmY8/aZkiA== + version "5.6.1" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.1.tgz#f97aff075c37ab1d427c49082fefeef0dba2d8ce" + integrity sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.1.0" @@ -2996,9 +2996,9 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@*", "@types/node@>= 8": - version "16.4.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.13.tgz#7dfd9c14661edc65cccd43a29eb454174642370d" - integrity sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg== + version "16.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.0.tgz#0d5685f85066f94e97f19e8a67fe003c5fadacc4" + integrity sha512-OyiZPohMMjZEYqcVo/UJ04GyAxXOJEZO/FpzyXxcH4r/ArrVoXHf4MbUrkLp0Tz7/p1mMKpo5zJ6ZHl8XBNthQ== "@types/node@^12.11.1": version "12.20.19" @@ -4396,9 +4396,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001243, caniuse-lite@^1.0.30001248: - version "1.0.30001249" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz#90a330057f8ff75bfe97a94d047d5e14fabb2ee8" - integrity sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw== + version "1.0.30001251" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85" + integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A== canonical-path@1.0.0: version "1.0.0" @@ -5994,9 +5994,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.793: - version "1.3.802" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.802.tgz#0afa989321de3e904ac653ee79e0d642883731a1" - integrity sha512-dXB0SGSypfm3iEDxrb5n/IVKeX4uuTnFHdve7v+yKJqNpEP0D4mjFJ8e1znmSR+OOVlVC+kDO6f2kAkTFXvJBg== + version "1.3.803" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.803.tgz#78993a991d096500f21a77e91cd2a44295fe3cbe" + integrity sha512-tmRK9qB8Zs8eLMtTBp+w2zVS9MUe62gQQQHjYdAc5Zljam3ZIokNb+vZLPRz9RCREp6EFRwyhOFwbt1fEriQ2Q== elliptic@^6.5.3: version "6.5.4" @@ -6550,9 +6550,9 @@ figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== figlet@^1.2.3: - version "1.5.0" - resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.0.tgz#2db4d00a584e5155a96080632db919213c3e003c" - integrity sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww== + version "1.5.2" + resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.2.tgz#dda34ff233c9a48e36fcff6741aeb5bafe49b634" + integrity sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ== figures@^2.0.0: version "2.0.0" @@ -7820,9 +7820,11 @@ is-arrayish@^0.3.1: integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-bigint@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.3.tgz#fc9d9e364210480675653ddaea0518528d49a581" - integrity sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" is-binary-path@^1.0.0: version "1.0.1" @@ -9833,9 +9835,9 @@ nan@^2.12.1: integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanoid@^3.1.20, nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== + version "3.1.24" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.24.tgz#d7ac20215f595c26d314ee5671169a27b609025f" + integrity sha512-WNhqqgD4qH7TQdU9ujXfFa/hQI5rOGGnZq+JRmz4JwMZFCgSZVquTq3ORUSv6IC+Y41ACBYV8a8J1kPkqGIiQg== nanomatch@^1.2.9: version "1.2.13" @@ -13363,9 +13365,9 @@ tapable@^2.2.0: integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: - version "4.4.16" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.16.tgz#4a48b3c025e77d9d0c788f038a09b91c594d326d" - integrity sha512-gOVUT/KWPkGFZQmCRDVFNUWBl7niIo/PRR7lzrIqtZpit+st54lGROuVjc6zEQM9FhH+dJfQIl+9F0k8GNXg5g== + version "4.4.17" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.17.tgz#44be5e3fa8353ee1d11db3b1401561223a5c3985" + integrity sha512-q7OwXq6NTdcYIa+k58nEMV3j1euhDhGCs/VRw9ymx/PbH0jtIM2+VTgDE/BW3rbLkrBUXs5fzEKgic5oUciu7g== dependencies: chownr "^1.1.4" fs-minipass "^1.2.7" @@ -13376,9 +13378,9 @@ tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: yallist "^3.1.1" tar@^6.0.2, tar@^6.1.0: - version "6.1.7" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.7.tgz#c566d1107d38b09e92983a68db5534fc7f6cab42" - integrity sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA== + version "6.1.8" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.8.tgz#4fc50cfe56511c538ce15b71e05eebe66530cbd4" + integrity sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -13736,9 +13738,9 @@ tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" - integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== tslint@~6.1.0: version "6.1.3"