From b7438ef155cbcf146b14d20dacff9364ccbc407b Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Wed, 27 Aug 2025 22:57:49 +0700 Subject: [PATCH] refactor: refactor forms components and remove legacy Taiga UI package (#3012) --- web/package-lock.json | 204 ++++++++-------- web/package.json | 21 +- web/projects/shared/styles/taiga.scss | 13 +- web/projects/ui/src/app/app.module.ts | 4 +- web/projects/ui/src/app/app.providers.ts | 6 +- .../portal/components/form.component.ts | 6 +- .../form/containers/array.component.ts | 230 ++++++++++++++++++ .../form/containers/control.component.ts | 154 ++++++++++++ .../form/containers/control.directive.ts | 45 ++++ .../form/containers/group.component.ts | 119 +++++++++ .../form/containers/object.component.ts | 118 +++++++++ .../union.component.ts} | 46 +++- .../components/form/control.directive.ts | 31 --- .../routes/portal/components/form/control.ts | 45 ---- .../form/controls/color.component.ts | 46 ++++ .../components/form/controls/control.ts | 41 ++++ .../components/form/controls/controls.ts | 22 ++ .../form/controls/datetime.component.ts | 144 +++++++++++ .../form/controls/file.component.ts | 118 +++++++++ .../multiselect.component.ts} | 46 +++- .../form/controls/number.component.ts | 54 ++++ .../form/controls/select.component.ts | 80 ++++++ .../form/controls/text.component.ts | 104 ++++++++ .../form/controls/textarea.component.ts | 52 ++++ .../form/controls/toggle.component.ts | 33 +++ .../form/form-array/form-array.component.html | 57 ----- .../form/form-array/form-array.component.scss | 50 ---- .../form/form-array/form-array.component.ts | 89 ------- .../form/form-color/form-color.component.html | 30 --- .../form/form-color/form-color.component.scss | 33 --- .../form/form-color/form-color.component.ts | 16 -- .../form-control/form-control.component.html | 58 ----- .../form-control/form-control.component.scss | 11 - .../form-control/form-control.component.ts | 72 ------ .../form-control/form-control.providers.ts | 31 --- .../form-datetime.component.html | 54 ---- .../form-datetime/form-datetime.component.ts | 37 --- .../form/form-file/form-file.component.html | 42 ---- .../form/form-file/form-file.component.scss | 47 ---- .../form/form-file/form-file.component.ts | 15 -- .../form/form-group/form-group.component.html | 30 --- .../form/form-group/form-group.component.scss | 35 --- .../form/form-group/form-group.component.ts | 36 --- .../form/form-group/form-group.providers.ts | 31 --- .../form-multiselect.component.html | 18 -- .../form-number/form-number.component.html | 22 -- .../form/form-number/form-number.component.ts | 12 - .../form-object/form-object.component.html | 23 -- .../form-object/form-object.component.scss | 41 ---- .../form/form-object/form-object.component.ts | 39 --- .../form-select/form-select.component.html | 17 -- .../form/form-select/form-select.component.ts | 32 --- .../form/form-text/form-text.component.html | 48 ---- .../form/form-text/form-text.component.scss | 8 - .../form/form-text/form-text.component.ts | 17 -- .../form-textarea.component.html | 20 -- .../form-textarea/form-textarea.component.ts | 13 - .../form-toggle/form-toggle.component.html | 13 - .../form/form-toggle/form-toggle.component.ts | 14 -- .../form/form-union/form-union.component.html | 11 - .../form/form-union/form-union.component.scss | 8 - .../portal/components/form/form.module.ts | 110 --------- .../portal/components/form/invalid.service.ts | 19 -- .../form/{ => pipes}/filter-hidden.pipe.ts | 1 - .../components/form/{ => pipes}/hint.pipe.ts | 1 - .../form/{ => pipes}/mustache.pipe.ts | 1 - .../services/modals/action-input.component.ts | 2 +- .../routes/system/routes/dns/dns.component.ts | 4 +- .../system/routes/email/email.component.ts | 4 +- 69 files changed, 1568 insertions(+), 1486 deletions(-) create mode 100644 web/projects/ui/src/app/routes/portal/components/form/containers/array.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/containers/control.directive.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/containers/group.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/containers/object.component.ts rename web/projects/ui/src/app/routes/portal/components/form/{form-union/form-union.component.ts => containers/union.component.ts} (58%) delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/control.directive.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/control.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/color.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/control.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/controls.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/datetime.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/file.component.ts rename web/projects/ui/src/app/routes/portal/components/form/{form-multiselect/form-multiselect.component.ts => controls/multiselect.component.ts} (57%) create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/text.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/textarea.component.ts create mode 100644 web/projects/ui/src/app/routes/portal/components/form/controls/toggle.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.providers.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.providers.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.html delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.scss delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/form.module.ts delete mode 100644 web/projects/ui/src/app/routes/portal/components/form/invalid.service.ts rename web/projects/ui/src/app/routes/portal/components/form/{ => pipes}/filter-hidden.pipe.ts (95%) rename web/projects/ui/src/app/routes/portal/components/form/{ => pipes}/hint.pipe.ts (96%) rename web/projects/ui/src/app/routes/portal/components/form/{ => pipes}/mustache.pipe.ts (93%) diff --git a/web/package-lock.json b/web/package-lock.json index 8eacd88cf..0f788543a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -25,19 +25,18 @@ "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.3.0", "@start9labs/start-sdk": "file:../sdk/baseDist", - "@taiga-ui/addon-charts": "4.48.0", - "@taiga-ui/addon-commerce": "4.48.0", - "@taiga-ui/addon-mobile": "4.48.0", - "@taiga-ui/addon-table": "4.48.0", - "@taiga-ui/cdk": "4.48.0", - "@taiga-ui/core": "4.48.0", + "@taiga-ui/addon-charts": "4.51.0", + "@taiga-ui/addon-commerce": "4.51.0", + "@taiga-ui/addon-mobile": "4.51.0", + "@taiga-ui/addon-table": "4.51.0", + "@taiga-ui/cdk": "4.51.0", + "@taiga-ui/core": "4.51.0", "@taiga-ui/dompurify": "4.1.11", "@taiga-ui/event-plugins": "4.6.0", - "@taiga-ui/experimental": "4.48.0", - "@taiga-ui/icons": "4.48.0", - "@taiga-ui/kit": "4.48.0", - "@taiga-ui/layout": "4.48.0", - "@taiga-ui/legacy": "4.48.0", + "@taiga-ui/experimental": "4.51.0", + "@taiga-ui/icons": "4.51.0", + "@taiga-ui/kit": "4.51.0", + "@taiga-ui/layout": "4.51.0", "@taiga-ui/polymorpheus": "4.9.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -2979,9 +2978,9 @@ ] }, "node_modules/@maskito/angular": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.10.2.tgz", - "integrity": "sha512-+CQ7KQGmu35THj/59Uex+GotMFzdLHFUlPj5X5qphl+tHX09atmRzx7SEUCSEErbftTLafAFeR5N5t1fVTJvmw==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.10.3.tgz", + "integrity": "sha512-Wu64iLuuMZH/3fXgQSj15i/XRDcGdxIYY1eoq+zEUX0JkN+f1DLYzS4QVUMz/APNb7mnpnmNP0omr0feEWj+Kg==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -2990,35 +2989,35 @@ "peerDependencies": { "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", - "@maskito/core": "^3.10.2" + "@maskito/core": "^3.10.3" } }, "node_modules/@maskito/core": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.10.2.tgz", - "integrity": "sha512-LKh/PrG5wtMQ4AFYrWkKVGJUQB2CJcIt59qMPhntYIBpjw/OHWboHD4WWWQ94GvkYKjKQyjMcS/zvx+JaDrx2A==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.10.3.tgz", + "integrity": "sha512-4SZeEF6PjDHC+J5ADrJaSrFmgqmGkqfE5Yi6BrNXze9TGvVRy9aHJCizShFvheqCEu6MsK0XprZot28wH9AhjQ==", "license": "Apache-2.0", "peer": true }, "node_modules/@maskito/kit": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.10.2.tgz", - "integrity": "sha512-d0YHheVt+DYZDL+A4uwoF0pF/rofczHz0KKYEuQrSdbKlRxOdyckQrj9iMCsmD73Hwne7LbjLL/rViHL4aFL2Q==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.10.3.tgz", + "integrity": "sha512-4IAL5WPlz4zi6vCMp8KbSAVh67WT+o0PzQ56dU4E7crN1jzBm1cN7MIbGawefOIXwAiqCb8zOSyTv/qqSL0xGQ==", "license": "Apache-2.0", "peer": true, "peerDependencies": { - "@maskito/core": "^3.10.2" + "@maskito/core": "^3.10.3" } }, "node_modules/@maskito/phone": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.10.2.tgz", - "integrity": "sha512-XP/mp7CTHYriy6U+zoIitlJCGCmMr+yxtJ/u5y9+S4H3T1siILU3K8CAqetxpK//8/Zopco8lyz1D7ASKofdRg==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.10.3.tgz", + "integrity": "sha512-xt0WLrzLbxiS+0j5QoR6lBjU+FvtqfUL3BOEAKcopgUa8lrswZr3g6fRy0BCcvrzpf4Jdpj3FsZcBRd6ljiEkg==", "license": "Apache-2.0", "peer": true, "peerDependencies": { - "@maskito/core": "^3.10.2", - "@maskito/kit": "^3.10.2", + "@maskito/core": "^3.10.3", + "@maskito/kit": "^3.10.3", "libphonenumber-js": ">=1.0.0" } }, @@ -4714,9 +4713,9 @@ "link": true }, "node_modules/@taiga-ui/addon-charts": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.48.0.tgz", - "integrity": "sha512-0oEfjhV+B50ITyS5oXnVAzeclSrAVX9FiEvWkX7zJ92uy7PKzkoGx+wEsKw3m1ax0I+cVYrh+rX6VivpX4dBZw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.51.0.tgz", + "integrity": "sha512-SqNlaljenvsRILpugAAOGxqpoNSy/6YZoq0rvM9Zs1BToOPWbiTj6w2lpezWMxgL0m4ciGGN7Bo9kq5CZu+QaQ==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4725,15 +4724,15 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@ng-web-apis/common": "^4.12.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0" } }, "node_modules/@taiga-ui/addon-commerce": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.48.0.tgz", - "integrity": "sha512-IGWBSRlsQmkNQfKFk90N0N7TkPsFBo0pBBuTXeuVGBo9us4AJafUAMnVlS5U77XSL1xK1pGRkazKfLgLz3yMzg==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.51.0.tgz", + "integrity": "sha512-2kGwV4FWZ4k6FoSICBmLfqpwZTdpAHLG79VS9gDv7Qd1vMYXRAHxJIi1phARZjl8EXpeAO6isTq+xpcHMdqT8A==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4742,22 +4741,22 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", - "@maskito/angular": "^3.10.2", - "@maskito/core": "^3.10.2", - "@maskito/kit": "^3.10.2", + "@maskito/angular": "^3.10.3", + "@maskito/core": "^3.10.3", + "@maskito/kit": "^3.10.3", "@ng-web-apis/common": "^4.12.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", - "@taiga-ui/i18n": "^4.48.0", - "@taiga-ui/kit": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", + "@taiga-ui/i18n": "^4.51.0", + "@taiga-ui/kit": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/addon-mobile": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.48.0.tgz", - "integrity": "sha512-aGuCkE0T+EaKSr31R2TYuN1h1STi8iATGlNHX4kZ3+Ab/mebER8Xi7uo5gy9olMOGB65syl5Bo4VL02/wc5HKw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.51.0.tgz", + "integrity": "sha512-oEtIrT0mWdiR0QRe9XUhdu+XjsfjPA4zWDjRU4l7mwCjhBqPjTGK5PchjnMpznpu/Y3/5cRR2+xOuEb8NXFd5w==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4767,18 +4766,18 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@ng-web-apis/common": "^4.12.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", - "@taiga-ui/kit": "^4.48.0", - "@taiga-ui/layout": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", + "@taiga-ui/kit": "^4.51.0", + "@taiga-ui/layout": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/addon-table": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.48.0.tgz", - "integrity": "sha512-omwAOlwxom03jTWECDjSDVTOItHD6ZyiPMB5aY/HI/jjsQIZXDlPJLYLfS0+rBR4mwBWBMCXaLvVPPAPy2U4eA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.51.0.tgz", + "integrity": "sha512-NTt11Yzcjts08qzlTvnFlMG2ANXu0Tk9w6aOnNRpKbZzlkEVGLFdkYazg10RDei1909PnbkF7TgdZ83IM+3c6w==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4787,18 +4786,18 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@ng-web-apis/intersection-observer": "^4.12.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", - "@taiga-ui/i18n": "^4.48.0", - "@taiga-ui/kit": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", + "@taiga-ui/i18n": "^4.51.0", + "@taiga-ui/kit": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.48.0.tgz", - "integrity": "sha512-CJdGnLqOmQsLTXDhliriVpvyjTCZNXtfqMpoBBNQwUdRC+2+0mhhltnmE2FnnyvsKYoFoZ87q1NpKkRqotvstA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.51.0.tgz", + "integrity": "sha512-LSUF12F1u0jQgrrImwc1wqZXGgIdNP39MKwUgEhF4qHuBtJGFJzL643Mw/vhX0Tlw5yo+ecVvr76oBAxMkWLww==", "license": "Apache-2.0", "dependencies": { "tslib": "2.8.1" @@ -4827,9 +4826,9 @@ } }, "node_modules/@taiga-ui/core": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.48.0.tgz", - "integrity": "sha512-PkPN4gS1Wnf1nB1e0D9kB+wc6GMndjyAZvxntduG1UKGyFAl4rohbAJI5Fh5bjm/Gr4mQUUBX1LzeQFDY+ob6Q==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.51.0.tgz", + "integrity": "sha512-S/P7YKfB7hGXDuo3HFYI8GddahMg4tlvO4Owi/P3qFjWw+ifVEbFZNF4A1JPTZUrpq9uIhMj7tPzeF99QRL/GQ==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4843,9 +4842,9 @@ "@angular/router": ">=16.0.0", "@ng-web-apis/common": "^4.12.0", "@ng-web-apis/mutation-observer": "^4.12.0", - "@taiga-ui/cdk": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", "@taiga-ui/event-plugins": "^4.6.0", - "@taiga-ui/i18n": "^4.48.0", + "@taiga-ui/i18n": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } @@ -4880,9 +4879,9 @@ } }, "node_modules/@taiga-ui/experimental": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.48.0.tgz", - "integrity": "sha512-ZKVNos1nbKo5koh34TBX5AsLRqbDoNn4crFKqyXux1MmmrCLgqYxeou7/u3g9bIqC263n+p3urM/9oFC7jllBw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.51.0.tgz", + "integrity": "sha512-ZbiXNEOGy+F0UaOwm+h4eDvOesh0PtybRp6PmC8OOXPGOu9SHtrcWhVwczhoSXLX+1fhHnt+P3YxWW2B+KqK/w==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4890,18 +4889,19 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@taiga-ui/addon-commerce": "^4.48.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", - "@taiga-ui/kit": "^4.48.0", + "@taiga-ui/addon-commerce": "^4.51.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", + "@taiga-ui/kit": "^4.51.0", + "@taiga-ui/layout": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.48.0.tgz", - "integrity": "sha512-E73l8P1YPFSydgDmz0ajn856ee7eDVIJosrgX3vpaAH1m2pICp4PYwZfqCuHwhogk/mKdAtnVZoBaOgr6ybXlg==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.51.0.tgz", + "integrity": "sha512-k0hbvNJZRqhLc538utmek9+p2gqG1ZWMm9F/0D8w00EdD8BDxCli/GIcXDjlxeM7HInHfRrlc9kQOsxpJ2wvoQ==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -4914,18 +4914,18 @@ } }, "node_modules/@taiga-ui/icons": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.48.0.tgz", - "integrity": "sha512-TCWAQ2RshcBwgumk7UayYuDwpNQCwP6bDppsn3yz/JcKH1OagDPcLRy3oV15Gpwvi0AcrnrfE74IkeMdClMQUQ==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.51.0.tgz", + "integrity": "sha512-YPNUxgb9kKtkvpuFRXrQEExHvFfTutgZvs5lMixPC6V5+ttud0UQzDDdqEAxk4e3z7ET0GbwVNebBnP/jo5ERA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" } }, "node_modules/@taiga-ui/kit": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.48.0.tgz", - "integrity": "sha512-OraV1GAZqmBYwqTrsJPGar6d3Vo0keUhCGzd8rUxeL0ZKtRX+vsRRPtKAQP7B8IYPxnkZRQLZuV1XLZqmwEiaw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.51.0.tgz", + "integrity": "sha512-gJ0TixxXUh8QkKXCA5o9pZF2ENJnUIVHpmiWatrKmxkYzlYBMpP99Iw4vXlj7Ax6j1Or84FAen/0s8IoFGSnOQ==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4935,25 +4935,25 @@ "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", "@angular/router": ">=16.0.0", - "@maskito/angular": "^3.10.2", - "@maskito/core": "^3.10.2", - "@maskito/kit": "^3.10.2", - "@maskito/phone": "^3.10.2", + "@maskito/angular": "^3.10.3", + "@maskito/core": "^3.10.3", + "@maskito/kit": "^3.10.3", + "@maskito/phone": "^3.10.3", "@ng-web-apis/common": "^4.12.0", "@ng-web-apis/intersection-observer": "^4.12.0", "@ng-web-apis/mutation-observer": "^4.12.0", "@ng-web-apis/resize-observer": "^4.12.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", - "@taiga-ui/i18n": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", + "@taiga-ui/i18n": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/layout": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.48.0.tgz", - "integrity": "sha512-Q4420HZRv4iIuC5kpGuHzbWR+njBusOjUlpKJ5B6coduw6oXP5zr/R7czZmD110+2jdLj2p4owlc0Rr+8LwNBQ==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.51.0.tgz", + "integrity": "sha512-Y1k8C9/SpRH3a6X+lCF2nZ+SYgKvwNTuBvFCHIGZDsD6ye2yxS939ae4j+QBF8w16FlcEZi4eNPH6P0qMht4fw==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4961,25 +4961,13 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@taiga-ui/cdk": "^4.48.0", - "@taiga-ui/core": "^4.48.0", - "@taiga-ui/kit": "^4.48.0", + "@taiga-ui/cdk": "^4.51.0", + "@taiga-ui/core": "^4.51.0", + "@taiga-ui/kit": "^4.51.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, - "node_modules/@taiga-ui/legacy": { - "version": "4.48.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.48.0.tgz", - "integrity": "sha512-1zv8oHcOYUs4W9T/ihL0b2psdVB7PFdLcZ6wkPBIaD/luVrdAGI1RUMrrtcm9SU6uo9hpqDkcaaymf9hnS6Itw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": ">=2.8.1" - }, - "peerDependencies": { - "@angular/core": ">=16.0.0" - } - }, "node_modules/@taiga-ui/polymorpheus": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.9.0.tgz", @@ -8457,9 +8445,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.10", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz", - "integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==", + "version": "1.12.14", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.14.tgz", + "integrity": "sha512-HBAMAV7f3yGYy7ZZN5FxQ1tXJTwC77G5/96Yn/SH/HPyKX2EMLGFuCIYUmdLU7CxxJlQcvJymP/PGLzyapurhQ==", "license": "MIT", "peer": true }, diff --git a/web/package.json b/web/package.json index da35463a0..72a72a821 100644 --- a/web/package.json +++ b/web/package.json @@ -46,19 +46,18 @@ "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.3.0", "@start9labs/start-sdk": "file:../sdk/baseDist", - "@taiga-ui/addon-charts": "4.48.0", - "@taiga-ui/addon-commerce": "4.48.0", - "@taiga-ui/addon-mobile": "4.48.0", - "@taiga-ui/addon-table": "4.48.0", - "@taiga-ui/cdk": "4.48.0", - "@taiga-ui/core": "4.48.0", + "@taiga-ui/addon-charts": "4.51.0", + "@taiga-ui/addon-commerce": "4.51.0", + "@taiga-ui/addon-mobile": "4.51.0", + "@taiga-ui/addon-table": "4.51.0", + "@taiga-ui/cdk": "4.51.0", + "@taiga-ui/core": "4.51.0", "@taiga-ui/dompurify": "4.1.11", "@taiga-ui/event-plugins": "4.6.0", - "@taiga-ui/experimental": "4.48.0", - "@taiga-ui/icons": "4.48.0", - "@taiga-ui/kit": "4.48.0", - "@taiga-ui/layout": "4.48.0", - "@taiga-ui/legacy": "4.48.0", + "@taiga-ui/experimental": "4.51.0", + "@taiga-ui/icons": "4.51.0", + "@taiga-ui/kit": "4.51.0", + "@taiga-ui/layout": "4.51.0", "@taiga-ui/polymorpheus": "4.9.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", diff --git a/web/projects/shared/styles/taiga.scss b/web/projects/shared/styles/taiga.scss index 4a76c0c24..cb30f47ee 100644 --- a/web/projects/shared/styles/taiga.scss +++ b/web/projects/shared/styles/taiga.scss @@ -88,7 +88,7 @@ --start9-base-5: rgba(60, 62, 64, 1); } -[tuiAppearance][data-appearance^='primary'] { +[tuiAppearance][data-appearance^='primary']:not([tuiCheckbox]._readonly) { @include taiga.appearance-disabled { background: var(--tui-status-neutral); color: #333; @@ -126,11 +126,8 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] { var(--tui-background-elevation-3) 75%, transparent ); - background-image: linear-gradient( - to bottom, - rgba(255, 255, 255, 0.15), - transparent - ), + background-image: + linear-gradient(to bottom, rgba(255, 255, 255, 0.15), transparent), linear-gradient(to bottom, rgba(255, 255, 255, 0.15), transparent); background-size: 1px 100%; background-repeat: no-repeat; @@ -162,6 +159,10 @@ tui-badge-notification { background: var(--tui-status-negative); } +tui-textfield [tuiTooltip] { + display: block !important; +} + [tuiCell] { &[data-height='spacious'] { padding-block: 0.75rem; diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index e96843bcb..fcc00f0e3 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -1,6 +1,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { NgModule } from '@angular/core' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { BrowserModule } from '@angular/platform-browser' import { ServiceWorkerModule } from '@angular/service-worker' import { TuiRoot } from '@taiga-ui/core' import { ToastContainerComponent } from 'src/app/components/toast-container.component' @@ -12,7 +12,7 @@ import { RoutingModule } from './routing.module' @NgModule({ declarations: [AppComponent], imports: [ - BrowserAnimationsModule, + BrowserModule, RoutingModule, ToastContainerComponent, TuiRoot, diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index c7b5a9814..cadf8370d 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -1,5 +1,6 @@ import { inject, provideAppInitializer } from '@angular/core' import { UntypedFormBuilder } from '@angular/forms' +import { provideAnimations } from '@angular/platform-browser/animations' import { Router } from '@angular/router' import { WA_LOCATION } from '@ng-web-apis/common' import initArgon from '@start9labs/argon2' @@ -28,7 +29,6 @@ import { TUI_DATE_TIME_VALUE_TRANSFORMER, TUI_DATE_VALUE_TRANSFORMER, } from '@taiga-ui/kit' -import { tuiTextfieldOptionsProvider } from '@taiga-ui/legacy' import { PatchDB } from 'patch-db-client' import { filter, of, pairwise } from 'rxjs' import { ConfigService } from 'src/app/services/config.service' @@ -37,6 +37,7 @@ import { PatchDbSource, } from 'src/app/services/patch-db/patch-db-source' import { StateService } from 'src/app/services/state.service' +import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe' import { ApiService } from './services/api/embassy-api.service' import { LiveApiService } from './services/api/embassy-live-api.service' import { MockApiService } from './services/api/embassy-mock-api.service' @@ -46,7 +47,6 @@ import { ClientStorageService } from './services/client-storage.service' import { DateTransformerService } from './services/date-transformer.service' import { DatetimeTransformerService } from './services/datetime-transformer.service' import { StorageService } from './services/storage.service' -import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe' const { useMocks, @@ -54,6 +54,7 @@ const { } = require('../../../../config.json') as WorkspaceConfig export const APP_PROVIDERS = [ + provideAnimations(), provideEventPlugins(), I18N_PROVIDERS, FilterPackagesPipe, @@ -61,7 +62,6 @@ export const APP_PROVIDERS = [ UntypedFormBuilder, tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }), tuiButtonOptionsProvider({ size: 'm' }), - tuiTextfieldOptionsProvider({ hintOnDisabled: true }), tuiDropdownOptionsProvider({ appearance: 'start-os' }), tuiAlertOptionsProvider({ autoClose: appearance => (appearance === 'negative' ? 0 : 3000), diff --git a/web/projects/ui/src/app/routes/portal/components/form.component.ts b/web/projects/ui/src/app/routes/portal/components/form.component.ts index c23906f48..eec7e7f2f 100644 --- a/web/projects/ui/src/app/routes/portal/components/form.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form.component.ts @@ -16,8 +16,8 @@ import { TuiButton, TuiDialogContext } from '@taiga-ui/core' import { TuiConfirmService } from '@taiga-ui/kit' import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus' import { Operation } from 'fast-json-patch' -import { FormModule } from 'src/app/routes/portal/components/form/form.module' -import { InvalidService } from 'src/app/routes/portal/components/form/invalid.service' +import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component' +import { InvalidService } from 'src/app/routes/portal/components/form/containers/control.directive' import { FormService } from 'src/app/services/form.service' export interface ActionButton { @@ -88,7 +88,7 @@ export interface FormContext { RouterModule, TuiValueChanges, TuiButton, - FormModule, + FormGroupComponent, ], providers: [InvalidService], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/web/projects/ui/src/app/routes/portal/components/form/containers/array.component.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/array.component.ts new file mode 100644 index 000000000..7a7cdff8a --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/array.component.ts @@ -0,0 +1,230 @@ +import { AsyncPipe } from '@angular/common' +import { + Component, + DestroyRef, + forwardRef, + HostBinding, + inject, + Input, +} from '@angular/core' +import { takeUntilDestroyed } from '@angular/core/rxjs-interop' +import { + AbstractControl, + FormArrayName, + ReactiveFormsModule, +} from '@angular/forms' +import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { + TUI_ANIMATIONS_SPEED, + TuiButton, + TuiError, + tuiFadeIn, + tuiHeightCollapse, + TuiIcon, + TuiLink, + tuiParentStop, + TuiTextfield, + tuiToAnimationOptions, +} from '@taiga-ui/core' +import { TuiFieldErrorPipe, TuiTooltip } from '@taiga-ui/kit' +import { filter } from 'rxjs' +import { FormService } from 'src/app/services/form.service' + +import { HintPipe } from '../pipes/hint.pipe' +import { MustachePipe } from '../pipes/mustache.pipe' +import { ERRORS, FormControlComponent } from './control.component' +import { ControlDirective } from './control.directive' +import { FormObjectComponent } from './object.component' + +@Component({ + selector: 'form-array', + template: ` +
+ {{ spec.name }} + @if (spec.description || spec.disabled) { + + } + +
+ + @for (item of array.control.controls; track item) { + @if (spec.spec.type === 'object') { + + {{ item.value | mustache: $any(spec.spec).displayAs }} + + + } @else { + + } + } + `, + styles: ` + @use '@taiga-ui/core/styles/taiga-ui-local' as taiga; + + :host { + display: block; + margin: 2rem 0; + } + + .label { + display: flex; + font-size: 1.25rem; + font-weight: bold; + } + + .add { + font-size: 1rem; + padding: 0 1rem; + margin-left: auto; + } + + .object { + display: block; + position: relative; + + &_open::after, + &:last-child::after { + opacity: 0; + } + + &::after { + @include taiga.transition(opacity); + + content: ''; + position: absolute; + bottom: -0.5rem; + height: 1px; + left: 3rem; + right: 1rem; + background: var(--tui-background-neutral-1); + } + } + + .remove { + margin: 0 0.375rem 0 auto; + pointer-events: auto; + + &::before { + font-size: 1rem; + } + } + + .control { + display: block; + margin: 0.5rem 0; + } + `, + animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop], + hostDirectives: [ControlDirective], + imports: [ + AsyncPipe, + ReactiveFormsModule, + TuiIcon, + TuiTooltip, + TuiLink, + TuiError, + TuiFieldErrorPipe, + TuiButton, + TuiTextfield, + i18nPipe, + HintPipe, + MustachePipe, + FormControlComponent, + forwardRef(() => FormObjectComponent), + ], +}) +export class FormArrayComponent { + @Input({ required: true }) + spec!: IST.ValueSpecList + + @HostBinding('@tuiParentStop') + readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED)) + readonly order = ERRORS + readonly array = inject(FormArrayName) + readonly open = new Map() + + private warned = false + private readonly formService = inject(FormService) + private readonly destroyRef = inject(DestroyRef) + private readonly dialog = inject(DialogService) + + get canAdd(): boolean { + return ( + !this.spec.disabled && + (!this.spec.maxLength || + this.spec.maxLength >= this.array.control.controls.length) + ) + } + + add() { + if (!this.warned && this.spec.warning) { + this.dialog + .openConfirm({ + label: 'Warning', + size: 's', + data: { + content: this.spec.warning as i18nKey, + yes: 'Ok', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.addItem() + }) + } else { + this.addItem() + } + + this.warned = true + } + + removeAt(index: number) { + this.removeItem(index) + } + + private removeItem(index: number) { + this.open.delete(this.array.control.at(index)) + this.array.control.removeAt(index) + } + + private addItem() { + this.array.control.insert(0, this.formService.getListItem(this.spec)) + this.open.set(this.array.control.at(0), true) + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts new file mode 100644 index 000000000..ca4901a84 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/control.component.ts @@ -0,0 +1,154 @@ +import { AsyncPipe } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + inject, + Input, + TemplateRef, + ViewChild, +} from '@angular/core' +import { takeUntilDestroyed } from '@angular/core/rxjs-interop' +import { i18nPipe } from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { tuiAsControl, TuiControl } from '@taiga-ui/cdk' +import { + TuiAlertService, + TuiButton, + TuiDialogContext, + TuiError, +} from '@taiga-ui/core' +import { + TUI_FORMAT_ERROR, + TUI_VALIDATION_ERRORS, + TuiFieldErrorPipe, +} from '@taiga-ui/kit' +import { PolymorpheusOutlet } from '@taiga-ui/polymorpheus' +import { filter } from 'rxjs' + +import { ControlSpec } from '../controls/control' +import { CONTROLS } from '../controls/controls' +import { ControlDirective } from './control.directive' + +export const ERRORS = [ + 'required', + 'pattern', + 'notNumber', + 'numberNotInteger', + 'numberNotInRange', + 'listNotUnique', + 'listNotInRange', + 'listItemIssue', +] + +@Component({ + selector: 'form-control', + template: ` + + + @if (spec.warning || immutable) { + + {{ spec.warning }} + @if (immutable) { +

{{ 'This value cannot be changed once set' | i18n }}!

+ } +
+ + +
+
+ } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + tuiAsControl(FormControlComponent), + { + provide: TUI_VALIDATION_ERRORS, + deps: [FormControlComponent], + useFactory: (control: FormControlComponent) => ({ + [TUI_FORMAT_ERROR]: 'Invalid file format', + required: 'Required', + pattern: (context: any) => + 'patterns' in control.spec && + getText(control.spec, String(context.requiredPattern)), + }), + }, + ], + hostDirectives: [ControlDirective], + imports: [ + AsyncPipe, + i18nPipe, + PolymorpheusOutlet, + TuiError, + TuiFieldErrorPipe, + TuiButton, + ], +}) +export class FormControlComponent< + T extends ControlSpec, + V, +> extends TuiControl { + private readonly destroyRef = inject(DestroyRef) + private readonly alerts = inject(TuiAlertService) + private readonly i18n = inject(i18nPipe) + + protected readonly controls = CONTROLS + + @Input({ required: true }) + spec!: T + + @ViewChild('warning') + warning?: TemplateRef> + + warned = false + readonly order = ERRORS + + get immutable(): boolean { + return 'immutable' in this.spec && this.spec.immutable + } + + onInput(value: V | null) { + const previous = this.value() + + if (!this.warned && this.warning) { + this.alerts + .open(this.warning, { + label: this.i18n.transform('Warning'), + appearance: 'warning', + closeable: false, + autoClose: 0, + }) + .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.onChange(previous) + }) + } + + this.warned = true + this.onChange(value === '' ? null : value) + } +} + +function getText({ patterns }: IST.ValueSpecText, pattern: unknown): string { + return ( + patterns?.find(({ regex }) => String(regex) === pattern)?.description || + 'Invalid format' + ) +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/containers/control.directive.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/control.directive.ts new file mode 100644 index 000000000..910e219ea --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/control.directive.ts @@ -0,0 +1,45 @@ +import { Directive, inject, Injectable, OnDestroy, OnInit } from '@angular/core' +import { ControlContainer, NgControl } from '@angular/forms' +import { tuiInjectElement } from '@taiga-ui/cdk' + +@Injectable() +export class InvalidService { + private readonly controls: ControlDirective[] = [] + + scrollIntoView() { + this.controls.find(d => d.invalid)?.scrollIntoView() + } + + add(control: ControlDirective) { + this.controls.push(control) + } + + remove(control: ControlDirective) { + this.controls.splice(this.controls.indexOf(control), 1) + } +} + +@Directive() +export class ControlDirective implements OnInit, OnDestroy { + private readonly service = inject(InvalidService, { optional: true }) + private readonly element = tuiInjectElement() + private readonly control = + inject(NgControl, { optional: true }) || + inject(ControlContainer, { optional: true }) + + get invalid(): boolean { + return !!this.control?.invalid + } + + scrollIntoView() { + this.element.scrollIntoView({ behavior: 'smooth' }) + } + + ngOnInit() { + this.service?.add(this) + } + + ngOnDestroy() { + this.service?.remove(this) + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/containers/group.component.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/group.component.ts new file mode 100644 index 000000000..d633e0940 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/group.component.ts @@ -0,0 +1,119 @@ +import { KeyValuePipe } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + Input, + SkipSelf, + ViewEncapsulation, +} from '@angular/core' +import { ControlContainer, ReactiveFormsModule } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core' +import { identity, of } from 'rxjs' + +import { FilterHiddenPipe } from '../pipes/filter-hidden.pipe' +import { FormArrayComponent } from './array.component' +import { FormControlComponent } from './control.component' +import { FormObjectComponent } from './object.component' +import { FormUnionComponent } from './union.component' + +@Component({ + selector: 'form-group', + template: ` + @for (entry of spec | keyvalue: asIsOrder | filterHidden; track entry) { + @switch (entry.value.type) { + @case ('object') { + + } + @case ('union') { + + } + @case ('list') { + + } + @default { + + } + } + } + `, + styles: ` + form-group .g-form-control:not(:first-child) { + display: block; + margin-top: 1rem; + } + + form-group .g-form-group { + position: relative; + padding-left: var(--tui-height-m); + + &::before, + &::after { + content: ''; + position: absolute; + background: var(--tui-background-neutral-1); + } + + &::before { + top: 0; + left: calc(1rem - 1px); + bottom: 0.5rem; + width: 2px; + } + + &::after { + left: 0.75rem; + bottom: 0; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + } + } + + form-group tui-tooltip { + z-index: 1; + margin-left: 0.25rem; + } + `, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + { + provide: TUI_DEFAULT_ERROR_MESSAGE, + useValue: of('Unknown error'), + }, + { + provide: ControlContainer, + deps: [[new SkipSelf(), ControlContainer]], + useFactory: identity, + }, + ], + imports: [ + KeyValuePipe, + ReactiveFormsModule, + FilterHiddenPipe, + FormControlComponent, + FormObjectComponent, + FormArrayComponent, + FormUnionComponent, + ], +}) +export class FormGroupComponent { + @Input() spec: IST.InputSpec = {} + + asIsOrder() { + return 0 + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/containers/object.component.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/object.component.ts new file mode 100644 index 000000000..9bf7437dc --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/object.component.ts @@ -0,0 +1,118 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + forwardRef, + inject, + Input, + Output, +} from '@angular/core' +import { ControlContainer } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { TuiButton, TuiIcon } from '@taiga-ui/core' +import { TuiExpand } from '@taiga-ui/experimental' +import { TuiTooltip } from '@taiga-ui/kit' + +import { ControlDirective } from './control.directive' +import { FormGroupComponent } from './group.component' + +@Component({ + selector: 'form-object', + template: ` +

+ + + {{ spec.name }} + @if (spec.description) { + + } +

+ +
+ +
+
+ `, + styles: ` + @use '@taiga-ui/core/styles/taiga-ui-local' as taiga; + + :host { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .title { + position: relative; + height: var(--tui-height-l); + display: flex; + align-items: center; + cursor: pointer; + font: var(--tui-font-text-l); + font-weight: bold; + margin: 0 0 -0.75rem; + } + + .button { + @include taiga.transition(transform); + + margin-right: 1rem; + + &_open { + transform: rotate(180deg); + } + } + + .expand { + align-self: stretch; + } + + .g-form-group { + padding-top: 0.75rem; + + &_invalid::before, + &_invalid::after { + background: var(--tui-status-negative-pale); + } + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + hostDirectives: [ControlDirective], + imports: [ + TuiButton, + TuiIcon, + TuiTooltip, + TuiExpand, + forwardRef(() => FormGroupComponent), + ], +}) +export class FormObjectComponent { + @Input({ required: true }) + spec!: IST.ValueSpecObject + + @Input() + open = false + + @Output() + readonly openChange = new EventEmitter() + + private readonly container = inject(ControlContainer) + + get invalid() { + return !this.container.valid && this.container.touched + } + + toggle() { + this.open = !this.open + this.openChange.emit(this.open) + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts b/web/projects/ui/src/app/routes/portal/components/form/containers/union.component.ts similarity index 58% rename from web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts rename to web/projects/ui/src/app/routes/portal/components/form/containers/union.component.ts index ce735a83f..547dd4de2 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/containers/union.component.ts @@ -1,19 +1,49 @@ import { ChangeDetectionStrategy, Component, + forwardRef, inject, Input, OnChanges, } from '@angular/core' -import { ControlContainer, FormGroupName } from '@angular/forms' +import { + ControlContainer, + FormGroupName, + ReactiveFormsModule, +} from '@angular/forms' import { IST } from '@start9labs/start-sdk' +import { tuiPure, TuiValueChanges } from '@taiga-ui/cdk' +import { TuiElasticContainer } from '@taiga-ui/kit' import { FormService } from 'src/app/services/form.service' -import { tuiPure } from '@taiga-ui/cdk' + +import { FormControlComponent } from './control.component' +import { FormGroupComponent } from './group.component' @Component({ selector: 'form-union', - templateUrl: './form-union.component.html', - styleUrls: ['./form-union.component.scss'], + template: ` + + + + + `, + styles: ` + :host { + display: block; + } + + .group { + display: block; + margin-top: 1rem; + } + `, changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { @@ -21,7 +51,13 @@ import { tuiPure } from '@taiga-ui/cdk' useExisting: FormGroupName, }, ], - standalone: false, + imports: [ + ReactiveFormsModule, + TuiValueChanges, + TuiElasticContainer, + FormControlComponent, + forwardRef(() => FormGroupComponent), + ], }) export class FormUnionComponent implements OnChanges { @Input({ required: true }) diff --git a/web/projects/ui/src/app/routes/portal/components/form/control.directive.ts b/web/projects/ui/src/app/routes/portal/components/form/control.directive.ts deleted file mode 100644 index a624c8bd4..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/control.directive.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Directive, ElementRef, inject, OnDestroy, OnInit } from '@angular/core' -import { ControlContainer, NgControl } from '@angular/forms' -import { InvalidService } from './invalid.service' - -@Directive({ - selector: 'form-control, form-array, form-object', - standalone: false, -}) -export class ControlDirective implements OnInit, OnDestroy { - private readonly invalidService = inject(InvalidService, { optional: true }) - private readonly element: ElementRef = inject(ElementRef) - private readonly control = - inject(NgControl, { optional: true }) || - inject(ControlContainer, { optional: true }) - - get invalid(): boolean { - return !!this.control?.invalid - } - - scrollIntoView() { - this.element.nativeElement.scrollIntoView({ behavior: 'smooth' }) - } - - ngOnInit() { - this.invalidService?.add(this) - } - - ngOnDestroy() { - this.invalidService?.remove(this) - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/control.ts b/web/projects/ui/src/app/routes/portal/components/form/control.ts deleted file mode 100644 index 5cd79f260..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/control.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { inject } from '@angular/core' -import { FormControlComponent } from './form-control/form-control.component' -import { IST } from '@start9labs/start-sdk' - -export abstract class Control< - Spec extends Exclude, - Value, -> { - private readonly control: FormControlComponent = - inject(FormControlComponent) - - get invalid(): boolean { - return this.control.touched && this.control.invalid - } - - get spec(): Spec { - return this.control.spec - } - - get readOnly(): boolean { - const def = - 'default' in this.spec && - this.spec.default != null && - this.spec.default !== this.value - - return ( - !!this.value && - !def && - !!this.control.control?.pristine && - this.control.immutable - ) - } - - get value(): Value | null { - return this.control.value - } - - set value(value: Value | null) { - this.control.onInput(value) - } - - onFocus(focused: boolean) { - this.control.onFocus(focused) - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/color.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/color.component.ts new file mode 100644 index 000000000..fb0266a88 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/color.component.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { TuiInputColor, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-color', + template: ` + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + `, + imports: [ + FormsModule, + TuiTextfield, + TuiInputColor, + TuiIcon, + TuiTooltip, + HintPipe, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormColorComponent extends Control {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/control.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/control.ts new file mode 100644 index 000000000..72862b090 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/control.ts @@ -0,0 +1,41 @@ +import { inject } from '@angular/core' +import { IST } from '@start9labs/start-sdk' +import { TuiControl } from '@taiga-ui/cdk' + +export type ControlSpec = Exclude< + IST.ValueSpec, + | IST.ValueSpecHidden + | IST.ValueSpecList + | IST.ValueSpecUnion + | IST.ValueSpecObject +> + +export abstract class Control { + public readonly control: any = inject(TuiControl) + + get spec(): Spec { + return this.control.spec + } + + get readOnly(): boolean { + const def = + 'default' in this.spec && + this.spec.default != null && + this.spec.default !== this.value + + return ( + !!this.value && + !def && + !!this.control['control']?.pristine && + this.control.immutable + ) + } + + get value(): Value | null { + return this.control.value() + } + + set value(value: Value | null) { + this.control.onInput(value) + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/controls.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/controls.ts new file mode 100644 index 000000000..d1886b3cd --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/controls.ts @@ -0,0 +1,22 @@ +import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' +import { FormColorComponent } from './color.component' +import { FormDatetimeComponent } from './datetime.component' +import { FormFileComponent } from './file.component' +import { FormMultiselectComponent } from './multiselect.component' +import { FormNumberComponent } from './number.component' +import { FormSelectComponent } from './select.component' +import { FormTextComponent } from './text.component' +import { FormTextareaComponent } from './textarea.component' +import { FormToggleComponent } from './toggle.component' + +export const CONTROLS = { + color: new PolymorpheusComponent(FormColorComponent), + datetime: new PolymorpheusComponent(FormDatetimeComponent), + file: new PolymorpheusComponent(FormFileComponent), + number: new PolymorpheusComponent(FormNumberComponent), + select: new PolymorpheusComponent(FormSelectComponent), + multiselect: new PolymorpheusComponent(FormMultiselectComponent), + text: new PolymorpheusComponent(FormTextComponent), + textarea: new PolymorpheusComponent(FormTextareaComponent), + toggle: new PolymorpheusComponent(FormToggleComponent), +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/datetime.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/datetime.component.ts new file mode 100644 index 000000000..cc046e289 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/datetime.component.ts @@ -0,0 +1,144 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { + TUI_FIRST_DAY, + TUI_LAST_DAY, + TuiDay, + TuiMapperPipe, + tuiPure, + TuiTime, +} from '@taiga-ui/cdk' +import { TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { + TuiInputDate, + TuiInputDateTime, + TuiInputTime, + TuiTooltip, +} from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-datetime', + template: ` + + @switch (spec.inputmode) { + @case ('time') { + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + } + @case ('date') { + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + + } + @case ('datetime-local') { + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + + } + } + `, + imports: [ + FormsModule, + TuiTextfield, + TuiIcon, + TuiTooltip, + TuiInputTime, + TuiInputDate, + TuiMapperPipe, + TuiInputDateTime, + HintPipe, + ], +}) +export class FormDatetimeComponent extends Control< + IST.ValueSpecDatetime, + string +> { + readonly min = TUI_FIRST_DAY + readonly max = TUI_LAST_DAY + + @tuiPure + getTime(value: string | null) { + return value ? TuiTime.fromString(value) : null + } + + getLimit(limit: string): [TuiDay, TuiTime] { + return [ + TuiDay.jsonParse(limit.slice(0, 10)), + limit.length === 10 + ? new TuiTime(0, 0) + : TuiTime.fromString(limit.slice(-5)), + ] + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/file.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/file.component.ts new file mode 100644 index 000000000..74788fa15 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/file.component.ts @@ -0,0 +1,118 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { i18nPipe } from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { TuiButton, TuiIcon } from '@taiga-ui/core' +import { TuiChip, TuiFileLike, TuiFiles, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' + +@Component({ + selector: 'form-file', + template: ` + + `, + styles: ` + @use '@taiga-ui/core/styles/taiga-ui-local' as taiga; + + .template { + @include taiga.transition(opacity); + + width: 100%; + display: flex; + align-items: center; + padding: 0 0.5rem; + font: var(--tui-font-text-m); + font-weight: bold; + + &_hidden { + opacity: 0; + } + } + + .drop { + @include taiga.fullsize(); + @include taiga.transition(opacity); + display: flex; + align-items: center; + justify-content: space-around; + + &_hidden { + opacity: 0; + } + } + + .label { + display: flex; + align-items: center; + max-width: 50%; + } + + small { + max-width: 50%; + font-weight: normal; + color: var(--tui-text-secondary); + margin-left: auto; + } + + tui-chip { + z-index: 1; + margin: -0.25rem -0.25rem -0.25rem auto; + pointer-events: auto; + } + `, + imports: [ + FormsModule, + TuiFiles, + TuiIcon, + TuiTooltip, + TuiChip, + TuiButton, + i18nPipe, + ], +}) +export class FormFileComponent extends Control< + IST.ValueSpecFile, + TuiFileLike +> {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/multiselect.component.ts similarity index 57% rename from web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.ts rename to web/projects/ui/src/app/routes/portal/components/form/controls/multiselect.component.ts index 454df7261..26d0a4a28 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/multiselect.component.ts @@ -1,13 +1,49 @@ import { Component } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' -import { tuiPure } from '@taiga-ui/cdk' +import { FormsModule } from '@angular/forms' import { invert } from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { tuiPure } from '@taiga-ui/cdk' +import { TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { TuiMultiSelect, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' @Component({ selector: 'form-multiselect', - templateUrl: './form-multiselect.component.html', - standalone: false, + template: ` + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + `, + styles: ` + // TODO: Remove after Taiga UI update + :host ::ng-deep .t-input { + pointer-events: none; + } + `, + imports: [ + FormsModule, + TuiTextfield, + TuiMultiSelect, + TuiIcon, + TuiTooltip, + HintPipe, + ], }) export class FormMultiselectComponent extends Control< IST.ValueSpecMultiselect, diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts new file mode 100644 index 000000000..bc7abf54b --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/number.component.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { TuiIcon, TuiNumberFormat, TuiTextfield } from '@taiga-ui/core' +import { TuiInputNumber, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-number', + template: ` + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + `, + imports: [ + FormsModule, + TuiTextfield, + TuiInputNumber, + TuiNumberFormat, + TuiIcon, + TuiTooltip, + HintPipe, + ], +}) +export class FormNumberComponent extends Control { + get precision(): number { + return this.spec.integer ? 0 : Infinity + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts new file mode 100644 index 000000000..00e87c10c --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts @@ -0,0 +1,80 @@ +import { Component, inject } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { invert } from '@start9labs/shared' +import { IST } from '@start9labs/start-sdk' +import { TUI_IS_MOBILE } from '@taiga-ui/cdk' +import { TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-select', + template: ` + + @if (spec.name) { + + } + @if (mobile) { + + } @else { + + } + + @if (spec | hint; as hint) { + + } + + `, + imports: [ + FormsModule, + TuiTextfield, + TuiSelect, + TuiDataListWrapper, + TuiIcon, + TuiTooltip, + HintPipe, + ], +}) +export class FormSelectComponent extends Control { + private readonly inverted = invert(this.spec.values) + + protected readonly mobile = inject(TUI_IS_MOBILE) + protected readonly items = Object.values(this.spec.values) + protected readonly disabledItemHandler = (item: string) => + Array.isArray(this.spec.disabled) && + !!this.inverted[item] && + this.spec.disabled.includes(this.inverted[item]!) + + get disabled(): boolean { + return typeof this.spec.disabled === 'string' + } + + get selected(): string | null { + return (this.value && this.spec.values[this.value]) || null + } + + set selected(value: string | null) { + this.value = (value && this.inverted[value]) || null + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/text.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/text.component.ts new file mode 100644 index 000000000..650f8f911 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/text.component.ts @@ -0,0 +1,104 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IST, utils } from '@start9labs/start-sdk' +import { tuiInjectElement } from '@taiga-ui/cdk' +import { TuiButton, TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-text', + template: ` + + @if (spec.name) { + + } + + @if (spec.generate) { + + } + @if (spec.masked) { + + } + + @if (spec | hint; as hint) { + + } + + `, + styles: ` + .remove { + display: none; + order: 1; + } + + :host-context(form-array > form-control > :host) .remove { + display: flex; + } + `, + imports: [ + FormsModule, + TuiTextfield, + TuiButton, + TuiIcon, + TuiTooltip, + HintPipe, + ], +}) +export class FormTextComponent extends Control { + private readonly el = tuiInjectElement() + + masked = true + + generate() { + this.value = utils.getDefaultString(this.spec.generate || '') + } + + remove() { + this.el.dispatchEvent(new CustomEvent('remove', { bubbles: true })) + } +} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/textarea.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/textarea.component.ts new file mode 100644 index 000000000..b4fdbcde2 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/textarea.component.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { TuiIcon, TuiTextfield } from '@taiga-ui/core' +import { TuiTextarea, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-textarea', + template: ` + + @if (spec.name) { + + } + + @if (spec | hint; as hint) { + + } + + `, + imports: [ + FormsModule, + TuiTextfield, + TuiTextarea, + TuiIcon, + TuiTooltip, + HintPipe, + ], +}) +export class FormTextareaComponent extends Control< + IST.ValueSpecTextarea, + string +> {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/controls/toggle.component.ts b/web/projects/ui/src/app/routes/portal/components/form/controls/toggle.component.ts new file mode 100644 index 000000000..89521ea73 --- /dev/null +++ b/web/projects/ui/src/app/routes/portal/components/form/controls/toggle.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IST } from '@start9labs/start-sdk' +import { TuiIcon } from '@taiga-ui/core' +import { TuiSwitch, TuiTooltip } from '@taiga-ui/kit' + +import { Control } from './control' +import { HintPipe } from '../pipes/hint.pipe' + +@Component({ + selector: 'form-toggle', + template: ` + {{ spec.name }} + @if (spec.description || spec.disabled) { + + } + + `, + host: { class: 'g-toggle' }, + imports: [TuiIcon, TuiTooltip, HintPipe, TuiSwitch, FormsModule], +}) +export class FormToggleComponent extends Control< + IST.ValueSpecToggle, + boolean +> {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html deleted file mode 100644 index f57a37de3..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html +++ /dev/null @@ -1,57 +0,0 @@ -
- {{ spec.name }} - @if (spec.description || spec.disabled) { - - } - -
- - -@for (item of array.control.controls; track item) { - @if (spec.spec.type === 'object') { - - {{ item.value | mustache: $any(spec.spec).displayAs }} - - - } @else { - - } - - - -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.scss deleted file mode 100644 index 0879acde5..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.scss +++ /dev/null @@ -1,50 +0,0 @@ -@use '@taiga-ui/core/styles/taiga-ui-local' as taiga; - -:host { - display: block; - margin: 2rem 0; -} - -.label { - display: flex; - font-size: 1.25rem; - font-weight: bold; -} - -.add { - font-size: 1rem; - padding: 0 1rem; - margin-left: auto; -} - -.object { - display: block; - position: relative; - - &_open::after, - &:last-child::after { - opacity: 0; - } - - &::after { - @include taiga.transition(opacity); - - content: ''; - position: absolute; - bottom: -0.5rem; - height: 1px; - left: 3rem; - right: 1rem; - background: var(--tui-background-neutral-1); - } -} - -.remove { - margin-left: auto; - pointer-events: auto; -} - -.control { - display: block; - margin: 0.5rem 0; -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts deleted file mode 100644 index 2fef50ae7..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - Component, - DestroyRef, - HostBinding, - inject, - Input, -} from '@angular/core' -import { takeUntilDestroyed } from '@angular/core/rxjs-interop' -import { AbstractControl, FormArrayName } from '@angular/forms' -import { - TUI_ANIMATIONS_SPEED, - tuiFadeIn, - tuiHeightCollapse, - tuiParentStop, - tuiToAnimationOptions, -} from '@taiga-ui/core' -import { filter } from 'rxjs' -import { IST } from '@start9labs/start-sdk' -import { FormService } from 'src/app/services/form.service' -import { ERRORS } from '../form-group/form-group.component' -import { DialogService, i18nKey } from '@start9labs/shared' - -@Component({ - selector: 'form-array', - templateUrl: './form-array.component.html', - styleUrls: ['./form-array.component.scss'], - animations: [tuiFadeIn, tuiHeightCollapse, tuiParentStop], - standalone: false, -}) -export class FormArrayComponent { - @Input({ required: true }) - spec!: IST.ValueSpecList - - @HostBinding('@tuiParentStop') - readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED)) - readonly order = ERRORS - readonly array = inject(FormArrayName) - readonly open = new Map() - - private warned = false - private readonly formService = inject(FormService) - private readonly destroyRef = inject(DestroyRef) - private readonly dialog = inject(DialogService) - - get canAdd(): boolean { - return ( - !this.spec.disabled && - (!this.spec.maxLength || - this.spec.maxLength >= this.array.control.controls.length) - ) - } - - add() { - if (!this.warned && this.spec.warning) { - this.dialog - .openConfirm({ - label: 'Warning', - size: 's', - data: { - content: this.spec.warning as i18nKey, - yes: 'Ok', - no: 'Cancel', - }, - }) - .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.addItem() - }) - } else { - this.addItem() - } - - this.warned = true - } - - removeAt(index: number) { - this.removeItem(index) - } - - private removeItem(index: number) { - this.open.delete(this.array.control.at(index)) - this.array.control.removeAt(index) - } - - private addItem() { - this.array.control.insert(0, this.formService.getListItem(this.spec)) - this.open.set(this.array.control.at(0), true) - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.html deleted file mode 100644 index 9a665b9cf..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.html +++ /dev/null @@ -1,30 +0,0 @@ - - {{ spec.name }} - @if (spec.required) { - * - } - - -
- @if (!readOnly && !spec.disabled) { - - } - -
-
diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.scss deleted file mode 100644 index 7a4acb240..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -@use '@taiga-ui/core/styles/taiga-ui-local' as taiga; - -.wrapper { - position: relative; - width: 1.5rem; - height: 1.5rem; - pointer-events: auto; - - &::after { - content: ''; - position: absolute; - height: 0.3rem; - width: 1.4rem; - bottom: -0.25rem; - background: currentColor; - border-radius: 0.125rem; - pointer-events: none; - } -} - -.color { - @include taiga.fullsize(); - opacity: 0; -} - -.icon { - @include taiga.fullsize(); - pointer-events: none; - - input:hover + & { - opacity: 1; - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.ts deleted file mode 100644 index af9caa708..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-color/form-color.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' -import { MaskitoOptions } from '@maskito/core' - -@Component({ - selector: 'form-color', - templateUrl: './form-color.component.html', - styleUrls: ['./form-color.component.scss'], - standalone: false, -}) -export class FormColorComponent extends Control { - readonly mask: MaskitoOptions = { - mask: ['#', ...Array(6).fill(/[0-9a-f]/i)], - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html deleted file mode 100644 index f10899051..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html +++ /dev/null @@ -1,58 +0,0 @@ -@switch (spec.type) { - @case ('color') { - - } - @case ('datetime') { - - } - @case ('file') { - - } - @case ('multiselect') { - - } - @case ('number') { - - } - @case ('select') { - - } - @case ('text') { - - } - @case ('textarea') { - - } - @case ('toggle') { - - } -} - -@if (spec.warning || immutable) { - - {{ spec.warning }} - @if (immutable) { -

{{ 'This value cannot be changed once set' | i18n }}!

- } -
- - -
-
-} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.scss deleted file mode 100644 index 844651118..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -:host { - display: block; -} - -.buttons { - margin-top: 0.5rem; - - :first-child { - margin-right: 0.5rem; - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts deleted file mode 100644 index fc17cc665..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - inject, - Input, - TemplateRef, - ViewChild, -} from '@angular/core' -import { takeUntilDestroyed } from '@angular/core/rxjs-interop' -import { AbstractTuiNullableControl } from '@taiga-ui/legacy' -import { filter } from 'rxjs' -import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core' -import { IST } from '@start9labs/start-sdk' -import { ERRORS } from '../form-group/form-group.component' -import { FORM_CONTROL_PROVIDERS } from './form-control.providers' -import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared' - -@Component({ - selector: 'form-control', - templateUrl: './form-control.component.html', - styleUrls: ['./form-control.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - providers: FORM_CONTROL_PROVIDERS, - standalone: false, -}) -export class FormControlComponent< - T extends Exclude, - V, -> extends AbstractTuiNullableControl { - private readonly alerts = inject(TuiAlertService) - private readonly i18n = inject(i18nPipe) - - @Input({ required: true }) - spec!: T - - @ViewChild('warning') - warning?: TemplateRef> - - warned = false - focused = false - readonly order = ERRORS - - get immutable(): boolean { - return 'immutable' in this.spec && this.spec.immutable - } - - onFocus(focused: boolean) { - this.focused = focused - this.updateFocused(focused) - } - - onInput(value: V | null) { - const previous = this.value - - if (!this.warned && this.warning) { - this.alerts - .open(this.warning, { - label: this.i18n.transform('Warning'), - appearance: 'warning', - closeable: false, - autoClose: 0, - }) - .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.value = previous - }) - } - - this.warned = true - this.value = value === '' ? null : value - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.providers.ts b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.providers.ts deleted file mode 100644 index 45de1f694..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.providers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { forwardRef, Provider } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { TUI_FORMAT_ERROR, TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' -import { FormControlComponent } from './form-control.component' - -interface ValidatorsPatternError { - actualValue: string - requiredPattern: string | RegExp -} - -export const FORM_CONTROL_PROVIDERS: Provider[] = [ - { - provide: TUI_VALIDATION_ERRORS, - deps: [forwardRef(() => FormControlComponent)], - useFactory: ( - control: FormControlComponent< - Exclude, - string - >, - ) => ({ - required: 'Required', - pattern: ({ requiredPattern }: ValidatorsPatternError) => - ('patterns' in control.spec && - control.spec.patterns.find( - ({ regex }) => String(regex) === String(requiredPattern), - )?.description) || - 'Invalid format', - [TUI_FORMAT_ERROR]: 'Invalid file format', - }), - }, -] diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.html deleted file mode 100644 index 801da8771..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.html +++ /dev/null @@ -1,54 +0,0 @@ - - @switch (spec.inputmode) { - @case ('time') { - - {{ spec.name }} - @if (spec.required) { - * - } - - } - @case ('date') { - - {{ spec.name }} - @if (spec.required) { - * - } - - } - @case ('datetime-local') { - - {{ spec.name }} - @if (spec.required) { - * - } - - } - } - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.ts deleted file mode 100644 index d59e0221a..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-datetime/form-datetime.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component } from '@angular/core' -import { - TUI_FIRST_DAY, - TUI_LAST_DAY, - TuiDay, - tuiPure, - TuiTime, -} from '@taiga-ui/cdk' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-datetime', - templateUrl: './form-datetime.component.html', - standalone: false, -}) -export class FormDatetimeComponent extends Control< - IST.ValueSpecDatetime, - string -> { - readonly min = TUI_FIRST_DAY - readonly max = TUI_LAST_DAY - - @tuiPure - getTime(value: string | null) { - return value ? TuiTime.fromString(value) : null - } - - getLimit(limit: string): [TuiDay, TuiTime] { - return [ - TuiDay.jsonParse(limit.slice(0, 10)), - limit.length === 10 - ? new TuiTime(0, 0) - : TuiTime.fromString(limit.slice(-5)), - ] - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html deleted file mode 100644 index e8b3cd99b..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html +++ /dev/null @@ -1,42 +0,0 @@ - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.scss deleted file mode 100644 index 8d852c453..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use '@taiga-ui/core/styles/taiga-ui-local' as taiga; - -.template { - @include taiga.transition(opacity); - - width: 100%; - display: flex; - align-items: center; - padding: 0 0.5rem; - font: var(--tui-font-text-m); - font-weight: bold; - - &_hidden { - opacity: 0; - } -} - -.drop { - @include taiga.fullsize(); - @include taiga.transition(opacity); - display: flex; - align-items: center; - justify-content: space-around; - - &_hidden { - opacity: 0; - } -} - -.label { - display: flex; - align-items: center; - max-width: 50%; -} - -small { - max-width: 50%; - font-weight: normal; - color: var(--tui-text-secondary); - margin-left: auto; -} - -tui-chip { - z-index: 1; - margin: -0.25rem -0.25rem -0.25rem auto; - pointer-events: auto; -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.ts deleted file mode 100644 index f1ef194b9..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from '@angular/core' -import { TuiFileLike } from '@taiga-ui/kit' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-file', - templateUrl: './form-file.component.html', - styleUrls: ['./form-file.component.scss'], - standalone: false, -}) -export class FormFileComponent extends Control< - IST.ValueSpecFile, - TuiFileLike -> {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.html deleted file mode 100644 index c00705442..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.html +++ /dev/null @@ -1,30 +0,0 @@ -@for (entry of spec | keyvalue: asIsOrder | filterHidden; track entry) { - - @switch (entry.value.type) { - @case ('object') { - - } - @case ('union') { - - } - @case ('list') { - - } - @default { - - } - } - -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.scss deleted file mode 100644 index 76989c209..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.scss +++ /dev/null @@ -1,35 +0,0 @@ -form-group .g-form-control:not(:first-child) { - margin-top: 1rem; -} - -form-group .g-form-group { - position: relative; - padding-left: var(--tui-height-m); - - &::before, - &::after { - content: ''; - position: absolute; - background: var(--tui-background-neutral-1); - } - - &::before { - top: 0; - left: calc(1rem - 1px); - bottom: 0.5rem; - width: 2px; - } - - &::after { - left: 0.75rem; - bottom: 0; - width: 0.5rem; - height: 0.5rem; - border-radius: 100%; - } -} - -form-group tui-tooltip { - z-index: 1; - margin-left: 0.25rem; -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.ts deleted file mode 100644 index 9a5c00413..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - Input, - ViewEncapsulation, -} from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { FORM_GROUP_PROVIDERS } from './form-group.providers' - -export const ERRORS = [ - 'required', - 'pattern', - 'notNumber', - 'numberNotInteger', - 'numberNotInRange', - 'listNotUnique', - 'listNotInRange', - 'listItemIssue', -] - -@Component({ - selector: 'form-group', - templateUrl: './form-group.component.html', - styleUrls: ['./form-group.component.scss'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - viewProviders: [FORM_GROUP_PROVIDERS], - standalone: false, -}) -export class FormGroupComponent { - @Input() spec: IST.InputSpec = {} - - asIsOrder() { - return 0 - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.providers.ts b/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.providers.ts deleted file mode 100644 index 4383a96d7..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-group/form-group.providers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Provider, SkipSelf } from '@angular/core' -import { ControlContainer } from '@angular/forms' -import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core' -import { tuiInputDateOptionsProvider } from '@taiga-ui/kit' -import { TUI_ARROW_MODE, tuiInputTimeOptionsProvider } from '@taiga-ui/legacy' -import { identity, of } from 'rxjs' - -export const FORM_GROUP_PROVIDERS: Provider[] = [ - { - provide: TUI_DEFAULT_ERROR_MESSAGE, - useValue: of('Unknown error'), - }, - { - provide: ControlContainer, - deps: [[new SkipSelf(), ControlContainer]], - useFactory: identity, - }, - { - provide: TUI_ARROW_MODE, - useValue: { - interactive: null, - disabled: null, - }, - }, - tuiInputDateOptionsProvider({ - nativePicker: true, - }), - tuiInputTimeOptionsProvider({ - nativePicker: true, - }), -] diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.html deleted file mode 100644 index 0e2a47cc2..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-multiselect/form-multiselect.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - {{ spec.name }} - - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.html deleted file mode 100644 index 060f37b98..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - {{ spec.name }} - @if (spec.required) { - * - } - - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.ts deleted file mode 100644 index 20af9ee3b..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-number/form-number.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-number', - templateUrl: './form-number.component.html', - standalone: false, -}) -export class FormNumberComponent extends Control { - protected readonly Infinity = Infinity -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.html deleted file mode 100644 index 445f6022d..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.html +++ /dev/null @@ -1,23 +0,0 @@ -

- - - {{ spec.name }} - @if (spec.description) { - - } -

- - -
- -
-
diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.scss deleted file mode 100644 index 097173211..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.scss +++ /dev/null @@ -1,41 +0,0 @@ -@use '@taiga-ui/core/styles/taiga-ui-local' as taiga; - -:host { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.title { - position: relative; - height: var(--tui-height-l); - display: flex; - align-items: center; - cursor: pointer; - font: var(--tui-font-text-l); - font-weight: bold; - margin: 0 0 -0.75rem; -} - -.button { - @include taiga.transition(transform); - - margin-right: 1rem; - - &_open { - transform: rotate(180deg); - } -} - -.expand { - align-self: stretch; -} - -.g-form-group { - padding-top: 0.75rem; - - &_invalid::before, - &_invalid::after { - background: var(--tui-status-negative-pale); - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.ts deleted file mode 100644 index 1d64c8386..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-object/form-object.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - inject, - Input, - Output, -} from '@angular/core' -import { ControlContainer } from '@angular/forms' -import { IST } from '@start9labs/start-sdk' - -@Component({ - selector: 'form-object', - templateUrl: './form-object.component.html', - styleUrls: ['./form-object.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false, -}) -export class FormObjectComponent { - @Input({ required: true }) - spec!: IST.ValueSpecObject - - @Input() - open = false - - @Output() - readonly openChange = new EventEmitter() - - private readonly container = inject(ControlContainer) - - get invalid() { - return !this.container.valid && this.container.touched - } - - toggle() { - this.open = !this.open - this.openChange.emit(this.open) - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.html deleted file mode 100644 index b8e79351a..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - {{ spec.name }} * - - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.ts deleted file mode 100644 index 09eda4ae7..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-select/form-select.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Component } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { invert } from '@start9labs/shared' -import { Control } from '../control' - -@Component({ - selector: 'form-select', - templateUrl: './form-select.component.html', - standalone: false, -}) -export class FormSelectComponent extends Control { - private readonly inverted = invert(this.spec.values) - - readonly items = Object.values(this.spec.values) - - readonly disabledItemHandler = (item: string) => - Array.isArray(this.spec.disabled) && - !!this.inverted[item] && - this.spec.disabled.includes(this.inverted[item]!) - - get disabled(): boolean { - return typeof this.spec.disabled === 'string' - } - - get selected(): string | null { - return (this.value && this.spec.values[this.value]) || null - } - - set selected(value: string | null) { - this.value = (value && this.inverted[value]) || null - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.html deleted file mode 100644 index 1240c6838..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.html +++ /dev/null @@ -1,48 +0,0 @@ - - {{ spec.name }} - @if (spec.required) { - * - } - - - - @if (spec.generate) { - - } - @if (spec.masked) { - - } - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.scss deleted file mode 100644 index 05e47885b..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -.button { - pointer-events: auto; - margin-left: 0.25rem; -} - -.masked { - -webkit-text-security: disc; -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.ts deleted file mode 100644 index 2eddb5dff..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-text/form-text.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component } from '@angular/core' -import { IST, utils } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-text', - templateUrl: './form-text.component.html', - styleUrls: ['./form-text.component.scss'], - standalone: false, -}) -export class FormTextComponent extends Control { - masked = true - - generate() { - this.value = utils.getDefaultString(this.spec.generate || '') - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.html deleted file mode 100644 index 32affd65a..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.html +++ /dev/null @@ -1,20 +0,0 @@ - - {{ spec.name }} - @if (spec.required) { - * - } - - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.ts deleted file mode 100644 index 72d6127fd..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-textarea/form-textarea.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-textarea', - templateUrl: './form-textarea.component.html', - standalone: false, -}) -export class FormTextareaComponent extends Control< - IST.ValueSpecTextarea, - string -> {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html deleted file mode 100644 index 8b49e15c9..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html +++ /dev/null @@ -1,13 +0,0 @@ -{{ spec.name }} -@if (spec.description || spec.disabled) { - -} - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts deleted file mode 100644 index 2b84779ce..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component } from '@angular/core' -import { IST } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-toggle', - templateUrl: './form-toggle.component.html', - host: { class: 'g-toggle' }, - standalone: false, -}) -export class FormToggleComponent extends Control< - IST.ValueSpecToggle, - boolean -> {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.html deleted file mode 100644 index 513007d65..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.scss b/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.scss deleted file mode 100644 index cfb2f95e8..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form-union/form-union.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - display: block; -} - -.group { - display: block; - margin-top: 1rem; -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form.module.ts b/web/projects/ui/src/app/routes/portal/components/form/form.module.ts deleted file mode 100644 index 04bef2b52..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/form.module.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { CommonModule } from '@angular/common' -import { NgModule } from '@angular/core' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { MaskitoDirective } from '@maskito/angular' -import { i18nPipe } from '@start9labs/shared' -import { TuiMapperPipe, TuiValueChanges } from '@taiga-ui/cdk' -import { - TuiAppearance, - TuiButton, - TuiError, - TuiExpand, - TuiHint, - TuiIcon, - TuiLink, - TuiNumberFormat, -} from '@taiga-ui/core' -import { - TuiChip, - TuiElasticContainer, - TuiFieldErrorPipe, - TuiFiles, - TuiSwitch, - TuiTooltip, -} from '@taiga-ui/kit' -import { - TuiInputDateModule, - TuiInputDateTimeModule, - TuiInputModule, - TuiInputNumberModule, - TuiInputTimeModule, - TuiMultiSelectModule, - TuiSelectModule, - TuiTextareaModule, - TuiTextfieldControllerModule, -} from '@taiga-ui/legacy' -import { ControlDirective } from './control.directive' -import { FilterHiddenPipe } from './filter-hidden.pipe' -import { FormArrayComponent } from './form-array/form-array.component' -import { FormColorComponent } from './form-color/form-color.component' -import { FormControlComponent } from './form-control/form-control.component' -import { FormDatetimeComponent } from './form-datetime/form-datetime.component' -import { FormFileComponent } from './form-file/form-file.component' -import { FormGroupComponent } from './form-group/form-group.component' -import { FormMultiselectComponent } from './form-multiselect/form-multiselect.component' -import { FormNumberComponent } from './form-number/form-number.component' -import { FormObjectComponent } from './form-object/form-object.component' -import { FormSelectComponent } from './form-select/form-select.component' -import { FormTextComponent } from './form-text/form-text.component' -import { FormTextareaComponent } from './form-textarea/form-textarea.component' -import { FormToggleComponent } from './form-toggle/form-toggle.component' -import { FormUnionComponent } from './form-union/form-union.component' -import { HintPipe } from './hint.pipe' -import { MustachePipe } from './mustache.pipe' - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - TuiInputModule, - TuiInputNumberModule, - ...TuiFiles, - TuiTextareaModule, - TuiSelectModule, - TuiMultiSelectModule, - TuiSwitch, - TuiTooltip, - ...TuiHint, - TuiChip, - TuiButton, - ...TuiExpand, - TuiTextfieldControllerModule, - TuiLink, - TuiError, - TuiFieldErrorPipe, - TuiValueChanges, - TuiElasticContainer, - MaskitoDirective, - TuiInputDateModule, - TuiInputTimeModule, - TuiInputDateTimeModule, - TuiMapperPipe, - TuiAppearance, - TuiIcon, - TuiNumberFormat, - i18nPipe, - ], - declarations: [ - FormGroupComponent, - FormControlComponent, - FormColorComponent, - FormDatetimeComponent, - FormTextComponent, - FormToggleComponent, - FormTextareaComponent, - FormNumberComponent, - FormSelectComponent, - FormMultiselectComponent, - FormFileComponent, - FormUnionComponent, - FormObjectComponent, - FormArrayComponent, - MustachePipe, - HintPipe, - ControlDirective, - FilterHiddenPipe, - ], - exports: [FormGroupComponent], -}) -export class FormModule {} diff --git a/web/projects/ui/src/app/routes/portal/components/form/invalid.service.ts b/web/projects/ui/src/app/routes/portal/components/form/invalid.service.ts deleted file mode 100644 index 9f474e853..000000000 --- a/web/projects/ui/src/app/routes/portal/components/form/invalid.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core' -import { ControlDirective } from './control.directive' - -@Injectable() -export class InvalidService { - private readonly controls: ControlDirective[] = [] - - scrollIntoView() { - this.controls.find(({ invalid }) => invalid)?.scrollIntoView() - } - - add(control: ControlDirective) { - this.controls.push(control) - } - - remove(control: ControlDirective) { - this.controls.splice(this.controls.indexOf(control), 1) - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/filter-hidden.pipe.ts b/web/projects/ui/src/app/routes/portal/components/form/pipes/filter-hidden.pipe.ts similarity index 95% rename from web/projects/ui/src/app/routes/portal/components/form/filter-hidden.pipe.ts rename to web/projects/ui/src/app/routes/portal/components/form/pipes/filter-hidden.pipe.ts index 63746d2c6..84666a2c9 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/filter-hidden.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/pipes/filter-hidden.pipe.ts @@ -4,7 +4,6 @@ import { KeyValue } from '@angular/common' @Pipe({ name: 'filterHidden', - standalone: false, }) export class FilterHiddenPipe implements PipeTransform { transform(value: KeyValue[]) { diff --git a/web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts b/web/projects/ui/src/app/routes/portal/components/form/pipes/hint.pipe.ts similarity index 96% rename from web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts rename to web/projects/ui/src/app/routes/portal/components/form/pipes/hint.pipe.ts index a5dd4b38f..862d52168 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/pipes/hint.pipe.ts @@ -4,7 +4,6 @@ import { IST } from '@start9labs/start-sdk' @Pipe({ name: 'hint', - standalone: false, }) export class HintPipe implements PipeTransform { private readonly i18n = inject(i18nPipe) diff --git a/web/projects/ui/src/app/routes/portal/components/form/mustache.pipe.ts b/web/projects/ui/src/app/routes/portal/components/form/pipes/mustache.pipe.ts similarity index 93% rename from web/projects/ui/src/app/routes/portal/components/form/mustache.pipe.ts rename to web/projects/ui/src/app/routes/portal/components/form/pipes/mustache.pipe.ts index 68fdb20cb..b7cc811de 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/mustache.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/pipes/mustache.pipe.ts @@ -3,7 +3,6 @@ import Mustache from 'mustache' @Pipe({ name: 'mustache', - standalone: false, }) export class MustachePipe implements PipeTransform { transform(value: any, displayAs: string): string { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts index 4d82e1e74..5f8b6b270 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts @@ -22,12 +22,12 @@ import { ActionButton, FormComponent, } from 'src/app/routes/portal/components/form.component' +import { InvalidService } from 'src/app/routes/portal/components/form/containers/control.directive' import { TaskInfoComponent } from 'src/app/routes/portal/modals/config-dep.component' import { ActionService } from 'src/app/services/action.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { getAllPackages, getManifest } from 'src/app/utils/get-package-data' -import { InvalidService } from '../../../components/form/invalid.service' export type PackageActionData = { pkgInfo: { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts index 2be6d19e6..2a0ab1da1 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/dns/dns.component.ts @@ -14,7 +14,7 @@ import { TuiButton, TuiTitle } from '@taiga-ui/core' import { TuiHeader } from '@taiga-ui/layout' import { PatchDB } from 'patch-db-client' import { combineLatest, first, switchMap } from 'rxjs' -import { FormModule } from 'src/app/routes/portal/components/form/form.module' +import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormService } from 'src/app/services/form.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -95,7 +95,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' CommonModule, FormsModule, ReactiveFormsModule, - FormModule, + FormGroupComponent, TuiButton, TuiHeader, TuiTitle, diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts index d1f020ca9..1c2d15bdd 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts @@ -15,7 +15,7 @@ import { TuiButton, TuiTextfield, TuiTitle } from '@taiga-ui/core' import { TuiHeader } from '@taiga-ui/layout' import { PatchDB } from 'patch-db-client' import { switchMap, tap } from 'rxjs' -import { FormModule } from 'src/app/routes/portal/components/form/form.module' +import { FormGroupComponent } from 'src/app/routes/portal/components/form/containers/group.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormService } from 'src/app/services/form.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -122,7 +122,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' CommonModule, FormsModule, ReactiveFormsModule, - FormModule, + FormGroupComponent, TuiButton, TuiTextfield, TuiHeader,