refactor: refactor forms components and remove legacy Taiga UI package (#3012)

This commit is contained in:
Alex Inkin
2025-08-27 22:57:49 +07:00
committed by GitHub
parent 2a27716e29
commit b7438ef155
69 changed files with 1568 additions and 1486 deletions

204
web/package-lock.json generated
View File

@@ -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
},

View File

@@ -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",

View File

@@ -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;

View File

@@ -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,

View File

@@ -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),

View File

@@ -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<T> {
@@ -88,7 +88,7 @@ export interface FormContext<T> {
RouterModule,
TuiValueChanges,
TuiButton,
FormModule,
FormGroupComponent,
],
providers: [InvalidService],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -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: `
<div class="label">
{{ spec.name }}
@if (spec.description || spec.disabled) {
<tui-icon [tuiTooltip]="spec | hint" />
}
<button
tuiLink
type="button"
class="add"
[disabled]="!canAdd"
(click)="add()"
>
+ {{ 'Add' | i18n }}
</button>
</div>
<tui-error [error]="order | tuiFieldError | async" />
@for (item of array.control.controls; track item) {
@if (spec.spec.type === 'object') {
<form-object
class="object"
[class.object_open]="!!open.get(item)"
[formGroup]="$any(item)"
[spec]="$any(spec.spec)"
[@tuiHeightCollapse]="animation"
[@tuiFadeIn]="animation"
[open]="!!open.get(item)"
(openChange)="open.set(item, $event)"
>
{{ item.value | mustache: $any(spec.spec).displayAs }}
<button
tuiIconButton
type="button"
class="remove"
iconStart="@tui.trash"
appearance="icon"
size="m"
title="Remove"
(click.stop)="removeAt($index)"
></button>
</form-object>
} @else {
<form-control
class="control"
tuiTextfieldSize="m"
[formControl]="$any(item)"
[spec]="$any(spec.spec)"
[@tuiHeightCollapse]="animation"
[@tuiFadeIn]="animation"
(remove)="removeAt($index)"
/>
}
}
`,
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<AbstractControl, boolean>()
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<boolean>({
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)
}
}

View File

@@ -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: `
<ng-container *polymorpheusOutlet="controls[spec.type]" />
<tui-error [error]="order | tuiFieldError | async" />
@if (spec.warning || immutable) {
<ng-template #warning let-completeWith="completeWith">
{{ spec.warning }}
@if (immutable) {
<p>{{ 'This value cannot be changed once set' | i18n }}!</p>
}
<div [style.margin-top.rem]="0.5">
<button
tuiButton
type="button"
appearance="secondary-grayscale"
size="s"
[style.margin-inline-end.rem]="0.5"
(click)="completeWith(false)"
>
{{ 'Continue' | i18n }}
</button>
<button
tuiButton
type="button"
appearance="flat-grayscale"
size="s"
(click)="completeWith(true)"
>
{{ 'Cancel' | i18n }}
</button>
</div>
</ng-template>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
tuiAsControl(FormControlComponent),
{
provide: TUI_VALIDATION_ERRORS,
deps: [FormControlComponent],
useFactory: (control: FormControlComponent<ControlSpec, string>) => ({
[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<V | null> {
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<TuiDialogContext<boolean>>
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<boolean>(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'
)
}

View File

@@ -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)
}
}

View File

@@ -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') {
<form-object
class="g-form-control"
[formGroupName]="entry.key"
[spec]="$any(entry.value)"
/>
}
@case ('union') {
<form-union
class="g-form-control"
[formGroupName]="entry.key"
[spec]="$any(entry.value)"
/>
}
@case ('list') {
<form-array [formArrayName]="entry.key" [spec]="$any(entry.value)" />
}
@default {
<form-control
class="g-form-control"
[formControlName]="entry.key"
[spec]="entry.value"
/>
}
}
}
`,
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
}
}

View File

