mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
refactor: refactor forms components and remove legacy Taiga UI package (#3012)
This commit is contained in:
204
web/package-lock.json
generated
204
web/package-lock.json
generated
@@ -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
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
> {}
|
||||
@@ -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,
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
> {}
|
||||
@@ -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
|
||||
> {}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)],
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
:first-child {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}),
|
||||
},
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
> {}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
.button {
|
||||
pointer-events: auto;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.masked {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
@@ -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 || '')
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
> {}
|
||||
@@ -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)"
|
||||
/>
|
||||
@@ -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
|
||||
> {}
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>[]) {
|
||||
@@ -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)
|
||||
@@ -3,7 +3,6 @@ import Mustache from 'mustache'
|
||||
|
||||
@Pipe({
|
||||
name: 'mustache',
|
||||
standalone: false,
|
||||
})
|
||||
export class MustachePipe implements PipeTransform {
|
||||
transform(value: any, displayAs: string): string {
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user