@@ -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: `
<h3 class="title" (click)="toggle()">
<button
tuiIconButton
size="s"
iconStart="@tui.chevron-down"
type="button"
class="button"
[class.button_open]="open"
[style.border-radius.%]="100"
[appearance]="invalid ? 'primary-destructive' : 'secondary'"
></button>
<ng-content />
{{ spec.name }}
@if (spec.description) {
<tui-icon [tuiTooltip]="spec.description" (click.stop)="(0)" />
}
</h3>
<tui-expand class="expand" [expanded]="open">
<div class="g-form-group" [class.g-form-group_invalid]="invalid">
<form-group [spec]="spec.spec" />
</div>
</tui-expand>
`,
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<boolean>()
private readonly container = inject(ControlContainer)
get invalid() {
return !this.container.valid && this.container.touched
}
toggle() {
this.open = !this.open
this.openChange.emit(this.open)
}
}

View File

@@ -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: `
<form-control
[spec]="selectSpec"
formControlName="selection"
(tuiValueChanges)="onUnion($event)"
/>
<tui-elastic-container class="g-form-group" formGroupName="value">
<form-group
class="group"
[spec]="(union && spec.variants[union]?.spec) || {}"
/>
</tui-elastic-container>
`,
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 })

View File

@@ -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<HTMLElement> = 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)
}
}

View File

@@ -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<IST.ValueSpec, IST.ValueSpecHidden>,
Value,
> {
private readonly control: FormControlComponent<Spec, Value> =
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)
}
}

View File

@@ -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: `
<tui-textfield iconStart=" " [tuiTextfieldCleaner]="false">
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<input
placeholder="#000000"
tuiInputColor
[invalid]="control.invalid()"
[readOnly]="readOnly"
[disabled]="!!spec.disabled"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
`,
imports: [
FormsModule,
TuiTextfield,
TuiInputColor,
TuiIcon,
TuiTooltip,
HintPipe,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormColorComponent extends Control<IST.ValueSpecColor, string> {}

View File

@@ -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<Spec extends ControlSpec, Value> {
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)
}
}

View File

@@ -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),
}

View File

@@ -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: `
<!--
TODO: Move @switch down to only affect <input ... /> after fix:
https://github.com/taiga-family/taiga-ui/issues/11780
-->
@switch (spec.inputmode) {
@case ('time') {
<tui-textfield (tuiActiveZoneChange)="!$event && control.onTouched()">
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<input
tuiInputTime
type="time"
[invalid]="control.invalid()"
[readOnly]="readOnly"
[disabled]="!!spec.disabled"
[ngModel]="getTime(value)"
(ngModelChange)="value = $event?.toString() || null"
(blur)="control.onTouched()"
/>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
}
@case ('date') {
<tui-textfield (tuiActiveZoneChange)="!$event && control.onTouched()">
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<input
tuiInputDate
type="date"
[invalid]="control.invalid()"
[readOnly]="readOnly"
[disabled]="!!spec.disabled"
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
<tui-calendar *tuiTextfieldDropdown />
</tui-textfield>
}
@case ('datetime-local') {
<tui-textfield (tuiActiveZoneChange)="!$event && control.onTouched()">
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<input
tuiInputDateTime
type="datetime-local"
[invalid]="control.invalid()"
[readOnly]="readOnly"
[disabled]="!!spec.disabled"
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
<tui-calendar *tuiTextfieldDropdown />
</tui-textfield>
}
}
`,
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)),
]
}
}

View File

@@ -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: `
<label tuiInputFiles>
<input
tuiInputFiles
[invalid]="control.invalid()"
[accept]="spec.extensions.join(',')"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
<ng-template let-drop>
<div class="template" [class.template_hidden]="drop">
<div class="label">
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
@if (spec.description) {
<tui-icon [tuiTooltip]="spec.description" />
}
</div>
@if (value) {
<tui-chip>
{{ value.name }}
<button
tuiIconButton
type="button"
appearance="icon"
size="xs"
iconStart="@tui.x"
(click.stop)="value = null"
>
{{ 'Delete' | i18n }}
</button>
</tui-chip>
} @else {
<small>{{ 'Click or drop file here' | i18n }}</small>
}
</div>
<div class="drop" [class.drop_hidden]="!drop">
{{ 'Drop file here' | i18n }}
</div>
</ng-template>
</label>
`,
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
> {}

View File

@@ -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: `
<tui-textfield multi [disabledItemHandler]="disabledItemHandler">
@if (spec.name) {
<label tuiLabel>{{ spec.name }}</label>
}
<select
tuiMultiSelect
[invalid]="control.invalid()"
[disabled]="disabled"
[readOnly]="readOnly"
[items]="items"
[(ngModel)]="selected"
(blur)="control.onTouched()"
></select>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
`,
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,

View File

@@ -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: `
<tui-textfield [tuiNumberFormat]="{ precision, decimalMode: 'not-zero' }">
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<input
tuiInputNumber
[postfix]="spec.units ? ' ' + spec.units : ''"
[min]="spec.min"
[max]="spec.max"
[step]="spec.step || 0"
[invalid]="control.invalid()"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[placeholder]="spec.placeholder || ''"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
`,
imports: [
FormsModule,
TuiTextfield,
TuiInputNumber,
TuiNumberFormat,
TuiIcon,
TuiTooltip,
HintPipe,
],
})
export class FormNumberComponent extends Control<IST.ValueSpecNumber, number> {
get precision(): number {
return this.spec.integer ? 0 : Infinity
}
}

View File

@@ -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: `
<tui-textfield
[tuiTextfieldCleaner]="false"
[disabledItemHandler]="disabledItemHandler"
(tuiActiveZoneChange)="!$event && control.onTouched()"
>
@if (spec.name) {
<label tuiLabel>{{ spec.name }} *</label>
}
@if (mobile) {
<select
tuiSelect
[disabled]="disabled"
[readOnly]="readOnly"
[invalid]="control.invalid()"
[placeholder]="spec.name"
[items]="items"
[(ngModel)]="selected"
></select>
} @else {
<input
tuiSelect
[disabled]="disabled"
[readOnly]="readOnly"
[invalid]="control.invalid()"
[placeholder]="spec.name"
[(ngModel)]="selected"
/>
}
<tui-data-list-wrapper *tuiTextfieldDropdown new [items]="items" />
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
`,
imports: [
FormsModule,
TuiTextfield,
TuiSelect,
TuiDataListWrapper,
TuiIcon,
TuiTooltip,
HintPipe,
],
})
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
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
}
}

View File

@@ -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: `
<tui-textfield>
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<input
tuiTextfield
[attr.inputmode]="spec.inputmode"
[attr.minLength]="spec.minLength"
[attr.maxLength]="spec.maxLength"
[style.-webkit-text-security]="spec.masked && masked ? 'disc' : null"
[placeholder]="spec.placeholder || ''"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[invalid]="control.invalid()"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
@if (spec.generate) {
<button
tuiIconButton
type="button"
appearance="icon"
title="Generate"
size="xs"
iconStart="@tui.refresh-ccw"
(click)="generate()"
></button>
}
@if (spec.masked) {
<button
tuiIconButton
type="button"
appearance="icon"
title="Toggle masking"
size="xs"
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
(click)="masked = !masked"
></button>
}
<button
tuiIconButton
type="button"
iconStart="@tui.trash"
appearance="icon"
size="xs"
title="Remove"
class="remove"
(click)="remove()"
></button>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
`,
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<IST.ValueSpecText, string> {
private readonly el = tuiInjectElement()
masked = true
generate() {
this.value = utils.getDefaultString(this.spec.generate || '')
}
remove() {
this.el.dispatchEvent(new CustomEvent('remove', { bubbles: true }))
}
}

View File

@@ -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: `
<tui-textfield>
@if (spec.name) {
<label tuiLabel>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</label>
}
<textarea
placeholder="Placeholder"
tuiTextarea
[max]="6"
[min]="3"
[attr.maxLength]="spec.maxLength"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[placeholder]="spec.placeholder || ''"
[invalid]="control.invalid()"
[(ngModel)]="value"
(blur)="control.onTouched()"
></textarea>
@if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" />
}
</tui-textfield>
`,
imports: [
FormsModule,
TuiTextfield,
TuiTextarea,
TuiIcon,
TuiTooltip,
HintPipe,
],
})
export class FormTextareaComponent extends Control<
IST.ValueSpecTextarea,
string
> {}

View File

@@ -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) {
<tui-icon [tuiTooltip]="spec | hint" />
}
<input
tuiSwitch
type="checkbox"
size="m"
[disabled]="!!spec.disabled || readOnly"
[showIcons]="false"
[(ngModel)]="value"
(blur)="control.onTouched()"
/>
`,
host: { class: 'g-toggle' },
imports: [TuiIcon, TuiTooltip, HintPipe, TuiSwitch, FormsModule],
})
export class FormToggleComponent extends Control<
IST.ValueSpecToggle,
boolean
> {}

View File

@@ -1,57 +0,0 @@
<div class="label">
{{ spec.name }}
@if (spec.description || spec.disabled) {
<tui-icon [tuiTooltip]="spec | hint" />
}
<button
tuiLink
type="button"
class="add"
[disabled]="!canAdd"
(click)="add()"
>
+ {{ 'Add' | i18n }}
</button>
</div>
<tui-error [error]="order | tuiFieldError | async" />
@for (item of array.control.controls; track item) {
@if (spec.spec.type === 'object') {
<form-object
class="object"
[class.object_open]="!!open.get(item)"
[formGroup]="$any(item)"
[spec]="$any(spec.spec)"
[@tuiHeightCollapse]="animation"
[@tuiFadeIn]="animation"
[open]="!!open.get(item)"
(openChange)="open.set(item, $event)"
>
{{ item.value | mustache: $any(spec.spec).displayAs }}
<ng-container *ngTemplateOutlet="remove" />
</form-object>
} @else {
<form-control
class="control"
tuiTextfieldSize="m"
[tuiTextfieldLabelOutside]="true"
[tuiTextfieldIcon]="remove"
[formControl]="$any(item)"
[spec]="$any(spec.spec)"
[@tuiHeightCollapse]="animation"
[@tuiFadeIn]="animation"
/>
}
<ng-template #remove>
<button
tuiIconButton
type="button"
class="remove"
iconStart="@tui.trash"
appearance="icon"
size="m"
title="Remove"
(click.stop)="removeAt($index)"
></button>
</ng-template>
}

View File

@@ -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;
}

View File

@@ -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<AbstractControl, boolean>()
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<boolean>({
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)
}
}

View File

@@ -1,30 +0,0 @@
<tui-input
[maskito]="mask"
[tuiTextfieldCustomContent]="color"
[tuiTextfieldCleaner]="false"
[tuiHintContent]="spec | hint"
[readOnly]="readOnly"
[disabled]="!!spec.disabled"
[pseudoInvalid]="invalid"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</tui-input>
<ng-template #color>
<div class="wrapper" [style.color]="value">
@if (!readOnly && !spec.disabled) {
<input
type="color"
class="color"
tabindex="-1"
[(ngModel)]="value"
(click.stop)="(0)"
/>
}
<tui-icon icon="@tui.paint-bucket" tuiAppearance="icon" class="icon" />
</div>
</ng-template>

View File

@@ -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;
}
}

View File

@@ -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<IST.ValueSpecColor, string> {
readonly mask: MaskitoOptions = {
mask: ['#', ...Array(6).fill(/[0-9a-f]/i)],
}
}

View File

@@ -1,58 +0,0 @@
@switch (spec.type) {
@case ('color') {
<form-color />
}
@case ('datetime') {
<form-datetime />
}
@case ('file') {
<form-file />
}
@case ('multiselect') {
<form-multiselect />
}
@case ('number') {
<form-number />
}
@case ('select') {
<form-select />
}
@case ('text') {
<form-text />
}
@case ('textarea') {
<form-textarea />
}
@case ('toggle') {
<form-toggle />
}
}
<tui-error [error]="order | tuiFieldError | async" />
@if (spec.warning || immutable) {
<ng-template #warning let-completeWith="completeWith">
{{ spec.warning }}
@if (immutable) {
<p>{{ 'This value cannot be changed once set' | i18n }}!</p>
}
<div class="buttons">
<button
tuiButton
type="button"
appearance="secondary"
size="s"
(click)="completeWith(true)"
>
{{ 'Cancel' | i18n }}
</button>
<button
tuiButton
type="button"
appearance="flat-grayscale"
size="s"
(click)="completeWith(false)"
>
{{ 'Continue' | i18n }}
</button>
</div>
</ng-template>
}

View File

@@ -1,11 +0,0 @@
:host {
display: block;
}
.buttons {
margin-top: 0.5rem;
:first-child {
margin-right: 0.5rem;
}
}

View File

@@ -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<IST.ValueSpec, IST.ValueSpecHidden>,
V,
> extends AbstractTuiNullableControl<V> {
private readonly alerts = inject(TuiAlertService)
private readonly i18n = inject(i18nPipe)
@Input({ required: true })
spec!: T
@ViewChild('warning')
warning?: TemplateRef<TuiDialogContext<boolean>>
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<boolean>(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
}
}

View File

@@ -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<IST.ValueSpec, IST.ValueSpecHidden>,
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',
}),
},
]

View File

@@ -1,54 +0,0 @@
<ng-container [tuiHintContent]="spec.description">
@switch (spec.inputmode) {
@case ('time') {
<tui-input-time
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[ngModel]="getTime(value)"
(ngModelChange)="value = $event?.toString() || null"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</tui-input-time>
}
@case ('date') {
<tui-input-date
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</tui-input-date>
}
@case ('datetime-local') {
<tui-input-date-time
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper: getLimit) : min"
[max]="spec.max ? (spec.max | tuiMapper: getLimit) : max"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
</tui-input-date-time>
}
}
</ng-container>

View File

@@ -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)),
]
}
}

View File

@@ -1,42 +0,0 @@
<label tuiInputFiles>
<input
tuiInputFiles
[invalid]="invalid"
[accept]="spec.extensions.join(',')"
[(ngModel)]="value"
(blur)="onFocus(false)"
/>
<ng-template let-drop>
<div class="template" [class.template_hidden]="drop">
<div class="label">
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
@if (spec.description) {
<tui-icon [tuiTooltip]="spec.description" />
}
</div>
@if (value) {
<tui-chip>
{{ value.name }}
<button
tuiIconButton
type="button"
appearance="icon"
size="xs"
iconStart="@tui.x"
(click.stop)="value = null"
>
{{ 'Delete' | i18n }}
</button>
</tui-chip>
} @else {
<small>{{ 'Click or drop file here' | i18n }}</small>
}
</div>
<div class="drop" [class.drop_hidden]="!drop">
{{ 'Drop file here' | i18n }}
</div>
</ng-template>
</label>

View File

@@ -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;
}

View File

@@ -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
> {}

View File

@@ -1,30 +0,0 @@
@for (entry of spec | keyvalue: asIsOrder | filterHidden; track entry) {
<ng-container [tuiTextfieldCleaner]="true">
@switch (entry.value.type) {
@case ('object') {
<form-object
class="g-form-control"
[formGroupName]="entry.key"
[spec]="$any(entry.value)"
/>
}
@case ('union') {
<form-union
class="g-form-control"
[formGroupName]="entry.key"
[spec]="$any(entry.value)"
/>
}
@case ('list') {
<form-array [formArrayName]="entry.key" [spec]="$any(entry.value)" />
}
@default {
<form-control
class="g-form-control"
[formControlName]="entry.key"
[spec]="entry.value"
/>
}
}
</ng-container>
}

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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,
}),
]

View File

@@ -1,18 +0,0 @@
<tui-multi-select
[tuiHintContent]="spec | hint"
[disabled]="disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[editable]="false"
[disabledItemHandler]="disabledItemHandler"
[(ngModel)]="selected"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<select
tuiSelect
multiple
[items]="items"
[disabledItemHandler]="disabledItemHandler"
></select>
</tui-multi-select>

View File

@@ -1,22 +0,0 @@
<tui-input-number
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[tuiTextfieldPostfix]="spec.units || ''"
[pseudoInvalid]="invalid"
[tuiNumberFormat]="{
precision: spec.integer ? 0 : Infinity,
decimalMode: 'not-zero',
}"
[min]="spec.min ?? -Infinity"
[max]="spec.max ?? Infinity"
[step]="spec.step || 0"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
<input tuiTextfieldLegacy [placeholder]="spec.placeholder || ''" />
</tui-input-number>

View File

@@ -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<IST.ValueSpecNumber, number> {
protected readonly Infinity = Infinity
}

View File

@@ -1,23 +0,0 @@
<h3 class="title" (click)="toggle()">
<button
tuiIconButton
size="s"
iconStart="@tui.chevron-down"
type="button"
class="button"
[class.button_open]="open"
[style.border-radius.%]="100"
[appearance]="invalid ? 'primary-destructive' : 'secondary'"
></button>
<ng-content />
{{ spec.name }}
@if (spec.description) {
<tui-icon [tuiTooltip]="spec.description" (click.stop)="(0)" />
}
</h3>
<tui-expand class="expand" [expanded]="open">
<div class="g-form-group" [class.g-form-group_invalid]="invalid">
<form-group [spec]="spec.spec" />
</div>
</tui-expand>

View File

@@ -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);
}
}

View File

@@ -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<boolean>()
private readonly container = inject(ControlContainer)
get invalid() {
return !this.container.valid && this.container.touched
}
toggle() {
this.open = !this.open
this.openChange.emit(this.open)
}
}

View File

@@ -1,17 +0,0 @@
<tui-select
[tuiHintContent]="spec | hint"
[disabled]="disabled"
[readOnly]="readOnly"
[tuiTextfieldCleaner]="false"
[pseudoInvalid]="invalid"
[(ngModel)]="selected"
(focusedChange)="onFocus($event)"
>
{{ spec.name }} *
<select
tuiSelect
[placeholder]="spec.name"
[items]="items"
[disabledItemHandler]="disabledItemHandler"
></select>
</tui-select>

View File

@@ -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<IST.ValueSpecSelect, string> {
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
}
}

View File

@@ -1,48 +0,0 @@
<tui-input
[tuiTextfieldCustomContent]="spec.masked || spec.generate ? toggle : ''"
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
<input
tuiTextfieldLegacy
[class.masked]="spec.masked && masked"
[placeholder]="spec.placeholder || ''"
[attr.minLength]="spec.minLength"
[attr.maxLength]="spec.maxLength"
[attr.inputmode]="spec.inputmode"
/>
</tui-input>
<ng-template #toggle>
@if (spec.generate) {
<button
tuiIconButton
type="button"
appearance="icon"
title="Generate"
size="xs"
class="button"
iconStart="@tui.refresh-ccw"
(click)="generate()"
></button>
}
@if (spec.masked) {
<button
tuiIconButton
type="button"
appearance="icon"
title="Toggle masking"
size="xs"
class="button"
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
(click)="masked = !masked"
></button>
}
</ng-template>

View File

@@ -1,8 +0,0 @@
.button {
pointer-events: auto;
margin-left: 0.25rem;
}
.masked {
-webkit-text-security: disc;
}

View File

@@ -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<IST.ValueSpecText, string> {
masked = true
generate() {
this.value = utils.getDefaultString(this.spec.generate || '')
}
}

View File

@@ -1,20 +0,0 @@
<tui-textarea
[tuiHintContent]="spec | hint"
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[expandable]="true"
[rows]="6"
[maxLength]="spec.maxLength"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
@if (spec.required) {
<span>*</span>
}
<textarea
tuiTextfieldLegacy
[placeholder]="spec.placeholder || ''"
></textarea>
</tui-textarea>

View File

@@ -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
> {}

View File

@@ -1,13 +0,0 @@
{{ spec.name }}
@if (spec.description || spec.disabled) {
<tui-icon [tuiTooltip]="spec | hint" />
}
<input
tuiSwitch
type="checkbox"
size="m"
[disabled]="!!spec.disabled || readOnly"
[showIcons]="false"
[(ngModel)]="value"
(blur)="onFocus(false)"
/>

View File

@@ -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
> {}

View File

@@ -1,11 +0,0 @@
<form-control
[spec]="selectSpec"
formControlName="selection"
(tuiValueChanges)="onUnion($event)"
></form-control>
<tui-elastic-container class="g-form-group" formGroupName="value">
<form-group
class="group"
[spec]="(union && spec.variants[union]?.spec) || {}"
></form-group>
</tui-elastic-container>

View File

@@ -1,8 +0,0 @@
:host {
display: block;
}
.group {
display: block;
margin-top: 1rem;
}

View File

@@ -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 {}

View File

@@ -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)
}
}

View File

@@ -4,7 +4,6 @@ import { KeyValue } from '@angular/common'
@Pipe({
name: 'filterHidden',
standalone: false,
})
export class FilterHiddenPipe implements PipeTransform {
transform(value: KeyValue<string, IST.ValueSpec>[]) {

View File

@@ -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)

View File

@@ -3,7 +3,6 @@ import Mustache from 'mustache'
@Pipe({
name: 'mustache',
standalone: false,
})
export class MustachePipe implements PipeTransform {
transform(value: any, displayAs: string): string {

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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,