Compare commits

...

149 Commits

Author SHA1 Message Date
Valentin-Gabriel Mihai
7f13fa62d3 Merge branch 'unprotected' into 'master'
update master

See merge request fforesight/shared-ui-libraries/common-ui!15
2024-12-13 13:15:01 +01:00
Dan Percic
4ed7215292 skip quality gate 2024-12-13 13:16:06 +02:00
Dan Percic
1ea220b489 Merge remote-tracking branch 'origin/master' into unprotected 2024-12-13 13:15:39 +02:00
Valentin Mihai
fc8be33dc6 RED-9580 - added getOne method for stats service to call new users stats endpoint and updated data from "delete user dialog" with new backend data 2024-12-13 12:02:51 +02:00
Valentin Mihai
58382ddfee RED-10301 - set app type config vars based existing tenants from local storage or used default config if no tenant was set before 2024-12-11 18:22:28 +02:00
Nicoleta Panaghiu
0d20afcf27 RED-10647: update manual base link depending on application type. 2024-12-11 18:08:21 +02:00
Nicoleta Panaghiu
aa5fc54576 RED-9856: support a smaller avatar size. 2024-12-10 17:02:25 +02:00
Christoph Schabert
e5c4fd7d3c add sonarqube config 2024-12-09 15:27:35 +01:00
Nicoleta Panaghiu
a4e3ed8854 RED-3800: removed debug statements. 2024-12-07 14:07:55 +02:00
Nicoleta Panaghiu
acabdba657 RED-3800: fixed sass update breaking changes. 2024-12-07 14:03:56 +02:00
Nicoleta Panaghiu
0e6e4f7b09 RED-10614: fixed missing injection context error, 2024-12-06 19:22:12 +02:00
Dan Percic
7bb24c4f91 lint 2024-12-05 12:50:45 +02:00
Dan Percic
e21c225ddd updates 2024-12-05 12:49:51 +02:00
Dan Percic
8582f2e6be ng 19 2024-12-05 11:46:15 +02:00
Dan Percic
b929f1d136 Merge branch 'VM/RED-10301' into 'unprotected'
RED-10301 - Use RM/DM UI depending on application type of tenant

See merge request fforesight/shared-ui-libraries/common-ui!14
2024-12-03 13:16:28 +01:00
Valentin Mihai
55b21c4989 lint 2024-12-03 12:37:45 +02:00
Valentin Mihai
911516add2 RED-10301 - Use RM/DM UI depending on application type of tenant 2024-12-03 12:31:24 +02:00
Nicoleta Panaghiu
cbdfcf4d8f RED-10517: fixed selected sorting. 2024-11-27 16:52:17 +02:00
Nicoleta Panaghiu
72e760fff8 RED-9885: scoped right-container transition. 2024-11-27 16:24:51 +02:00
Nicoleta Panaghiu
ae0eebcc6f RED-10509: added disableStopPropagation option for action config. 2024-11-24 13:30:43 +02:00
Timo Bejan
7c6f9fc25e removed logging 2024-11-06 15:49:04 +02:00
Timo Bejan
310cc4bb51 Removed offset and fixed notification polling 2024-11-06 15:39:31 +02:00
Valentin Mihai
99facc0434 RED-10373 - updated details radio option description to can receive number params 2024-11-04 16:55:40 +02:00
Dan Percic
ebaf1709b1 add some overrides & keycloak token$ 2024-11-01 19:20:42 +02:00
Nicoleta Panaghiu
17d2e8c530 RED-10206: added disabled and tooltip directives for button config. 2024-11-01 14:46:19 +02:00
Dan Percic
b9c01a287c lint fixes 2024-10-31 11:28:14 +02:00
Dan Percic
6c7865a8ec fix filters track expression 2024-10-31 11:26:49 +02:00
Nicoleta Panaghiu
9e457a13b4 RED-8277: only show pointer cursor if entity has routerLink. 2024-10-23 16:09:16 +03:00
Nicoleta Panaghiu
fba9b330dd RED-8277: use pointer cursor for table items. 2024-10-22 17:55:52 +03:00
Valentin Mihai
d7ad450ca1 RED-7340 - red border around the page input field should only appear after the user has finished entering the page range 2024-10-22 17:19:48 +03:00
Valentin Mihai
a5d10ad148 RED-7340 - When a user has entered an invalid page range string, display a read border around the input field 2024-10-21 17:52:48 +03:00
Nicoleta Panaghiu
e92bd55cfc RED-3800: some fixes after switching from div to a. 2024-10-17 11:59:38 +03:00
Nicoleta Panaghiu
f36b1fa8e2 RED-8277: use a instead of div. 2024-10-16 15:26:29 +03:00
Nicoleta Panaghiu
3f214d9726 RED-10139: moved formToSignal functions to common.
Future refactoring purposes.
2024-10-16 13:44:53 +03:00
Valentin Mihai
ba85260cc4 RED-7340 - set fix width for extra option input 2024-10-16 11:55:33 +03:00
Nicoleta Panaghiu
e88929f0d4 RED-10180: increased extraOption container width. 2024-10-14 17:25:31 +03:00
Nicoleta Panaghiu
32de775859 RED-10183: set default language in user preference. 2024-10-14 12:48:58 +03:00
Valentin Mihai
3c89b8f7e7 RED-7340 - remove not used imports 2024-10-05 20:48:00 +03:00
Valentin Mihai
34387d49d2 RED-7340 - updated details radio component to have an option input when is needed 2024-09-27 16:41:59 +03:00
Nicoleta Panaghiu
304657d259 RED-9985: prevent filter categories from collapsing upon selection. 2024-09-26 11:51:52 +03:00
Nicoleta Panaghiu
835cb7820e RED-9578: move custom component above the question. 2024-09-25 13:41:37 +03:00
Adina Țeudan
3724a6c0b6 Ignored keys for form changed in dialogs 2024-09-23 22:23:02 +03:00
Nicoleta Panaghiu
c71a4995a6 RED-9578: added support for custom component and reversing buttons. 2024-09-19 14:52:04 +03:00
Valentin Mihai
c644eaeba2 RED-7345 - added force-annotation class 2024-09-16 21:15:52 +03:00
Nicoleta Panaghiu
2faecb44a9 RED-9372: always include scroll-bar mixin. 2024-09-10 11:37:55 +03:00
Nicoleta Panaghiu
81513d34dc RED-9372: fixed table-items moving on hover. 2024-09-05 17:14:22 +03:00
Nicoleta Panaghiu
9bc05f1165 RED-9987: added sendSetPasswordMail flag. 2024-09-03 13:45:40 +03:00
Nicoleta Panaghiu
007e761bd5 RED-9916: fixed button ids. 2024-08-20 13:22:07 +03:00
Nicoleta Panaghiu
0ca5e7e2ad RED-9777: increase dialog help button z-index. 2024-08-14 11:42:31 +03:00
Nicoleta Panaghiu
6547eb2ad5 RED-9777: fixed viewer header alignment + added components view parent. 2024-08-12 11:15:53 +03:00
Adina Țeudan
17943f2e8d HasScrollbar directive with signals 2024-08-09 23:51:51 +03:00
Adina Țeudan
c8b3e3eb3c Revert "RED-9372: fixed table-item elements moving on hover when scrollable."
This reverts commit 845c819392bc5f73a5feb584dc879e965107a9c0.
2024-08-09 23:51:21 +03:00
Nicoleta Panaghiu
c331a61309 RED-9817: removed HTML code for space. 2024-08-09 12:15:50 +03:00
Valentin Mihai
5b51eb85a1 RED-9788 - remove active listing entity service 2024-08-09 08:56:48 +03:00
Valentin Mihai
f079b6c157 RED-9201 - update upload component height 2024-08-08 11:05:01 +03:00
Adina Țeudan
a8b5cfce14 Unordered async guards 2024-08-07 21:30:41 +03:00
Adina Țeudan
85c6d91b2e Some updates 2024-08-07 21:14:40 +03:00
Valentin Mihai
7f8336a64f RED-3800 - Refactoring / Code Cleanup 2024-08-07 20:32:30 +03:00
Valentin Mihai
1155c16f8a RED-3800 - Refactoring / Code Cleanup 2024-08-07 20:29:44 +03:00
Valentin Mihai
66277814c5 RED-9776 - added active entity class 2024-08-04 12:30:14 +03:00
Nicoleta Panaghiu
0fa39cf900 RED-9504: use translate pipe & clean imports. 2024-08-02 11:33:53 +03:00
Nicoleta Panaghiu
28acee573d RED-9731: fixed help button position on resize + small filters fix. 2024-08-01 14:38:32 +03:00
Nicoleta Panaghiu
80fceb45dd RED-9516: fixed filter options alignment. 2024-07-30 17:03:14 +03:00
Nicoleta Panaghiu
fb4b58a496 RED-9433: added auto line-break for checkbox label overflow. 2024-07-30 14:14:13 +03:00
Nicoleta Panaghiu
9bbaecb7e5 RED-9571: fixed initials-avatar not updating in some cases. 2024-07-30 11:12:16 +03:00
Valentin Mihai
9df87dc218 RED-9201 - addded id to file name label 2024-07-26 15:23:45 +03:00
Adina Țeudan
646e16557f Added icons 2024-07-25 18:43:09 +03:00
Nicoleta Panaghiu
98ac49fbc8 RED-9657: fixed help mode links. 2024-07-16 13:32:58 +03:00
Nicoleta Panaghiu
845c819392 RED-9372: fixed table-item elements moving on hover when scrollable. 2024-07-15 16:11:11 +03:00
Adina Țeudan
c33ae3c918 getparamFromDialog fn 2024-07-12 18:51:08 +03:00
Nicoleta Panaghiu
88743acacc RED-9437: fix help mode button placement in dialogs. 2024-07-09 14:48:40 +03:00
Dan Percic
9b1179d99a Signal updates 2024-07-09 12:11:15 +03:00
Valentin Mihai
01c244aa07 RED-9513 - Enter Key Not Working: Unable to Add Terms to Dictionary 2024-07-04 12:52:26 +03:00
Valentin Mihai
ce334dbdeb RED-9362 - Help Mode tooltip missing 2024-06-27 15:32:15 +03:00
Valentin Mihai
1c48cea02e RED-9453 - prevent enter key 2024-06-27 14:53:44 +03:00
Valentin Mihai
ca0db25484 RED-9390 - Workload filter does not remember previous ALL or NONE selection 2024-06-27 13:23:35 +03:00
Dan Percic
f872158c5f Merge branch 'VM/RED-8748' into 'unprotected'
RED-8748 - Integrated component view in DocuMine

See merge request fforesight/shared-ui-libraries/common-ui!12
2024-06-26 14:49:35 +02:00
Valentin Mihai
f24e5dbb82 RED-8748 - added hidden property to filter model 2024-06-25 00:21:16 +03:00
Adina Țeudan
590ebcbae2 Moved fullscreen icons to common-ui 2024-06-20 14:57:38 +03:00
Dan Percic
f3faa6a6cc migrate inputs to signals & remove inputs/index.ts 2024-06-20 11:36:47 +03:00
Dan Percic
d595a22db1 unfuck circular imports 2024-06-19 13:05:12 +03:00
Nicoleta Panaghiu
04ae68891c Merge branch 'unprotected' into RED-9321 2024-06-18 12:45:20 +03:00
Dan Percic
f60ea513ac migrate control flow 2024-06-18 10:22:27 +03:00
Dan Percic
0b64044f57 lint 2024-06-17 21:27:16 +03:00
Dan Percic
0738d8c4ef migrate buttons to signal inputs 2024-06-17 21:26:02 +03:00
Dan Percic
76771023c2 lint 2024-06-17 19:18:01 +03:00
Dan Percic
213cabce7a ng 18 2024-06-17 19:15:43 +03:00
Adina Țeudan
e165ba7500 Clamp filter values 2024-06-17 15:59:51 +03:00
Adina Țeudan
4c0437d7fc Simple popup filter improvements 2024-06-17 15:29:26 +03:00
Adina Țeudan
aec5da15df Simple popup filter 2024-06-17 12:18:25 +03:00
Nicoleta Panaghiu
40c6f881fc RED-9321: add new file to index. 2024-06-14 15:06:29 +03:00
Nicoleta Panaghiu
a764ab1050 RED-9321: refactored help mode module. 2024-06-14 14:59:33 +03:00
Nicoleta Panaghiu
960b434ef6 RED-9321: made some components standalone, removed upload-file module. 2024-06-12 14:33:00 +03:00
Dan Percic
748cce4032 Merge branch 'VM/RED-8748' into 'unprotected'
RED-8748 - Integrated component view in DocuMine

See merge request fforesight/shared-ui-libraries/common-ui!11
2024-06-11 19:53:00 +02:00
Valentin Mihai
4ccde998e0 Merge branch 'unprotected' into VM/RED-8748 2024-06-11 20:17:46 +03:00
Adina Țeudan
04eaca1600 Moved visibility icons to common-ui 2024-06-10 16:22:48 +03:00
Valentin Mihai
e733adf374 Merge branch 'unprotected' into VM/RED-8748 2024-06-10 14:41:24 +03:00
Valentin Mihai
5eef6d403a check for existing help mode button 2024-06-06 18:29:48 +03:00
Adina Țeudan
5b7dd6a55a Added clamp-5 mixin 2024-05-29 21:01:50 +03:00
Valentin Mihai
3a9f36e71b RED-8882 - Help Mode design improvements 2024-05-29 10:52:33 +03:00
Valentin Mihai
a668b23118 Merge branch 'unprotected' into VM/RED-8748 2024-05-22 21:42:37 +03:00
Valentin Mihai
e7fca876bb RED-8882 - made help button smaller 2024-05-22 21:31:22 +03:00
Adina Țeudan
e8f5bc8f2c RED-6959: Added selected items count to table header 2024-05-22 14:37:08 +03:00
Valentin Mihai
1674055943 Merge branch 'unprotected' into VM/RED-8748 2024-05-20 19:56:44 +03:00
Valentin Mihai
4811d301e6 RED-8882 - removed base dialog 2024-05-20 19:06:58 +03:00
Valentin Mihai
174d77d2ea RED-8882 - moved help mode dialog preference into iqser-user-preferences.service 2024-05-20 17:10:25 +03:00
Valentin Mihai
848e46cf6a removed user preferences service from help mode service 2024-05-20 16:27:20 +03:00
Valentin Mihai
b7c8634407 removed base dialog from help mode dialog 2024-05-20 14:31:30 +03:00
Valentin Mihai
5cc09f251e fix build 2024-05-20 10:39:36 +03:00
Valentin Mihai
ee1d7b2445 Merge branch 'VM/RED-8882' into unprotected 2024-05-20 10:35:38 +03:00
Valentin Mihai
9473a7f5bd Merge branch 'unprotected' into VM/RED-8748 2024-05-17 19:09:48 +03:00
Valentin Mihai
217a44c2e0 removed not needed imports 2024-05-17 18:59:48 +03:00
Valentin Mihai
0021307c71 RED-8882 - Help Mode design improvements 2024-05-17 17:50:32 +03:00
Adina Țeudan
6f288516e3 Minor UI fixes, base dialog improvements 2024-05-16 23:31:52 +03:00
Valentin Mihai
252c261621 lint 2024-05-16 18:47:35 +03:00
Valentin Mihai
786d235de0 RED-8882 - Help Mode design improvements 2024-05-16 18:46:25 +03:00
Nicoleta Panaghiu
e737d134ad RED-9097: fixed workflow file attributes not updating. 2024-05-08 13:37:47 +03:00
Valentin Mihai
3b0b6542cd lint 2024-04-30 16:09:34 +03:00
Valentin Mihai
61d4d4f5c6 RED-8748 - updated filter service 2024-04-30 16:06:25 +03:00
Adina Țeudan
12c5c1cb4e Fix 2024-04-29 17:06:07 +03:00
Adina Țeudan
02a9cb49e9 Inputs styles 2024-04-29 17:03:34 +03:00
Adina Țeudan
fb6951a7ba Some styles, paginated get all ids 2024-04-29 16:59:35 +03:00
Adina Țeudan
e7a04dda9e Loading indicator z-index, include ngx-toastr in common styles 2024-04-29 15:18:39 +03:00
Adina Țeudan
696e7f6f6d Paginated entities service updates (sorting) 2024-04-18 00:22:45 +03:00
Adina Țeudan
d4b69a18a9 Deprecated (close) -> (closed) 2024-04-17 21:01:13 +03:00
Adina Țeudan
4cf42c8943 Fixed filter card input padding 2024-04-17 20:32:51 +03:00
Adina Țeudan
fae7d912d2 Fixed filter card input padding 2024-04-17 20:32:19 +03:00
Adina Țeudan
fc06bcc31d Use flex gap instead of margin right in tables 2024-04-11 18:25:01 +03:00
Adina Țeudan
005167487d Enable multiselect checkbox 2024-04-11 15:43:37 +03:00
Adina Țeudan
c12ed0c968 Transform "active" boolean attribute in round-checkbox 2024-04-11 15:29:28 +03:00
Adina Țeudan
cbd9fd055b RED-8731: Updated material & fixed toggle appearance 2024-04-11 15:14:58 +03:00
Adina Țeudan
301ea99abe CSS updates 2024-04-09 00:17:42 +03:00
Adina Țeudan
8a2033740e Moved color-picker to common-ui 2024-04-07 21:32:38 +03:00
Nicoleta Panaghiu
c12b6f4d35 RED-8622: implemented add entity functionality. 2024-04-05 13:07:32 +03:00
Adina Țeudan
e08e28e095 Fixed icon button hover ripple color 2024-04-04 17:08:39 +03:00
Nicoleta Panaghiu
957dc404a8 RED-8817: decreased extra option description opacity. 2024-03-26 15:52:04 +02:00
Nicoleta Panaghiu
0dd210b04f RED-8817: aligned description with option label. 2024-03-25 10:46:12 +02:00
Adina Țeudan
b6813bccf3 Added prevent default to stop propagation directive 2024-03-22 00:17:13 +02:00
Nicoleta Panaghiu
48a5160d6f RED-8817: added description support for extraOption. 2024-03-21 17:01:37 +02:00
Adina Țeudan
d67798bd60 Pagination updates 2024-03-18 17:22:45 +02:00
Nicoleta Panaghiu
292573f3b3 RED-8728: made the required asterisk red. 2024-03-15 15:03:58 +02:00
Adina Țeudan
55dcb40dcd Button styles 2024-03-12 22:13:01 +02:00
Adina Țeudan
ecf9c8912e Upload file component updates 2024-03-07 18:15:32 +02:00
Dan Percic
9b427b0cac add a devInfo method to toaster service 2024-03-07 15:57:11 +02:00
Adina Țeudan
47e371a41e Refactor 2024-03-06 17:25:53 +02:00
Adina Țeudan
9faca532c6 Rename, cleanup 2024-03-06 17:04:38 +02:00
Adina Țeudan
bf628b33a2 Paginated entities service: search support 2024-03-06 17:02:44 +02:00
Nicoleta Panaghiu
3ea4e45b87 RED-8679: added support for custom table item id formats. 2024-03-01 17:07:58 +02:00
Nicoleta Panaghiu
94e0021ed4 RED-8226: added support for raw toastr messages. 2024-02-28 12:56:39 +02:00
217 changed files with 3155 additions and 1878 deletions

View File

@ -6,7 +6,7 @@ module.exports = {
globals: {
NodeJS: true,
},
ignorePatterns: ['!**/*'],
ignorePatterns: ['!**/*', 'jest.config.ts'],
overrides: [
{
files: ['*.ts'],
@ -206,6 +206,7 @@ module.exports = {
],
rules: {
'rxjs/no-ignored-subscription': 'error',
'@angular-eslint/prefer-standalone': 'off',
'@angular-eslint/directive-selector': [
'error',
{

19
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,19 @@
sonarqube:
stage: test
image:
name: sonarsource/sonar-scanner-cli:11.1
entrypoint:
- ''
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: '0'
cache:
key: "${CI_JOB_NAME}"
paths:
- ".sonar/cache"
script:
- sonar-scanner
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
- if: "$CI_COMMIT_BRANCH =~ /^release/"

2
sonar-project.properties Normal file
View File

@ -0,0 +1,2 @@
sonar.projectKey=common-ui
sonar.qualitygate.wait=false

View File

@ -0,0 +1,3 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 7.4L0 1.4L1.4 0L6 4.6L10.6 0L12 1.4L6 7.4Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -0,0 +1,3 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2.8L1.4 7.4L0 6L6 0L12 6L10.6 7.4L6 2.8Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 182 B

View File

@ -0,0 +1,15 @@
<svg id="Capa_1" style="enable-background:new 0 0 464.736 464.736;" viewBox="0 0 464.736 464.736"
x="0px"
xml:space="preserve" xmlns="http://www.w3.org/2000/svg" y="0px">
<g>
<path d="M446.598,18.143c-24.183-24.184-63.393-24.191-87.592-0.008l-16.717,16.717c-8.98-8.979-23.525-8.979-32.504,0
c-8.981,8.972-8.981,23.533,0,32.505l5.416,5.419L134.613,253.377h-0.016l-62.685,62.691c-4.982,4.982-7.919,11.646-8.235,18.684
l-0.15,3.344c0,0.016,0,0.03,0,0.046l-2.529,56.704c-0.104,2.633,0.883,5.185,2.739,7.048c1.751,1.759,4.145,2.738,6.63,2.738
c0.135,0,0.269,0,0.42-0.008l30.064-1.331h0.016l18.318-0.815l8.318-0.366c9.203-0.412,17.944-4.259,24.469-10.776l240.898-240.891
l4.506,4.505c4.49,4.488,10.372,6.733,16.252,6.733c5.881,0,11.764-2.245,16.253-6.733c8.98-8.973,8.98-23.534,0-32.505
l16.716-16.718C470.782,81.544,470.782,42.334,446.598,18.143z M272.639,227.33l-84.6,15.96l137.998-138.004l34.332,34.316
L272.639,227.33z" fill="currentColor" />
<path d="M64.5,423.872c-35.617,0-64.5,9.145-64.5,20.435c0,11.284,28.883,20.428,64.5,20.428s64.486-9.143,64.486-20.428
C128.986,433.016,100.117,423.872,64.5,423.872z" fill="currentColor" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="minimize" stroke="none" stroke-width="1">
<path
d="M40,60 L40,90 L30,90 L30,77 L7,100 L0,93 L23,70 L10,70 L10,60 L40,60 Z M90,60 L90,70 L77,70 L100,93 L93,100 L70,77 L70,90 L60,90 L60,60 L90,60 Z M93,0 L100,7 L77,30 L90,30 L90,40 L60,40 L60,10 L70,10 L70,23 L93,0 Z M7,0 L30,23 L30,10 L40,10 L40,40 L10,40 L10,30 L23,30 L0,7 L7,0 Z"
fill="currentColor" fill-rule="nonzero" id="Combined-Shape"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 642 B

View File

@ -0,0 +1,3 @@
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 12V10H11V12H7ZM3 7V5H15V7H3ZM0 2V0H18V2H0Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="full-screen" stroke="none" stroke-width="1">
<path
d="M36.5,56.5 L43.5,63.5 L17,90 L30,90 L30,100 L0,100 L0,70 L10,70 L10,83 L36.5,56.5 Z M63.5,56.5 L90,83 L90,70 L100,70 L100,100 L70,100 L70,90 L83,90 L56.5,63.5 L63.5,56.5 Z M100,0 L100,30 L90,30 L90,17 L63.5,43.5 L56.5,36.5 L83,10 L70,10 L70,0 L100,0 Z M30,0 L30,10 L17,10 L43.5,36.5 L36.5,43.5 L10,17 L10,30 L0,30 L0,0 L30,0 Z"
fill="currentColor" fill-rule="nonzero" id="Combined-Shape"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="20px" version="1.1" viewBox="0 0 20 20" width="20px" xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="evenodd" id="Help-Mode" stroke="none" stroke-width="1">
<g fill="currentColor" fill-rule="nonzero" id="01.-Help-button" transform="translate(-1408.000000, -645.000000)">
<g id="help-button" transform="translate(1294.000000, 635.000000)">
<g id="help" transform="translate(114.000000, 10.000000)">
<path
d="M10,0 C15.5,1.01033361e-15 20,4.5 20,10 C20,15.5 15.5,20 10,20 C4.5,20 3.55271368e-15,15.5 3.55271368e-15,10 C7.10542736e-15,4.5 4.5,-1.01033361e-15 10,0 Z M10,2 C5.6,2 2,5.6 2,10 C2,14.4 5.6,18 10,18 C14.4,18 18,14.4 18,10 C18,5.6 14.4,2 10,2 Z M10.86,12.9 L10.86,14.9 L8.86,14.9 L8.86,12.9 L10.86,12.9 Z M9.86,4.9 C11.56,4.9 12.86,6.2 12.86,7.9 C12.86,8.8 12.36,9.7 11.66,10.3 C11.3830769,10.4846154 10.9357396,10.839645 10.8685571,11.4437415 L10.86,11.6 L10.86,11.9 L8.86,11.9 L8.86,11.6 C8.86,10.5 9.46,9.4 10.46,8.7 C10.76,8.5 10.86,8.2 10.86,7.9 C10.86,7.3 10.46,6.9 9.86,6.9 C9.30285714,6.9 8.91816327,7.24489796 8.86604956,7.77456268 L8.86,7.9 L6.86,7.9 C6.86,6.2 8.16,4.9 9.86,4.9 Z"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,5 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
<path
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@ -0,0 +1,5 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@ -63,6 +63,8 @@ body {
--iqser-app-name-font-family: Inter, sans-serif;
--iqser-circle-button-radius: 50%;
--iqser-side-nav-item-radius: 20px;
--iqser-dot-overlay-background: rgba(var(--iqser-primary-rgb), 0.1);
--iqser-chevron-button-bg: transparent;
}
$required-variables: 'iqser-primary';

View File

@ -32,6 +32,7 @@
.buttons {
display: flex;
margin-right: 8px;
align-items: center;
> *:not(:last-child) {
margin-right: 14px;
@ -56,15 +57,11 @@
.dev-mode {
background-color: var(--iqser-primary);
color: var(--iqser-white);
font-size: 22px;
line-height: 16px;
text-align: center;
position: fixed;
top: 0;
z-index: 100;
right: 0;
height: var(--iqser-top-bar-height);
word-break: break-all;
writing-mode: vertical-rl;
text-orientation: upright;
display: flex;
justify-content: center;
align-items: center;

View File

@ -37,7 +37,7 @@
}
.overlay {
background: rgba(var(--iqser-primary-rgb), 0.1);
background: var(--iqser-dot-overlay-background);
}
}
@ -66,6 +66,14 @@ iqser-icon-button {
--mdc-text-button-label-text-color: var(--iqser-text);
padding: 0 14px;
width: 100%;
&:hover:not([disabled]) {
.mat-mdc-button-persistent-ripple::before {
background-color: #000;
opacity: 0.04;
}
}
&[disabled] {
--mdc-text-button-disabled-label-text-color: rgba(var(--iqser-text-rgb), 0.3);
@ -169,7 +177,6 @@ iqser-circle-button {
iqser-chevron-button {
@include buttonShape;
@include ariaExpanded;
@include dotOverlay;
@include labelNoWrap;
display: block;
@ -177,10 +184,14 @@ iqser-chevron-button {
.mat-mdc-button {
@include iconSize14;
background-color: var(--iqser-chevron-button-bg);
&:not([disabled]) {
--mdc-text-button-label-text-color: var(--iqser-text);
}
}
@include dotOverlay;
}
iqser-user-button {

View File

@ -5,6 +5,7 @@ $ripple-size: 26px;
flex: 0 0 $checkbox-size;
width: $checkbox-size;
height: $checkbox-size;
margin-top: 4px;
}
.mat-mdc-checkbox,
@ -24,10 +25,14 @@ $ripple-size: 26px;
--mdc-checkbox-selected-hover-state-layer-color: var(--iqser-primary);
--mdc-checkbox-selected-pressed-state-layer-color: var(--iqser-primary);
.mdc-form-field > label {
padding-left: 8px;
line-height: 24px;
white-space: nowrap;
.mdc-form-field {
align-items: start;
& > label {
padding-left: 8px;
line-height: 24px;
white-space: normal;
}
}
.mdc-checkbox__ripple {

View File

@ -18,9 +18,18 @@
&.large {
height: 32px;
width: 32px;
min-width: 32px;
font-size: var(--iqser-font-size);
}
&.extra-small {
height: 16px;
width: 16px;
min-width: 16px;
font-size: 10px;
font-weight: lighter;
}
&.gray-dark {
background-color: var(--iqser-user-avatar-1);
color: var(--iqser-text);

View File

@ -47,7 +47,10 @@
font-weight: bold;
padding-bottom: 8px;
}
}
&.redaction,
&.force-annotation {
iqser-details-radio {
padding-top: 20px;
}
@ -72,3 +75,7 @@
margin-left: auto;
}
}
.large-form-dialog .dialog > form {
display: contents;
}

View File

@ -0,0 +1,59 @@
.iqser-upload-file {
.upload-area,
.file-area {
display: flex;
align-items: center;
border-radius: 8px;
width: 100%;
box-sizing: border-box;
background: var(--iqser-alt-background);
height: 68px;
&.drag-over {
background-color: var(--iqser-file-drop-drag-over);
}
}
.upload-area {
gap: 16px;
cursor: pointer;
padding: 0 32px;
mat-icon,
div {
opacity: 0.5;
transition: 0.1s;
}
div {
font-size: 16px;
font-weight: 500;
}
}
.file-area {
gap: 10px;
mat-icon:first-child {
opacity: 0.5;
margin-left: 16px;
}
mat-icon:last-child {
margin-left: auto;
margin-right: 16px;
cursor: pointer;
}
mat-icon {
transform: scale(0.7);
}
p {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 490px;
}
}
}

View File

@ -7,11 +7,11 @@
.full-page-section {
opacity: 0.7;
background: var(--iqser-background);
z-index: 900;
z-index: 1001;
}
.full-page-content {
z-index: 1000;
z-index: 1002;
justify-content: center;
align-items: center;
flex-direction: column;

View File

@ -94,29 +94,20 @@ iqser-dynamic-input {
input:not([type='checkbox']),
textarea {
box-sizing: border-box;
margin-top: 3px;
min-height: var(--iqser-inputs-height);
line-height: 32px;
padding-left: calc((var(--iqser-inputs-height) - 14px) / 2);
padding-right: calc((var(--iqser-inputs-height) - 14px) / 2);
}
.mat-mdc-form-field {
margin-top: 3px;
input {
margin-top: 0;
}
}
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
.mdc-text-field--outlined {
--mdc-outlined-text-field-focus-outline-width: 1px;
--mdc-shape-small: 8px; // border-radius
--mdc-outlined-text-field-container-shape: 8px; // border-radius
--mdc-shape-small: 8px;
--mdc-outlined-text-field-container-shape: 8px;
border-bottom-left-radius: var(--mdc-shape-small);
border-bottom-right-radius: var(--mdc-shape-small);
}
@ -220,7 +211,7 @@ iqser-dynamic-input {
font-size: 11px;
letter-spacing: 0;
line-height: 14px;
margin-bottom: 2px;
margin-bottom: 5px;
color: var(--iqser-text);
&.mat-checkbox-layout {
@ -231,7 +222,7 @@ iqser-dynamic-input {
&.required label:after {
content: ' *';
color: var(--iqser-primary);
color: var(--iqser-red-1);
}
&.datepicker-wrapper {

View File

@ -156,13 +156,15 @@ section.settings {
box-sizing: border-box;
background: var(--iqser-background);
overflow: hidden;
transition:
width ease-in-out 0.2s,
min-width ease-in-out 0.2s;
&.with-transition {
transition:
width ease-in-out 0.2s,
min-width ease-in-out 0.2s;
}
@include common-mixins.scroll-bar;
&:hover {
overflow-y: auto;
@include common-mixins.scroll-bar;
}
.collapsed-wrapper {
@ -254,6 +256,10 @@ section.settings {
cursor: pointer;
}
.cursor-default {
cursor: default;
}
.fit-content {
width: fit-content;
}

View File

@ -25,7 +25,7 @@
.mat-mdc-menu-item {
font-size: var(--iqser-font-size);
color: var(--iqser-text);
padding: 0 26px 0 8px;
padding: 0 26px 0 8px !important;
margin: var(--iqser-menu-item-margin);
border-radius: 4px;
width: -webkit-fill-available;
@ -58,7 +58,7 @@
}
&.padding-left {
padding-left: 56px;
padding-left: 56px !important;
}
&:last-of-type {

View File

@ -1,9 +1,10 @@
@use 'common-mixins';
.mat-mdc-select {
padding: 0 11px;
padding: 0 calc(var(--iqser-inputs-height) - 25px);
box-sizing: border-box;
--mat-select-trigger-text-line-height: 36px;
--mat-select-trigger-text-line-height: var(--iqser-inputs-height);
--mat-select-trigger-text-size: var(--iqser-inputs-font-size);
}
.mat-mdc-select-panel {
@ -53,6 +54,6 @@
}
}
.mat-mdc-option .mat-mdc-option-pseudo-checkbox {
.mat-mdc-option:not(.mat-mdc-option-multiple) .mat-mdc-option-pseudo-checkbox {
display: none;
}

View File

@ -1,3 +1,5 @@
@use 'ngx-toastr/toastr';
@use 'common-utilities';
@use 'common-inputs';
@use 'common-buttons';
@ -28,6 +30,7 @@
@use 'common-toggle-button';
@use 'common-tooltips';
@use 'common-file-drop';
@use 'common-file-upload';
@use 'common-side-nav';
@use 'common-color-picker';
@use 'common-skeleton';

View File

@ -8,10 +8,7 @@
flex: 1;
align-items: center;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 10px;
}
gap: 10px;
}
.header-item {
@ -23,6 +20,7 @@
border-bottom: 1px solid var(--iqser-separator);
box-sizing: border-box;
padding: 0 24px;
gap: 10px;
.header-title {
display: flex;
@ -34,17 +32,10 @@
padding: 0 24px 0 10px;
}
> *:not(:last-child) {
margin-right: 10px;
}
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 16px;
}
gap: 16px;
}
}

View File

@ -149,6 +149,10 @@ pre {
@include mixins.line-clamp(4);
}
.clamp-5 {
@include mixins.line-clamp(5);
}
.no-wrap {
white-space: nowrap;
}

View File

@ -32,9 +32,19 @@
--mdc-switch-track-width: 30px;
--mdc-switch-track-height: 16px;
--mdc-switch-track-shape: 8px;
--mdc-switch-handle-width: 12px;
--mdc-switch-handle-height: 12px;
--mat-switch-with-icon-handle-size: 12px;
--mdc-switch-handle-shape: 6px;
--mat-switch-unselected-with-icon-handle-horizontal-margin: 0 2px;
--mat-switch-unselected-pressed-handle-horizontal-margin: 0 2px;
--mat-switch-selected-with-icon-handle-horizontal-margin: 0 6px;
--mat-switch-selected-pressed-handle-horizontal-margin: 0 6px;
--mat-switch-selected-handle-horizontal-margin: 0 6px;
--mat-switch-unselected-handle-size: 12px;
--mat-switch-selected-handle-size: 12px;
--mat-switch-pressed-handle-size: 12px;
}
.mdc-form-field > label {
@ -42,17 +52,6 @@
padding-left: 0;
}
.mdc-switch__handle {
right: 8px;
left: unset;
}
.mdc-switch--unselected {
.mdc-switch__handle {
right: 4px;
}
}
.mdc-switch__icons,
.mdc-switch__ripple {
display: none;

View File

@ -1,3 +1,5 @@
@use 'sass:string';
@use 'sass:list';
/* Margins, paddings */
$start: 0;
@ -7,19 +9,19 @@ $values: '';
$sides: (top, bottom, left, right);
@for $i from $start + 1 through $end {
$values: append($values, $i, comma);
$values: set-nth($values, 1, $start);
$values: list.append($values, $i, comma);
$values: list.set-nth($values, 1, $start);
}
// TODO: Check if !important can be avoided
@each $space in $values {
@each $side in $sides {
.m#{str-slice($side, 0, 1)}-#{$space} {
.m#{string.slice($side, 0, 1)}-#{$space} {
margin-#{$side}: #{$space}px !important;
}
.p#{str-slice($side, 0, 1)}-#{$space} {
.p#{string.slice($side, 0, 1)}-#{$space} {
padding-#{$side}: #{$space}px !important;
}
}

View File

@ -3,13 +3,11 @@ export * from './lib/dialog';
export * from './lib/form';
export * from './lib/listing';
export * from './lib/help-mode';
export * from './lib/inputs';
export * from './lib/services';
export * from './lib/loading';
export * from './lib/error';
export * from './lib/search';
export * from './lib/upload-file';
export * from './lib/empty-state';
export * from './lib/caching';
export * from './lib/translations';
export * from './lib/pipes';

View File

@ -1,6 +1,8 @@
<button [class.overlay]="showDot" [class.primary]="primary" [disabled]="disabled" [id]="buttonId" mat-button>
<span>{{ label }}</span>
<button [class.overlay]="showDot()" [class.primary]="primary()" [disabled]="disabled()" [id]="buttonId()" mat-button>
<span>{{ label() }}</span>
<mat-icon class="chevron-icon" iconPositionEnd svgIcon="iqser:arrow-down"></mat-icon>
</button>
<div *ngIf="showDot" class="dot"></div>
@if (showDot()) {
<div class="dot"></div>
}

View File

@ -1,5 +1,5 @@
button {
padding: 0 10px 0 14px;
--mat-text-button-with-icon-horizontal-padding: 10px 0 14px;
mat-icon {
width: 14px;

View File

@ -1,21 +1,19 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { randomString } from '../../utils';
import { NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, input } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { randomString } from '../../utils';
@Component({
selector: 'iqser-chevron-button',
templateUrl: './chevron-button.component.html',
styleUrls: ['./chevron-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, MatIconModule, MatButtonModule],
imports: [MatIconModule, MatButtonModule],
})
export class ChevronButtonComponent {
@Input({ required: true }) label!: string;
@Input() showDot = false;
@Input() primary = false;
@Input() disabled = false;
@Input() buttonId = `${randomString()}-chevron-button`;
readonly label = input.required<string>();
readonly showDot = input(false, { transform: booleanAttribute });
readonly primary = input(false, { transform: booleanAttribute });
readonly disabled = input(false, { transform: booleanAttribute });
readonly buttonId = input(`${randomString()}-chevron-button`);
}

View File

@ -0,0 +1 @@
export * from './chevron-button.component';

View File

@ -1,22 +1,25 @@
<div [matTooltipClass]="tooltipClass || ''" [matTooltipPosition]="tooltipPosition" [matTooltip]="tooltip || ''">
<div [matTooltipClass]="tooltipClass()" [matTooltipPosition]="tooltipPosition()" [matTooltip]="tooltip()">
<button
(click)="performAction($event)"
[class.dark-bg]="type === _circleButtonTypes.dark"
[class.grey-selected]="greySelected"
[class.help]="type === _circleButtonTypes.help"
[class.overlay]="showDot"
[class.primary]="type === _circleButtonTypes.primary"
[class.warn]="type === _circleButtonTypes.warn"
[disabled]="disabled"
[id]="buttonId"
[class.dark-bg]="type() === _circleButtonTypes.dark"
[class.grey-selected]="greySelected()"
[class.overlay]="showDot()"
[class.primary]="type() === _circleButtonTypes.primary"
[class.warn]="type() === _circleButtonTypes.warn"
[disabled]="disabled()"
[id]="buttonId()"
[iqserStopPropagation]="action.observed && !_hasRouterLink"
[type]="isSubmit ? 'submit' : 'button'"
[type]="isSubmit() ? 'submit' : 'button'"
mat-icon-button
>
<mat-icon [svgIcon]="icon"></mat-icon>
<mat-icon [svgIcon]="icon()"></mat-icon>
</button>
<div *ngIf="showDot" class="dot"></div>
@if (showDot()) {
<div class="dot"></div>
}
<div *ngIf="dropdownButton" [class.disabled]="disabled" class="arrow-down"></div>
@if (dropdownButton()) {
<div [class.disabled]="disabled()" class="arrow-down"></div>
}
</div>

View File

@ -1,5 +1,16 @@
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
effect,
ElementRef,
EventEmitter,
inject,
input,
numberAttribute,
Output,
viewChild,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
@ -13,39 +24,39 @@ import { CircleButtonType, CircleButtonTypes } from '../types/circle-button.type
templateUrl: './circle-button.component.html',
styleUrls: ['./circle-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [MatTooltipModule, MatIconModule, NgIf, MatButtonModule, StopPropagationDirective],
imports: [MatTooltipModule, MatIconModule, MatButtonModule, StopPropagationDirective],
})
export class CircleButtonComponent implements OnInit {
export class CircleButtonComponent {
readonly #elementRef = inject(ElementRef<HTMLElement>);
@ViewChild(MatTooltip) private readonly _matTooltip!: MatTooltip;
protected readonly _matTooltip = viewChild.required(MatTooltip);
protected readonly _circleButtonTypes = CircleButtonTypes;
protected readonly _hasRouterLink = !!inject(RouterLink, { optional: true, host: true });
@Input() buttonId = `${randomString()}-circle-button`;
@Input({ required: true }) icon!: string;
@Input() tooltip?: string;
@Input() tooltipClass?: string;
@Input() showDot = false;
@Input() tooltipPosition: IqserTooltipPosition = IqserTooltipPositions.above;
@Input() disabled = false;
@Input() type: CircleButtonType = CircleButtonTypes.default;
@Input() greySelected = false;
@Input() helpModeButton = false;
@Input() removeTooltip = false;
@Input() isSubmit = false;
@Input() dropdownButton = false;
@Input() size = 34;
@Input() iconSize = 14;
readonly buttonId = input(`${randomString()}-circle-button`);
readonly icon = input.required<string>();
readonly tooltip = input('');
readonly tooltipClass = input('');
readonly showDot = input(false, { transform: booleanAttribute });
readonly tooltipPosition = input<IqserTooltipPosition>(IqserTooltipPositions.above);
readonly disabled = input(false, { transform: booleanAttribute });
readonly type = input<CircleButtonType>(CircleButtonTypes.default);
readonly greySelected = input(false, { transform: booleanAttribute });
readonly removeTooltip = input(false, { transform: booleanAttribute });
readonly isSubmit = input(false, { transform: booleanAttribute });
readonly dropdownButton = input(false, { transform: booleanAttribute });
readonly size = input(34, { transform: numberAttribute });
readonly iconSize = input(14, { transform: numberAttribute });
@Output() readonly action = new EventEmitter<MouseEvent>();
ngOnInit() {
this.#elementRef.nativeElement.style.setProperty('--circle-button-size', `${this.size}px`);
this.#elementRef.nativeElement.style.setProperty('--circle-button-icon-size', `${this.iconSize}px`);
constructor() {
effect(() => {
this.#elementRef.nativeElement.style.setProperty('--circle-button-size', `${this.size()}px`);
this.#elementRef.nativeElement.style.setProperty('--circle-button-icon-size', `${this.iconSize()}px`);
});
}
performAction($event: MouseEvent) {
if (this.removeTooltip) {
this._matTooltip.hide();
if (this.removeTooltip()) {
this._matTooltip().hide();
// Timeout to allow tooltip to disappear first,
// useful when removing an item from the list without a confirmation dialog
setTimeout(() => this.action.emit($event));

View File

@ -1,14 +1,19 @@
<button
(click)="!disabled && action.emit($event)"
[disabled]="disabled"
[id]="buttonId"
(click)="!disabled() && emitAction($event)"
[disabled]="disabled()"
[id]="buttonId()"
[iqserStopPropagation]="action.observed && !_hasRouterLink"
[ngClass]="classes"
[type]="submit ? 'submit' : 'button'"
[ngClass]="_classes()"
[type]="submit() ? 'submit' : 'button'"
mat-button
>
<mat-icon *ngIf="icon" [svgIcon]="icon"></mat-icon>
<span>{{ label }}</span>
@if (icon(); as icon) {
<mat-icon [svgIcon]="icon"></mat-icon>
}
<span>{{ label() }}</span>
</button>
<div *ngIf="showDot" class="dot"></div>
@if (showDot()) {
<div class="dot"></div>
}

View File

@ -1,37 +1,42 @@
import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output } from '@angular/core';
import { IconButtonType, IconButtonTypes } from '../types/icon-button.type';
import { randomString } from '../../utils';
import { NgClass, NgIf } from '@angular/common';
import { NgClass } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, computed, EventEmitter, inject, input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { StopPropagationDirective } from '../../directives';
import { RouterLink } from '@angular/router';
import { StopPropagationDirective } from '../../directives';
import { randomString } from '../../utils';
import { IconButtonType, IconButtonTypes } from '../types/icon-button.type';
@Component({
selector: 'iqser-icon-button',
templateUrl: './icon-button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgClass, MatButtonModule, NgIf, MatIconModule, StopPropagationDirective],
imports: [NgClass, MatButtonModule, MatIconModule, StopPropagationDirective],
})
export class IconButtonComponent {
@Input({ required: true }) label!: string;
@Input() buttonId = `${randomString()}-icon-button`;
@Input() icon?: string;
@Input() showDot = false;
@Input() active = false;
@Input() disabled = false;
@Input() submit = false;
@Input() type: IconButtonType = IconButtonTypes.default;
@Output() readonly action = new EventEmitter<MouseEvent>();
protected readonly _hasRouterLink = !!inject(RouterLink, { optional: true, host: true });
get classes(): Record<string, boolean> {
readonly label = input.required<string>();
readonly buttonId = input(`${randomString()}-icon-button`);
readonly icon = input<string>();
readonly showDot = input(false, { transform: booleanAttribute });
readonly active = input(false, { transform: booleanAttribute });
readonly disabled = input(false, { transform: booleanAttribute });
readonly submit = input(false, { transform: booleanAttribute });
readonly type = input<IconButtonType>(IconButtonTypes.default);
protected readonly _classes = computed(() => {
return {
overlay: this.showDot,
[this.type]: true,
'has-icon': !!this.icon,
active: this.active,
overlay: this.showDot(),
[this.type()]: true,
'has-icon': !!this.icon(),
active: this.active(),
};
});
@Output() readonly action = new EventEmitter<MouseEvent>();
emitAction($event: MouseEvent) {
const activeElement = document.activeElement as HTMLElement;
if (activeElement.tagName?.toLowerCase() === 'button') {
this.action.emit($event);
}
}
}

View File

@ -3,4 +3,3 @@ export * from './types/circle-button.type';
export * from './icon-button/icon-button.component';
export * from './circle-button/circle-button.component';
export * from './chevron-button/chevron-button.component';

View File

@ -3,7 +3,6 @@ export const CircleButtonTypes = {
primary: 'primary',
warn: 'warn',
dark: 'dark',
help: 'help',
} as const;
export type CircleButtonType = keyof typeof CircleButtonTypes;

View File

@ -1,4 +1,4 @@
import { List } from '../utils';
import { List } from '../utils/types/iqser-types';
export interface DynamicCache {
readonly urls: List;

View File

@ -3,18 +3,20 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { debounceTime, firstValueFrom, fromEvent, merge, of, Subscription } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ConfirmOptions } from '.';
import { IconButtonTypes } from '../buttons';
import { LoadingService } from '../loading';
import { Toaster } from '../services';
import { hasFormChanged, IqserEventTarget } from '../utils';
import { IconButtonTypes } from '../buttons/types/icon-button.type';
import { LoadingService } from '../loading/loading.service';
import { Toaster } from '../services/toaster.service';
import { hasFormChanged } from '../utils/functions';
import { IqserEventTarget } from '../utils/types/events.type';
import { ConfirmationDialogService } from './confirmation-dialog.service';
import { ConfirmOptions } from './confirmation-dialog/confirmation-dialog.component';
const DIALOG_CONTAINER = 'mat-dialog-container';
const TEXT_INPUT = 'text';
export interface SaveOptions {
closeAfterSave?: boolean;
nextAction?: boolean;
addMembers?: boolean;
}
@ -22,7 +24,7 @@ export interface SaveOptions {
export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy {
readonly #confirmationDialogService = inject(ConfirmationDialogService);
readonly #dialog = inject(MatDialog);
readonly #hasErrors = signal(true);
protected readonly _hasErrors = signal(true);
protected readonly _formBuilder = inject(UntypedFormBuilder);
protected readonly _loadingService = inject(LoadingService);
protected readonly _toaster = inject(Toaster);
@ -45,17 +47,18 @@ export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy {
}
get disabled(): boolean {
return !this.valid || !this.changed || this.#hasErrors();
return !this.valid || !this.changed || this._hasErrors();
}
ngAfterViewInit() {
this._subscriptions.add(this._dialogRef.backdropClick().subscribe(() => this.close()));
const valueChanges = this.form?.valueChanges ?? of(null);
const events = [fromEvent(window, 'keyup'), fromEvent(window, 'input'), valueChanges];
this._hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]);
const events$ = merge(...events).pipe(
debounceTime(10),
tap(() => {
this.#hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]);
this._hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]);
}),
);
this._subscriptions.add(events$.subscribe());

View File

@ -1,7 +1,13 @@
import { inject, Injectable } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ConfirmationDialogComponent, ConfirmOption, defaultDialogConfig, IConfirmationDialogData, TitleColors } from '.';
import { MatDialog } from '@angular/material/dialog';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
ConfirmationDialogComponent,
ConfirmOption,
IConfirmationDialogData,
TitleColors,
} from './confirmation-dialog/confirmation-dialog.component';
import { defaultDialogConfig } from './dialog.service';
@Injectable({
providedIn: 'root',

View File

@ -1,56 +1,83 @@
<section class="dialog">
<div [class.warn]="isDeleteAction" [innerHTML]="config.title" class="dialog-header heading-l"></div>
<div *ngIf="showToast && config.toastMessage" class="inline-dialog-toast toast-error">
<div [translate]="config.toastMessage"></div>
<a (click)="showToast = false" class="toast-close-button">
<mat-icon svgIcon="iqser:close"></mat-icon>
</a>
</div>
@if (showToast && config.toastMessage) {
<div class="inline-dialog-toast toast-error">
<div [translate]="config.toastMessage"></div>
<a (click)="showToast = false" class="toast-close-button">
<mat-icon svgIcon="iqser:close"></mat-icon>
</a>
</div>
}
<div class="dialog-content">
@if (config.component) {
<ng-container #detailsComponent></ng-container>
}
<p [class.heading]="isDeleteAction" [innerHTML]="config.question" class="mt-0 mb-8"></p>
<p *ngIf="config.details" [innerHTML]="config.details" class="mt-0"></p>
@if (config.details) {
<p [innerHTML]="config.details" class="mt-0"></p>
}
<div *ngIf="config.requireInput" class="iqser-input-group required w-300 mt-24">
<label>{{ inputLabel }}</label>
<input [(ngModel)]="inputValue" id="confirmation-input" />
</div>
@if (config.requireInput) {
<div class="iqser-input-group required w-300 mt-24">
<label>{{ inputLabel }}</label>
<input [(ngModel)]="inputValue" id="confirmation-input" />
</div>
}
<div *ngIf="config.checkboxes.length > 0" class="mt-24 checkboxes-wrapper">
<ng-container *ngFor="let checkbox of config.checkboxes">
<mat-checkbox [(ngModel)]="checkbox.value" [class.error]="!checkbox.value && showToast" color="primary">
{{ checkbox.label | translate: config.translateParams }}
</mat-checkbox>
<ng-container *ngTemplateOutlet="checkbox.extraContent; context: { data: checkbox.extraContentData }"></ng-container>
</ng-container>
</div>
@if (config.checkboxes.length > 0) {
<div class="mt-24 checkboxes-wrapper">
@for (checkbox of config.checkboxes; track checkbox) {
<mat-checkbox [(ngModel)]="checkbox.value" [class.error]="!checkbox.value && showToast" color="primary">
{{ checkbox.label | translate: config.translateParams }}
</mat-checkbox>
<ng-container *ngTemplateOutlet="checkbox.extraContent; context: { data: checkbox.extraContentData }"></ng-container>
}
</div>
}
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="confirm(confirmOption)"
[disabled]="(config.requireInput && confirmationDoesNotMatch()) || config.disableConfirm"
[label]="config.confirmationText"
[type]="iconButtonTypes.primary"
buttonId="confirm"
></iqser-icon-button>
<div class="dialog-actions" [class.reverse]="config.cancelButtonPrimary">
@if (!config.cancelButtonPrimary) {
<iqser-icon-button
(action)="confirm(confirmOption)"
[disabled]="(config.requireInput && confirmationDoesNotMatch()) || config.disableConfirm"
[label]="config.confirmationText"
[type]="iconButtonTypes.primary"
buttonId="confirm"
></iqser-icon-button>
} @else {
<div (click)="confirm(confirmOption)" class="all-caps-label cancel no-uppercase" id="confirm">
{{ config.confirmationText }}
</div>
}
<iqser-icon-button
(action)="confirm(confirmOptions.CONFIRM_WITH_ACTION)"
*ngIf="config.alternativeConfirmationText"
[disabled]="config.requireInput && confirmationDoesNotMatch()"
[label]="config.alternativeConfirmationText"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
@if (config.alternativeConfirmationText) {
<iqser-icon-button
(action)="confirm(confirmOptions.CONFIRM_WITH_ACTION)"
[disabled]="config.requireInput && confirmationDoesNotMatch()"
[label]="config.alternativeConfirmationText"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
}
<div (click)="confirm(confirmOptions.DISCARD_CHANGES)" *ngIf="config.discardChangesText" class="all-caps-label cancel">
{{ config.discardChangesText }}
</div>
@if (config.discardChangesText) {
<div (click)="confirm(confirmOptions.DISCARD_CHANGES)" class="all-caps-label cancel">
{{ config.discardChangesText }}
</div>
}
<div (click)="deny()" *ngIf="!config.discardChangesText" class="all-caps-label cancel">
{{ config.denyText }}
</div>
@if (!config.discardChangesText) {
@if (config.cancelButtonPrimary) {
<iqser-icon-button (click)="deny()" [label]="config.denyText" [type]="iconButtonTypes.primary"></iqser-icon-button>
} @else {
<div (click)="deny()" class="all-caps-label cancel">
{{ config.denyText }}
</div>
}
}
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>

View File

@ -6,3 +6,12 @@
display: flex;
flex-direction: column;
}
.reverse {
flex-direction: row-reverse;
justify-content: flex-end;
}
.no-uppercase {
text-transform: unset;
}

View File

@ -1,12 +1,23 @@
import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, HostListener, inject, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
HostListener,
inject,
TemplateRef,
Type,
viewChild,
ViewContainerRef,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes } from '../../buttons';
import { CircleButtonComponent, IconButtonTypes } from '../../buttons';
import { IconButtonComponent } from '../../buttons';
import { ValuesOf } from '../../utils';
export const TitleColors = {
@ -46,6 +57,9 @@ interface InternalConfirmationDialogData {
readonly checkboxes: CheckBox[];
readonly checkboxesValidation: boolean;
readonly toastMessage?: string;
readonly component?: Type<unknown>;
readonly componentInputs?: { [key: string]: unknown };
readonly cancelButtonPrimary?: boolean;
}
export type IConfirmationDialogData = Partial<InternalConfirmationDialogData>;
@ -63,6 +77,9 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
denyText: options?.denyText ?? _('common.confirmation-dialog.deny'),
checkboxes: options?.checkboxes ?? [],
checkboxesValidation: typeof options?.checkboxesValidation === 'boolean' ? options.checkboxesValidation : true,
component: options?.component,
componentInputs: options?.componentInputs,
cancelButtonPrimary: options?.cancelButtonPrimary ?? false,
};
}
@ -70,12 +87,9 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
NgIf,
MatIconModule,
FormsModule,
NgForOf,
MatCheckboxModule,
TranslateModule,
NgTemplateOutlet,
@ -84,13 +98,14 @@ function getConfig(options?: IConfirmationDialogData): InternalConfirmationDialo
MatDialogModule,
],
})
export class ConfirmationDialogComponent {
export class ConfirmationDialogComponent implements AfterViewInit {
readonly config = getConfig(inject(MAT_DIALOG_DATA));
inputValue = '';
showToast = false;
readonly inputLabel: string;
readonly confirmOptions = ConfirmOptions;
readonly iconButtonTypes = IconButtonTypes;
readonly detailsComponentRef = viewChild.required('detailsComponent', { read: ViewContainerRef });
constructor(
private readonly _dialogRef: MatDialogRef<ConfirmationDialogComponent, ConfirmOption>,
@ -115,13 +130,19 @@ export class ConfirmationDialogComponent {
return ConfirmOptions.CONFIRM;
}
@HostListener('window:keyup.enter')
onKeyupEnter(): void {
if (this.config.requireInput && !this.confirmationDoesNotMatch()) {
this.confirm(ConfirmOptions.CONFIRM);
@HostListener('window:keyup.enter', ['$event'])
onKeyupEnter(event: KeyboardEvent): void {
event?.stopImmediatePropagation();
if (!this.config.requireInput || !this.confirmationDoesNotMatch()) {
if (!this.config.cancelButtonPrimary) this.confirm(ConfirmOptions.CONFIRM);
else this.deny();
}
}
ngAfterViewInit() {
this.#initializeDetailsComponent();
}
confirmationDoesNotMatch(): boolean {
return this.inputValue.toLowerCase() !== this.config.confirmationText.toLowerCase();
}
@ -156,9 +177,13 @@ export class ConfirmationDialogComponent {
});
}
@HostListener('window:keydown.Enter', ['$event'])
onEnter(event: KeyboardEvent): void {
event?.stopImmediatePropagation();
this.confirm(ConfirmOptions.CONFIRM);
#initializeDetailsComponent() {
if (!this.config.component) return;
const component = this.detailsComponentRef().createComponent(this.config.component);
if (this.config.componentInputs) {
for (const [key, value] of Object.entries(this.config.componentInputs)) {
(component.instance as any)[key] = value;
}
}
}
}

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';
import { mergeMap } from 'rxjs/operators';
import { Injectable, Type } from '@angular/core';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { from } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
export const largeDialogConfig: MatDialogConfig = {
width: '90vw',
@ -64,4 +64,34 @@ export abstract class DialogService<T extends string> {
return ref;
}
open(
type: Type<unknown>,
data?: unknown,
config?: object,
cb?: (...params: unknown[]) => Promise<unknown> | void,
finallyCb?: (...params: unknown[]) => void | Promise<unknown>,
): MatDialogRef<unknown> {
const ref = this._dialog.open(type, {
...defaultDialogConfig,
...(config || {}),
data,
});
const fn = async (result: unknown) => {
if (result && cb) {
await cb(result);
}
if (finallyCb) {
await finallyCb(result);
}
};
ref.afterClosed()
.pipe(mergeMap(result => from(fn(result))))
.subscribe();
return ref;
}
}

View File

@ -1,8 +1,9 @@
import { Directive, HostListener, inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { hasFormChanged, IqserEventTarget } from '../utils';
import { FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { IconButtonTypes } from '../buttons';
import { hasFormChanged, IqserEventTarget } from '../utils';
const DIALOG_CONTAINER = 'mat-dialog-container';
const DATA_TYPE_SYMBOL = Symbol.for('DATA_TYPE');
@ -12,14 +13,17 @@ export type DATA_TYPE = typeof DATA_TYPE_SYMBOL;
export type RETURN_TYPE = typeof RETURN_TYPE_SYMBOL;
@Directive()
export abstract class IqserDialogComponent<ComponentType, DataType, ReturnType> {
export abstract class IqserDialogComponent<ComponentType, DataType = null, ReturnType = void> {
readonly [DATA_TYPE_SYMBOL]!: DataType;
readonly [RETURN_TYPE_SYMBOL]!: ReturnType;
readonly iconButtonTypes = IconButtonTypes;
readonly dialogRef = inject(MatDialogRef<ComponentType, ReturnType>);
readonly data = inject<DataType>(MAT_DIALOG_DATA);
readonly dialog = inject(MatDialog);
readonly form?: FormGroup;
readonly ignoredKeys: string[] = [];
initialFormValue: Record<string, unknown> = {};
constructor(private readonly _editMode = false) {
@ -35,7 +39,7 @@ export abstract class IqserDialogComponent<ComponentType, DataType, ReturnType>
}
get changed(): boolean {
return !this.form || hasFormChanged(this.form, this.initialFormValue);
return !this.form || hasFormChanged(this.form, this.initialFormValue, this.ignoredKeys);
}
get disabled(): boolean {

View File

@ -1,9 +1,8 @@
import { booleanAttribute, Directive, Input } from '@angular/core';
import { booleanAttribute, Directive, input } from '@angular/core';
@Directive({
selector: '[iqserDisableStopPropagation]',
standalone: true,
})
export class DisableStopPropagationDirective {
@Input({ transform: booleanAttribute }) iqserDisableStopPropagation = true;
readonly iqserDisableStopPropagation = input(true, { transform: booleanAttribute });
}

View File

@ -1,39 +1,35 @@
import { ChangeDetectorRef, Directive, ElementRef, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { Directive, ElementRef, OnDestroy, OnInit, signal } from '@angular/core';
@Directive({
selector: '[iqserHasScrollbar]',
standalone: true,
host: {
'[class]': '_class()',
},
})
export class HasScrollbarDirective implements OnInit, OnDestroy {
@HostBinding('class') class = '';
private readonly _resizeObserver: ResizeObserver;
protected readonly _class = signal('');
get hasScrollbar() {
const element = this._elementRef?.nativeElement as HTMLElement;
return element.clientHeight < element.scrollHeight;
}
constructor(
protected readonly _elementRef: ElementRef,
protected readonly _changeDetector: ChangeDetectorRef,
) {
this._resizeObserver = new ResizeObserver(entry => {
constructor(protected readonly _elementRef: ElementRef) {
this._resizeObserver = new ResizeObserver(() => {
this.process();
});
this._resizeObserver.observe(this._elementRef.nativeElement);
}
private get _hasScrollbar() {
const element = this._elementRef?.nativeElement as HTMLElement;
return element.clientHeight < element.scrollHeight;
}
ngOnInit() {
setTimeout(() => this.process(), 0);
}
process() {
const newClass = this.hasScrollbar ? 'has-scrollbar' : '';
if (this.class !== newClass) {
this.class = newClass;
this._changeDetector.markForCheck();
}
const newClass = this._hasScrollbar ? 'has-scrollbar' : '';
this._class.set(newClass);
}
ngOnDestroy() {

View File

@ -2,7 +2,6 @@ import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/c
@Directive({
selector: '[iqserHiddenAction]',
standalone: true,
})
export class HiddenActionDirective {
@Input() requiredClicks = 4;

View File

@ -3,7 +3,6 @@ import { NGXLogger } from 'ngx-logger';
@Directive({
selector: '[iqserPreventDefault]',
standalone: true,
})
export class PreventDefaultDirective {
readonly #logger = inject(NGXLogger);

View File

@ -1,10 +1,9 @@
import { booleanAttribute, Directive, HostListener, inject, Input } from '@angular/core';
import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
import { NGXLogger } from 'ngx-logger';
import { DisableStopPropagationDirective } from './disable-stop-propagation.directive';
@Directive({
selector: '[iqserStopPropagation]',
standalone: true,
})
export class StopPropagationDirective {
readonly #disableStopPropagation = inject(DisableStopPropagationDirective, { optional: true });
@ -13,13 +12,14 @@ export class StopPropagationDirective {
@HostListener('click', ['$event'])
onClick($event: Event) {
if (this.#disableStopPropagation?.iqserDisableStopPropagation) {
if (this.#disableStopPropagation?.iqserDisableStopPropagation()) {
this.#logger.info('[CLICK] iqserStopPropagation is disabled by iqserDisableStopPropagation');
return;
}
if (this.iqserStopPropagation) {
this.#logger.info('[CLICK] iqserStopPropagation');
$event.preventDefault();
$event.stopPropagation();
}
}

View File

@ -2,7 +2,6 @@ import { Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/
@Directive({
selector: '[iqserSyncWidth]',
standalone: true,
})
export class SyncWidthDirective implements OnDestroy {
@Input() iqserSyncWidth!: string;

View File

@ -1,26 +1,22 @@
<div
[ngStyle]="{
'padding-top': verticalPadding + 'px',
'padding-left': horizontalPadding + 'px',
'padding-right': horizontalPadding + 'px'
}"
class="empty-state"
>
<mat-icon *ngIf="icon" [svgIcon]="icon"></mat-icon>
<div [ngStyle]="styles()" class="empty-state">
@if (icon(); as icon) {
<mat-icon [svgIcon]="icon"></mat-icon>
}
<div class="ng-content-wrapper heading-l">
<ng-content></ng-content>
</div>
<div [innerHTML]="text" class="heading-l"></div>
<div [innerHTML]="text()" class="heading-l"></div>
<iqser-icon-button
(action)="action.emit()"
*ngIf="showButton"
[buttonId]="buttonId"
[icon]="buttonIcon"
[attr.help-mode-key]="helpModeKey"
[label]="buttonLabel"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
@if (showButton() && this.action.observed) {
<iqser-icon-button
(action)="action.emit()"
[buttonId]="buttonId()"
[icon]="buttonIcon()"
[attr.help-mode-key]="helpModeKey()"
[label]="buttonLabel()"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
}
</div>

View File

@ -1,33 +1,42 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IconButtonComponent, IconButtonTypes } from '../buttons';
import { randomString } from '../utils';
import { NgIf, NgStyle } from '@angular/common';
import { NgStyle } from '@angular/common';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
EventEmitter,
input,
numberAttribute,
Output,
} from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { IqserHelpModeModule } from '../help-mode';
import { IconButtonComponent } from '../buttons/icon-button/icon-button.component';
import { IconButtonTypes } from '../buttons/types/icon-button.type';
import { randomString } from '../utils/functions';
@Component({
selector: 'iqser-empty-state [text]',
selector: 'iqser-empty-state',
templateUrl: './empty-state.component.html',
styleUrls: ['./empty-state.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgStyle, MatIconModule, NgIf, IconButtonComponent, IqserHelpModeModule],
imports: [NgStyle, MatIconModule, IconButtonComponent],
})
export class EmptyStateComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
export class EmptyStateComponent {
protected readonly iconButtonTypes = IconButtonTypes;
@Input() text!: string;
@Input() icon?: string;
@Input() showButton = true;
@Input() buttonIcon = 'iqser:plus';
@Input() buttonLabel?: string;
@Input() buttonId = `${randomString()}-icon-button`;
@Input() horizontalPadding = 100;
@Input() verticalPadding = 120;
@Input() helpModeKey?: string;
readonly text = input.required<string>();
readonly icon = input<string>();
readonly showButton = input(true, { transform: booleanAttribute });
readonly buttonIcon = input('iqser:plus');
readonly buttonLabel = input<string>();
readonly buttonId = input(`${randomString()}-icon-button`);
readonly horizontalPadding = input(100, { transform: numberAttribute });
readonly verticalPadding = input(120, { transform: numberAttribute });
protected readonly styles = computed(() => ({
'padding-top': this.verticalPadding() + 'px',
'padding-left': this.horizontalPadding() + 'px',
'padding-right': this.horizontalPadding() + 'px',
}));
readonly helpModeKey = input<string>();
@Output() readonly action = new EventEmitter();
ngOnInit(): void {
this.showButton = this.showButton && this.action.observed;
}
}

View File

@ -1,8 +1,5 @@
<div
*ngIf="errorService.connectionStatus$ | async as status"
[@animateOpenClose]="status"
[ngClass]="status"
class="indicator flex-align-items-center"
>
<span [translate]="connectionStatusTranslations[status]"></span>
</div>
@if (connectionStatus(); as status) {
<div [@animateOpenClose]="status" [ngClass]="status" class="indicator flex-align-items-center">
<span [translate]="connectionStatusTranslations[status]"></span>
</div>
}

View File

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { connectionStatusTranslations } from '../../translations';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { connectionStatusTranslations } from '../../translations';
import { ErrorService } from '../error.service';
@Component({
@ -16,8 +17,9 @@ import { ErrorService } from '../error.service';
]),
],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class ConnectionStatusComponent {
connectionStatusTranslations = connectionStatusTranslations;
protected readonly errorService = inject(ErrorService);
protected readonly connectionStatusTranslations = connectionStatusTranslations;
protected readonly connectionStatus = toSignal(inject(ErrorService).connectionStatus$);
}

View File

@ -1,13 +1,11 @@
<ng-container *ngIf="errorService.error$ | async as error">
@if (errorService.error$ | async; as error) {
<section class="full-page-section"></section>
<section class="full-page-content flex-align-items-center">
<mat-icon svgIcon="iqser:failure"></mat-icon>
<div [translate]="errorTitle(error)" class="heading-l mt-24"></div>
<div *ngIf="error.message" class="mt-16 error">{{ error.message }}</div>
@if (error.message) {
<div class="mt-16 error">{{ error.message }}</div>
}
<iqser-icon-button
(action)="action(error)"
[icon]="actionIcon(error)"
@ -16,4 +14,4 @@
class="mt-20"
></iqser-icon-button>
</section>
</ng-container>
}

View File

@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { IconButtonTypes } from '../../buttons';
import { CustomError, ErrorService, ErrorType } from '../error.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-full-page-error',
templateUrl: './full-page-error.component.html',
styleUrls: ['./full-page-error.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class FullPageErrorComponent {
readonly iconButtonTypes = IconButtonTypes;
constructor(readonly errorService: ErrorService) {}
protected readonly iconButtonTypes = IconButtonTypes;
protected readonly errorService = inject(ErrorService);
errorTitle(error: ErrorType): string {
return error instanceof CustomError ? error.label : _('error.title');

View File

@ -1,42 +1,51 @@
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup">
<iqser-input-with-action
*ngIf="primaryGroup.filterceptionPlaceholder"
[(value)]="searchService.searchValue"
[id]="'filterception-' + primaryGroup.slug"
[placeholder]="primaryGroup.filterceptionPlaceholder"
[width]="'full'"
></iqser-input-with-action>
@if (primaryFilterGroup$ | async; as primaryGroup) {
@if (primaryGroup.filterceptionPlaceholder) {
<div class="input-wrapper">
<iqser-input-with-action
[(value)]="searchService.searchValue"
[id]="'filterception-' + primaryGroup.slug"
[placeholder]="primaryGroup.filterceptionPlaceholder"
[width]="'full'"
></iqser-input-with-action>
</div>
}
<ng-container *ngTemplateOutlet="filterHeader"></ng-container>
<div *ngIf="primaryFilters$ | async as filters" class="filter-content">
<ng-container
*ngFor="let filter of filters"
[ngTemplateOutletContext]="{
filter: filter,
filterGroup: primaryGroup,
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
</div>
<div *ngIf="secondaryFilterGroup$ | async as secondaryGroup" class="filter-options">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
@if (primaryFilters$ | async; as filters) {
<div class="filter-content">
@for (filter of filters; track filter.id) {
<ng-container
[ngTemplateOutletContext]="{
filter: filter,
filterGroup: primaryGroup,
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async,
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
}
</div>
}
<ng-container
*ngFor="let filter of secondaryGroup.filters"
[ngTemplateOutletContext]="{
filter: filter,
filterGroup: secondaryGroup,
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
</div>
</ng-container>
@if (secondaryFilterGroup$ | async; as secondaryGroup) {
<div class="filter-options">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
@for (filter of secondaryGroup.filters; track filter.id) {
<ng-container
[ngTemplateOutletContext]="{
filter: filter,
filterGroup: secondaryGroup,
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async,
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
}
</div>
}
}
<ng-template #defaultFilterLabelTemplate let-filter="filter">
{{ filter?.label }}
@ -44,30 +53,51 @@
<!--TODO: move to separate component-->
<ng-template #filterHeader>
<div *ngIf="primaryFilterGroup$ | async as primaryGroup" class="filter-menu-header">
<div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div>
<div class="actions">
@if (primaryFilterGroup$ | async; as primaryGroup) {
<div class="filter-menu-header">
<div
(click)="activatePrimaryFilters()"
*ngIf="!primaryGroup.singleSelect"
class="all-caps-label primary pointer"
iqserStopPropagation
translate="actions.all"
[translateParams]="{ count: primaryGroup.filters.length }"
[translate]="primaryFiltersLabel()"
class="all-caps-label"
></div>
<div (click)="deactivateFilters()" class="all-caps-label primary pointer" iqserStopPropagation translate="actions.none"></div>
<div class="actions">
@if (!primaryGroup.singleSelect) {
<div
(click)="activatePrimaryFilters()"
class="all-caps-label primary pointer"
iqserStopPropagation
translate="actions.all"
></div>
}
<div
(click)="deactivatePrimaryFilters()"
class="all-caps-label primary pointer"
iqserStopPropagation
translate="actions.none"
></div>
</div>
</div>
</div>
}
</ng-template>
<!--TODO: move to separate component-->
<ng-template #defaultFilterTemplate let-atLeastOneIsExpandable="atLeastOneIsExpandable" let-filter="filter" let-filterGroup="filterGroup">
<div (click)="toggleFilterExpanded(filter)" class="mat-mdc-menu-item flex" iqserStopPropagation>
<div *ngIf="filter.children?.length > 0" class="arrow-wrapper">
<mat-icon *ngIf="filter.expanded" color="accent" svgIcon="iqser:arrow-down"></mat-icon>
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="iqser:arrow-right"></mat-icon>
</div>
@if (filter.children?.length > 0) {
<div class="arrow-wrapper">
@if (filter.expanded) {
<mat-icon color="accent" svgIcon="iqser:arrow-down"></mat-icon>
}
@if (!filter.expanded) {
<mat-icon color="accent" svgIcon="iqser:arrow-right"></mat-icon>
}
</div>
}
<div *ngIf="atLeastOneIsExpandable && filter.children?.length === 0" class="arrow-wrapper spacer">&nbsp;</div>
@if (atLeastOneIsExpandable && filter.children?.length === 0) {
<div class="arrow-wrapper spacer">&nbsp;</div>
}
<mat-checkbox
(click)="filterCheckboxClicked(filter, filterGroup)"
@ -83,19 +113,28 @@
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate()"></ng-container>
</div>
<div *ngIf="filter.children?.length && filter.expanded">
<div *ngFor="let child of filter.children" class="padding-left mat-mdc-menu-item" iqserStopPropagation>
<mat-checkbox (click)="filterCheckboxClicked(child, filterGroup, filter)" [checked]="child.checked" iqserStopPropagation>
<ng-container
[ngTemplateOutletContext]="{ filter: child }"
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
@if (filter.children?.length && filter.expanded) {
<div>
@for (child of filter.children; track child) {
@if (!child.hidden) {
<div class="padding-left mat-mdc-menu-item" iqserStopPropagation>
<mat-checkbox
(click)="filterCheckboxClicked(child, filterGroup, filter)"
[checked]="child.checked"
iqserStopPropagation
>
<ng-container
[ngTemplateOutletContext]="{ filter: child }"
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
></ng-container>
</mat-checkbox>
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate()"></ng-container>
</div>
}
}
</div>
</div>
}
</ng-template>

View File

@ -32,7 +32,7 @@
padding-bottom: 8px;
}
iqser-input-with-action {
.input-wrapper {
padding: 0 8px 8px 8px;
}

View File

@ -1,16 +1,20 @@
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, TemplateRef } from '@angular/core';
import { AsyncPipe, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, effect, ElementRef, inject, input, numberAttribute, OnInit, TemplateRef } from '@angular/core';
import { MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateModule } from '@ngx-translate/core';
import { combineLatest, Observable, pipe } from 'rxjs';
import { map } from 'rxjs/operators';
import { StopPropagationDirective } from '../../directives/stop-propagation.directive';
import { InputWithActionComponent } from '../../inputs/input-with-action/input-with-action.component';
import { SearchService } from '../../search/search.service';
import { shareDistinctLast, shareLast } from '../../utils/operators';
import { FilterService } from '../filter.service';
import { Filter } from '../models/filter';
import { IFilterGroup } from '../models/filter-group.model';
import { IFilter } from '../models/filter.model';
import { INestedFilter } from '../models/nested-filter.model';
import { IFilterGroup } from '../models/filter-group.model';
import { extractFilterValues, handleCheckedValue } from '../filter-utils';
import { FilterService } from '../filter.service';
import { SearchService } from '../../search';
import { Filter } from '../models/filter';
import { map } from 'rxjs/operators';
import { shareDistinctLast, shareLast } from '../../utils';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length;
const atLeastOneIsExpandable = pipe(
@ -18,19 +22,8 @@ const atLeastOneIsExpandable = pipe(
shareDistinctLast(),
);
export interface LocalStorageFilter {
id: string;
checked: boolean;
children?: LocalStorageFilter[] | null;
}
export interface LocalStorageFilters {
primaryFilters: LocalStorageFilter[] | null;
secondaryFilters: LocalStorageFilter[] | null;
}
@Component({
selector: 'iqser-filter-card [primaryFiltersSlug]',
selector: 'iqser-filter-card',
templateUrl: './filter-card.component.html',
styleUrls: ['./filter-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -44,27 +37,29 @@ export interface LocalStorageFilters {
},
},
],
imports: [AsyncPipe, InputWithActionComponent, NgTemplateOutlet, TranslateModule, MatIcon, MatCheckbox, StopPropagationDirective],
})
export class FilterCardComponent implements OnInit {
@Input() primaryFiltersSlug!: string;
@Input() fileId?: string;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() secondaryFiltersSlug = '';
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
@Input() minWidth = 350;
readonly #filterService = inject(FilterService);
readonly #elementRef = inject(ElementRef);
protected readonly searchService = inject<SearchService<Filter>>(SearchService);
readonly primaryFiltersSlug = input.required<string>();
readonly fileId = input<string>();
readonly actionsTemplate = input<TemplateRef<unknown>>();
readonly secondaryFiltersSlug = input('');
readonly primaryFiltersLabel = input<string>(_('filter-menu.filter-types'));
readonly minWidth = input(350, { transform: numberAttribute });
primaryFilterGroup$!: Observable<IFilterGroup | undefined>;
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>;
primaryFilters$!: Observable<IFilter[] | undefined>;
atLeastOneFilterIsExpandable$?: Observable<boolean>;
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
constructor(
readonly filterService: FilterService,
readonly searchService: SearchService<Filter>,
private readonly _elementRef: ElementRef,
) {}
constructor() {
effect(() => {
(this.#elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth()}px`);
});
}
private get _primaryFilters$(): Observable<IFilter[]> {
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
@ -74,87 +69,37 @@ export class FilterCardComponent implements OnInit {
}
ngOnInit() {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
this.primaryFilterGroup$ = this.#filterService.getGroup$(this.primaryFiltersSlug()).pipe(shareLast());
this.secondaryFilterGroup$ = this.#filterService.getGroup$(this.secondaryFiltersSlug()).pipe(shareLast());
this.primaryFilters$ = this._primaryFilters$;
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
(this._elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth}px`);
}
filterCheckboxClicked(nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void {
if (filterGroup.singleSelect) {
this.deactivateFilters(nestedFilter.id);
}
this.#filterService.filterCheckboxClicked({
nestedFilter,
filterGroup,
parent,
primaryFiltersSlug: this.primaryFiltersSlug(),
});
this.#filterService.updateFiltersInLocalStorage(this.fileId());
}
// eslint-disable-next-line no-param-reassign
nestedFilter.checked = !nestedFilter.checked;
if (parent) {
handleCheckedValue(parent);
} else {
// eslint-disable-next-line no-param-reassign
if (nestedFilter.indeterminate) {
nestedFilter.checked = false;
}
// eslint-disable-next-line no-param-reassign
nestedFilter.indeterminate = false;
// eslint-disable-next-line no-return-assign,no-param-reassign
nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked));
}
this.filterService.refresh();
this.#updateFiltersInLocalStorage();
deactivatePrimaryFilters() {
this.#filterService.deactivateFilters({ primaryFiltersSlug: this.primaryFiltersSlug() });
this.#filterService.updateFiltersInLocalStorage(this.fileId());
}
activatePrimaryFilters(): void {
this._setFilters(this.primaryFiltersSlug, true);
}
deactivateFilters(exceptedFilterId?: string): void {
this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
if (this.secondaryFiltersSlug) {
this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
}
this.#filterService.setFilters(this.primaryFiltersSlug(), true);
this.#filterService.updateFiltersInLocalStorage(this.fileId());
}
toggleFilterExpanded(nestedFilter: INestedFilter): void {
// eslint-disable-next-line no-param-reassign
nestedFilter.expanded = !nestedFilter.expanded;
this.filterService.refresh();
}
private _setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) {
const filters = this.filterService.getGroup(filterGroup)?.filters;
filters?.forEach(f => {
if (f.id !== exceptedFilterId) {
// eslint-disable-next-line no-param-reassign
f.checked = checked;
// eslint-disable-next-line no-param-reassign
f.indeterminate = false;
// eslint-disable-next-line no-return-assign,no-param-reassign
f.children?.forEach(ff => (ff.checked = checked));
}
});
this.filterService.refresh();
}
#updateFiltersInLocalStorage(): void {
if (this.fileId) {
const primaryFilters = this.filterService.getGroup('primaryFilters');
const secondaryFilters = this.filterService.getGroup('secondaryFilters');
const filters: LocalStorageFilters = {
primaryFilters: extractFilterValues(primaryFilters?.filters),
secondaryFilters: extractFilterValues(secondaryFilters?.filters),
};
const workloadFiltersString = localStorage.getItem('workload-filters') ?? '{}';
const workloadFilters = JSON.parse(workloadFiltersString);
workloadFilters[this.fileId] = filters;
localStorage.setItem('workload-filters', JSON.stringify(workloadFilters));
}
this.#filterService.refresh();
}
}

View File

@ -1,10 +1,11 @@
/* eslint-disable no-param-reassign */
import { INestedFilter } from './models/nested-filter.model';
import { IListable } from '../listing/models/listable';
import { Id } from '../listing/models/trackable';
import { IFilterGroup } from './models/filter-group.model';
import { IFilter } from './models/filter.model';
import { LocalStorageFilter } from './models/local-filters.model';
import { NestedFilter } from './models/nested-filter';
import { Id, IListable } from '../listing';
import { LocalStorageFilter } from './filter-card/filter-card.component';
import { INestedFilter } from './models/nested-filter.model';
function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[]) {
if (!oldFilters || !newFilters) {
@ -15,6 +16,7 @@ function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[])
const newFilter = newFilters.find(f => f.id === filter.id);
if (newFilter) {
newFilter.checked = filter.checked;
newFilter.expanded = filter.expanded;
newFilter.indeterminate = filter.indeterminate;
if (filter.children && newFilter.children) {
copySettings(filter.children, newFilter.children);

View File

@ -1,13 +1,31 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { map, startWith, switchMap } from 'rxjs/operators';
import { processFilters, toFlatFilters } from './filter-utils';
import { IFilterGroup } from './models/filter-group.model';
import { INestedFilter } from './models/nested-filter.model';
import { get, shareDistinctLast, shareLast, some } from '../utils';
import { NestedFilter } from './models/nested-filter';
import { extractFilterValues, handleCheckedValue, processFilters, toFlatFilters } from './filter-utils';
import { Filter } from './models/filter';
import { IFilterGroup } from './models/filter-group.model';
import { IFilter } from './models/filter.model';
import { LocalStorageFilters } from './models/local-filters.model';
import { NestedFilter } from './models/nested-filter';
import { INestedFilter } from './models/nested-filter.model';
export interface CheckboxClickedParams {
nestedFilter: INestedFilter;
filterGroup: IFilterGroup;
parent?: INestedFilter;
primaryFiltersSlug: string;
}
export interface DeactivateFiltersParams {
primaryFiltersSlug: string;
secondaryFiltersSlug?: string;
exceptedFilterId?: string;
}
const PRIMARY_FILTERS = 'primaryFilters';
const SECONDARY_FILTERS = 'secondaryFilters';
const WORKLOAD_FILTERS_KEY = 'workload-filters';
@Injectable()
export class FilterService {
@ -196,4 +214,70 @@ export class FilterService {
this.addSingleFilter(filter);
}
}
setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) {
const filters = this.getGroup(filterGroup)?.filters;
filters?.forEach(f => {
if (f.id !== exceptedFilterId) {
// eslint-disable-next-line no-param-reassign
f.checked = checked;
// eslint-disable-next-line no-param-reassign
f.indeterminate = false;
// eslint-disable-next-line no-return-assign,no-param-reassign
f.children?.forEach(ff => (ff.checked = checked));
}
});
this.refresh();
}
deactivateFilters(params: DeactivateFiltersParams) {
const { primaryFiltersSlug, secondaryFiltersSlug, exceptedFilterId } = params;
this.setFilters(primaryFiltersSlug, false, exceptedFilterId);
if (secondaryFiltersSlug) {
this.setFilters(secondaryFiltersSlug, false, exceptedFilterId);
}
}
filterCheckboxClicked(params: CheckboxClickedParams) {
const { filterGroup, nestedFilter, parent, primaryFiltersSlug } = params;
if (filterGroup.singleSelect) {
this.deactivateFilters({ primaryFiltersSlug, exceptedFilterId: nestedFilter.id });
}
// eslint-disable-next-line no-param-reassign
nestedFilter.checked = !nestedFilter.checked;
if (parent) {
handleCheckedValue(parent);
} else {
// eslint-disable-next-line no-param-reassign
if (nestedFilter.indeterminate) {
nestedFilter.checked = false;
}
// eslint-disable-next-line no-param-reassign
nestedFilter.indeterminate = false;
// eslint-disable-next-line no-return-assign,no-param-reassign
nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked));
}
this.refresh();
}
updateFiltersInLocalStorage(fileId?: string) {
if (fileId) {
const primaryFilters = this.getGroup(PRIMARY_FILTERS);
const secondaryFilters = this.getGroup(SECONDARY_FILTERS);
const filters: LocalStorageFilters = {
primaryFilters: extractFilterValues(primaryFilters?.filters),
secondaryFilters: extractFilterValues(secondaryFilters?.filters),
};
const workloadFiltersString = localStorage.getItem(WORKLOAD_FILTERS_KEY) ?? '{}';
const workloadFilters = JSON.parse(workloadFiltersString);
workloadFilters[fileId] = filters;
localStorage.setItem(WORKLOAD_FILTERS_KEY, JSON.stringify(workloadFilters));
}
}
}

View File

@ -1,19 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { TranslateModule } from '@ngx-translate/core';
import { ChevronButtonComponent, IconButtonComponent } from '../buttons';
import { PopupFilterComponent } from './popup-filter/popup-filter.component';
import { ChevronButtonComponent } from '../buttons/chevron-button/chevron-button.component';
import { IconButtonComponent } from '../buttons/icon-button/icon-button.component';
import { PreventDefaultDirective } from '../directives/prevent-default.directive';
import { StopPropagationDirective } from '../directives/stop-propagation.directive';
import { InputWithActionComponent } from '../inputs/input-with-action/input-with-action.component';
import { QuickFiltersComponent } from './quick-filters/quick-filters.component';
import { IqserHelpModeModule } from '../help-mode';
import { SingleFilterComponent } from './single-filter/single-filter.component';
import { FilterCardComponent } from './filter-card/filter-card.component';
import { MatIconModule } from '@angular/material/icon';
import { PreventDefaultDirective, StopPropagationDirective } from '../directives';
import { InputWithActionComponent } from '../inputs';
const components = [QuickFiltersComponent, PopupFilterComponent, SingleFilterComponent, FilterCardComponent];
const components = [QuickFiltersComponent, SingleFilterComponent];
@NgModule({
declarations: [...components],
@ -23,7 +22,6 @@ const components = [QuickFiltersComponent, PopupFilterComponent, SingleFilterCom
MatCheckboxModule,
MatMenuModule,
TranslateModule,
IqserHelpModeModule,
IconButtonComponent,
ChevronButtonComponent,
MatIconModule,

View File

@ -11,3 +11,4 @@ export * from './models/nested-filter.model';
export * from './popup-filter/popup-filter.component';
export * from './quick-filters/quick-filters.component';
export * from './simple-popup-filter/simple-popup-filter.component';

View File

@ -10,5 +10,6 @@ export interface IFilter {
readonly required?: boolean;
readonly disabled?: boolean;
readonly helpModeKey?: string;
readonly hidden?: boolean;
readonly metadata?: Record<string, any>;
}

View File

@ -1,5 +1,5 @@
import { IListable } from '../../listing/models/listable';
import { IFilter } from './filter.model';
import { IListable } from '../../listing';
export class Filter implements IFilter, IListable {
readonly id: string;
@ -10,6 +10,7 @@ export class Filter implements IFilter, IListable {
readonly checker?: (obj?: unknown) => boolean;
readonly skipTranslation?: boolean;
readonly metadata?: Record<string, any>;
readonly hidden?: boolean;
checked: boolean;
matches?: number;
@ -25,6 +26,7 @@ export class Filter implements IFilter, IListable {
this.required = !!filter.required;
this.skipTranslation = !!filter.skipTranslation;
this.metadata = filter.metadata;
this.hidden = !!filter.hidden;
}
get searchKey(): string {

View File

@ -0,0 +1,10 @@
export interface LocalStorageFilter {
id: string;
checked: boolean;
children?: LocalStorageFilter[] | null;
}
export interface LocalStorageFilters {
primaryFilters: LocalStorageFilter[] | null;
secondaryFilters: LocalStorageFilter[] | null;
}

View File

@ -1,4 +1,4 @@
import { IListable } from '../../listing';
import { IListable } from '../../listing/models/listable';
import { Filter } from './filter';
import { INestedFilter } from './nested-filter.model';
@ -8,8 +8,8 @@ export class NestedFilter extends Filter implements INestedFilter, IListable {
disabled?: boolean;
helpModeKey?: string;
readonly children: Filter[];
readonly skipTranslation?: boolean;
readonly metadata?: Record<string, any>;
override readonly skipTranslation?: boolean;
override readonly metadata?: Record<string, any>;
constructor(nestedFilter: INestedFilter) {
super(nestedFilter);

View File

@ -0,0 +1,4 @@
export interface SimpleFilterOption<T> {
value: T;
label: string;
}

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup">
<ng-container *ngIf="primaryGroup.icon">
@if (primaryFilterGroup$ | async; as primaryGroup) {
@if (primaryGroup.icon) {
<iqser-icon-button
[attr.aria-expanded]="expanded$ | async"
[class.disabled]="primaryFiltersDisabled$ | async"
@ -10,9 +10,9 @@
[showDot]="hasActiveFilters$ | async"
buttonId="{{ primaryGroup.slug }}"
></iqser-icon-button>
</ng-container>
}
<ng-container *ngIf="!primaryGroup.icon">
@if (!primaryGroup.icon) {
<iqser-chevron-button
[attr.aria-expanded]="expanded$ | async"
[class.disabled]="primaryFiltersDisabled$ | async"
@ -21,11 +21,11 @@
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async"
></iqser-chevron-button>
</ng-container>
}
<mat-menu
#filterMenu="matMenu"
(close)="expanded.next(false)"
(closed)="expanded.next(false)"
[class]="(secondaryFilterGroup$ | async)?.filters.length > 0 ? 'padding-bottom-0' : ''"
xPosition="before"
>
@ -41,4 +41,4 @@
</ng-template>
</div>
</mat-menu>
</ng-container>
}

View File

@ -1,16 +1,34 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core';
import { MatMenu, MatMenuContent, MatMenuTrigger } from '@angular/material/menu';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateModule } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { delay, map } from 'rxjs/operators';
import { shareDistinctLast, shareLast, some } from '../../utils';
import { ChevronButtonComponent } from '../../buttons/chevron-button/chevron-button.component';
import { IconButtonComponent } from '../../buttons/icon-button/icon-button.component';
import { StopPropagationDirective } from '../../directives/stop-propagation.directive';
import { shareDistinctLast, shareLast, some } from '../../utils/operators';
import { FilterCardComponent } from '../filter-card/filter-card.component';
import { FilterService } from '../filter.service';
import { IFilterGroup } from '../models/filter-group.model';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-popup-filter [primaryFiltersSlug]',
templateUrl: './popup-filter.component.html',
styleUrls: ['./popup-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
IconButtonComponent,
AsyncPipe,
TranslateModule,
MatMenuTrigger,
ChevronButtonComponent,
MatMenu,
FilterCardComponent,
StopPropagationDirective,
MatMenuContent,
],
})
export class PopupFilterComponent implements OnInit {
@Input() primaryFiltersSlug!: string;

View File

@ -1,12 +1,13 @@
<ng-container *ngIf="quickFilters$ | async as filters">
<div
(click)="filterService.toggleFilter('quickFilters', filter.id)"
*ngFor="let filter of filters"
[class.active]="filter.checked"
[class.disabled]="filter.disabled"
class="quick-filter"
[attr.help-mode-key]="filter.helpModeKey"
>
{{ filter.label }}
</div>
</ng-container>
@if (quickFilters$ | async; as filters) {
@for (filter of filters; track filter.id) {
<div
(click)="filterService.toggleFilter('quickFilters', filter.id)"
[class.active]="filter.checked"
[class.disabled]="filter.disabled"
class="quick-filter"
[attr.help-mode-key]="filter.helpModeKey"
>
{{ filter.label }}
</div>
}
}

View File

@ -6,6 +6,7 @@ import { FilterService } from '../filter.service';
templateUrl: './quick-filters.component.html',
styleUrls: ['./quick-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');

View File

@ -0,0 +1,66 @@
@if (type() === 'text' && icon()) {
<iqser-icon-button
[attr.aria-expanded]="expanded()"
[class.disabled]="disabled()"
[disabled]="disabled()"
[icon]="icon()"
[label]="label()"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters()"
></iqser-icon-button>
}
@if (type() === 'text' && !icon()) {
<iqser-chevron-button
[attr.aria-expanded]="expanded()"
[class.disabled]="disabled()"
[disabled]="disabled()"
[label]="label()"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters()"
></iqser-chevron-button>
}
@if (type() === 'icon') {
<iqser-circle-button
[attr.aria-expanded]="expanded()"
[class.disabled]="disabled()"
[disabled]="disabled()"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters()"
[icon]="icon() || 'iqser:filter-list'"
></iqser-circle-button>
}
<mat-menu #filterMenu="matMenu" (closed)="expanded.set(false)" xPosition="before">
<div iqserStopPropagation>
<ng-template matMenuContent>
<div class="input-wrapper">
<iqser-input-with-action
(valueChange)="searchValue.set($event)"
[placeholder]="filterPlaceholder()"
[value]="searchValue()"
[width]="'full'"
></iqser-input-with-action>
</div>
<div class="filter-menu-header">
<div class="all-caps-label" translate="filter-menu.label"></div>
<div class="actions">
<div (click)="_selectAll()" class="all-caps-label primary pointer" iqserStopPropagation translate="actions.all"></div>
<div (click)="_clear()" class="all-caps-label primary pointer" iqserStopPropagation translate="actions.none"></div>
</div>
</div>
<div class="filter-content">
@for (option of displayedOptions(); track option) {
<div mat-menu-item (click)="_filterCheckboxClicked(option)">
<mat-checkbox [checked]="selectedOptions().includes(option)" class="filter-menu-checkbox">
<span class="clamp-1">{{ option.label }}</span>
</mat-checkbox>
</div>
}
</div>
</ng-template>
</div>
</mat-menu>

View File

@ -0,0 +1,25 @@
@use 'common-mixins';
.filter-menu-options,
.filter-menu-header {
display: flex;
justify-content: space-between;
padding: 8px 16px 16px 16px;
min-width: var(--filter-card-min-width);
.actions {
display: flex;
gap: 8px;
}
}
.input-wrapper {
padding: 0 8px 8px 8px;
}
.filter-content {
max-height: 300px;
max-width: 500px;
overflow: auto;
@include common-mixins.scroll-bar;
}

View File

@ -0,0 +1,81 @@
import { Component, computed, effect, input, output, signal, untracked } from '@angular/core';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatMenuModule } from '@angular/material/menu';
import { TranslateModule } from '@ngx-translate/core';
import { CircleButtonComponent, IconButtonComponent } from '../../buttons';
import { ChevronButtonComponent } from '../../buttons/chevron-button/chevron-button.component';
import { StopPropagationDirective } from '../../directives';
import { InputWithActionComponent } from '../../inputs/input-with-action/input-with-action.component';
import { SimpleFilterOption } from '../models/simple-filter-option';
@Component({
selector: 'iqser-simple-popup-filter',
templateUrl: './simple-popup-filter.component.html',
styleUrls: ['./simple-popup-filter.component.scss'],
imports: [
MatMenuModule,
IconButtonComponent,
ChevronButtonComponent,
StopPropagationDirective,
InputWithActionComponent,
TranslateModule,
MatCheckbox,
IconButtonComponent,
CircleButtonComponent,
],
})
export class SimplePopupFilterComponent<T> {
readonly options = input.required<SimpleFilterOption<T>[]>();
readonly icon = input<string>();
readonly label = input<string>();
readonly filterPlaceholder = input.required<string>();
readonly disabled = input<boolean>(false);
readonly type = input<'text' | 'icon'>('text');
readonly selectionChanged = output<SimpleFilterOption<T>[]>();
readonly expanded = signal(false);
readonly selectedOptions = signal<SimpleFilterOption<T>[]>([]);
readonly hasActiveFilters = computed(() => this.selectedOptions().length > 0);
readonly searchValue = signal('');
readonly displayedOptions = computed(() =>
this.options().filter(option => option.label.toLowerCase().includes(this.searchValue().toLowerCase())),
);
constructor() {
effect(() => {
this.selectionChanged.emit(this.selectedOptions());
});
/** If the options change and the selected options are not in the new options, remove them. */
effect(
() => {
const allOptions = this.options();
const selectedOptions = untracked(this.selectedOptions);
if (selectedOptions.some(selectedOption => !allOptions.find(o => o.value === selectedOption))) {
this.selectedOptions.set(
selectedOptions.filter(selectedOption => allOptions.find(o => o.value === selectedOption.value)),
);
}
},
{ allowSignalWrites: true },
);
}
protected _selectAll(): void {
this.selectedOptions.set(untracked(this.options));
}
protected _clear(): void {
this.selectedOptions.set([]);
}
protected _filterCheckboxClicked(option: SimpleFilterOption<T>): void {
if (this.selectedOptions().includes(option)) {
this.selectedOptions.set(this.selectedOptions().filter(selectedOption => selectedOption !== option));
} else {
this.selectedOptions.set([...this.selectedOptions(), option]);
}
}
}

View File

@ -7,6 +7,7 @@ import { IFilter } from '../models/filter.model';
selector: 'iqser-single-filter',
templateUrl: './single-filter.component.html',
styleUrls: ['./single-filter.component.scss'],
standalone: false,
})
export class SingleFilterComponent {
@Input() filter!: IFilter;

View File

@ -1,6 +1,15 @@
<iqser-circle-button
(action)="activateHelpMode()"
[tooltip]="'help-mode.button-text' | translate"
icon="iqser:help-outline"
type="help"
></iqser-circle-button>
<label
[id]="buttonId"
class="help-mode-slide-toggle"
[class.dialog-toggle]="dialogButton"
[class.active]="helpModeService.isHelpModeActive()"
[matTooltip]="buttonTooltip"
#helpModeButton
>
<input type="checkbox" class="toggle-input" [checked]="helpModeService.isHelpModeActive()" (change)="toggleHelpMode()" />
<div class="toggle-track">
<div class="toggle-thumb">
<mat-icon svgIcon="iqser:help-outline" [class.active-thumb]="helpModeService.isHelpModeActive()"></mat-icon>
</div>
</div>
</label>

View File

@ -0,0 +1,65 @@
:host {
display: flex;
align-items: center;
width: 40px;
height: 24px;
}
.help-mode-slide-toggle {
display: inline-block;
position: relative;
width: 40px;
height: 24px;
cursor: pointer;
&.active,
&.dialog-toggle {
z-index: 1200;
}
.toggle-input {
display: none;
}
.toggle-track {
position: relative;
display: flex;
align-items: center;
justify-content: start;
width: 100%;
height: 25px;
background-color: var(--iqser-grey-4);
border-radius: 20px;
}
.toggle-thumb {
margin-left: 6%;
width: 20px;
height: 20px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: margin-left 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
mat-icon {
transform: scale(0.6);
}
.active-thumb {
color: var(--iqser-helpmode-primary);
}
}
.toggle-input:checked + .toggle-track {
background: var(--iqser-helpmode-primary);
}
.toggle-input:checked + .toggle-track {
.toggle-thumb {
margin-left: 43.5%;
}
}
}

View File

@ -1,31 +1,95 @@
/* eslint-disable @angular-eslint/prefer-on-push-component-change-detection */
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { HelpModeService } from '../index';
import { Component, effect, ElementRef, HostListener, Input, OnDestroy, OnInit, viewChild } from '@angular/core';
import { MatIcon } from '@angular/material/icon';
import { HelpModeService } from '../help-mode.service';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
selector: 'iqser-help-button',
templateUrl: './help-button.component.html',
styleUrls: ['./help-button.component.scss'],
imports: [MatIcon, MatTooltip],
})
export class HelpButtonComponent implements OnInit, OnDestroy {
@Input() dialogButton = false;
@Input() helpButtonKey?: string;
#helpModeHasBeenActivated = false;
readonly helpModeButton = viewChild.required<ElementRef>('helpModeButton');
@Input() dialogButton = true;
constructor(private readonly _helpModeService: HelpModeService) {}
ngOnInit(): void {
this._helpModeService.helpButtonKey = this.helpButtonKey;
constructor(
private readonly _elementRef: ElementRef,
private readonly _translateService: TranslateService,
readonly helpModeService: HelpModeService,
) {
effect(() => {
if (this.helpModeService.isHelpModeActive()) {
this.#helpModeHasBeenActivated = true;
setTimeout(() => this.#applyActiveButtonStyles(), 300);
} else if (this.#helpModeHasBeenActivated) {
setTimeout(() => this.#applyInactiveButtonStyles(), 300);
}
});
}
ngOnDestroy(): void {
this._helpModeService.helpButtonKey = undefined;
get buttonTooltip() {
const translation = this.helpModeService.isHelpModeActive() ? _('help-button.disable') : _('help-button.enable');
return this._translateService.instant(translation);
}
activateHelpMode(): void {
if (this.helpButtonKey) {
const url = this._helpModeService.generateDocsLink(this.helpButtonKey);
window.open(url, '_blank');
get buttonId() {
return `help-mode-button${this.dialogButton ? '-dialog' : ''}`;
}
get currentComponentRect() {
return this._elementRef.nativeElement.getBoundingClientRect();
}
@HostListener('window:resize', ['$event'])
onResize() {
if (this.helpModeService.isHelpModeActive()) this.#applyActiveButtonStyles();
}
ngOnInit() {
if (this.dialogButton) {
const defaultButton = document.getElementById('help-mode-button') as HTMLElement;
defaultButton.style.setProperty('z-index', '100');
}
}
ngOnDestroy() {
if (this.dialogButton) {
const defaultButton = document.getElementById('help-mode-button') as HTMLElement;
defaultButton.style.removeProperty('z-index');
if (!this.helpModeService.isHelpModeActive()) {
const helpButtonElement = document.querySelectorAll('iqser-help-button')[this.dialogButton ? 1 : 0];
if (helpButtonElement.contains(this.helpModeButton().nativeElement))
helpButtonElement?.removeChild(this.helpModeButton().nativeElement);
}
}
}
toggleHelpMode(): void {
if (this.helpModeService.isHelpModeActive()) {
this.helpModeService.deactivateHelpMode();
return;
}
this._helpModeService.activateHelpMode(this.dialogButton);
this.helpModeService.activateHelpMode(this.dialogButton);
}
#applyActiveButtonStyles() {
this.helpModeButton().nativeElement.style.setProperty('position', 'absolute');
this.helpModeButton().nativeElement.style.setProperty('top', `${this.currentComponentRect.top}px`);
this.helpModeButton().nativeElement.style.setProperty('left', `${this.currentComponentRect.left}px`);
document.body.appendChild(this.helpModeButton().nativeElement);
}
#applyInactiveButtonStyles() {
this.helpModeButton().nativeElement.style.setProperty('position', 'relative');
this.helpModeButton().nativeElement.style.setProperty('top', 'unset');
this.helpModeButton().nativeElement.style.setProperty('left', 'unset');
document.body.removeChild(this.helpModeButton().nativeElement);
document.querySelectorAll('iqser-help-button')[this.dialogButton ? 1 : 0]?.appendChild(this.helpModeButton().nativeElement);
}
}

View File

@ -3,6 +3,9 @@
<p class="heading-l pre" [innerHTML]="'help-mode.welcome-to-help-mode' | translate"></p>
<img src="assets/illustrations/illustration.gif" alt="" width="335" />
<p class="pre" [innerHTML]="'help-mode.clicking-anywhere-on' | translate"></p>
<mat-checkbox [checked]="doNotShowAgainOption" (change)="setDoNotShowAgainOption($event.checked)" color="primary">
{{ 'help-mode.options.do-not-show-again' | translate }}
</mat-checkbox>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
</section>

View File

@ -1,4 +1,9 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Subscription } from 'rxjs';
import { MatCheckbox } from '@angular/material/checkbox';
import { CircleButtonComponent } from '../../buttons';
import { TranslateModule } from '@ngx-translate/core';
const HIGHER_CDK_OVERLAY_CONTAINER_ZINDEX = '1200';
const DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX = '800';
@ -7,17 +12,36 @@ const DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX = '800';
templateUrl: './help-mode-dialog.component.html',
styleUrls: ['./help-mode-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatCheckbox, CircleButtonComponent, TranslateModule],
})
export class HelpModeDialogComponent implements OnInit, OnDestroy {
#backdropClickSubscription: Subscription;
protected doNotShowAgainOption = false;
constructor(protected readonly _dialogRef: MatDialogRef<HelpModeDialogComponent>) {
this.#backdropClickSubscription = this._dialogRef.backdropClick().subscribe(() => this.close());
}
@HostListener('window:keydown.Enter', ['$event'])
@HostListener('window:keydown.Escape', ['$event'])
close() {
return this._dialogRef.close(this.doNotShowAgainOption);
}
ngOnInit(): void {
this._setCdkOverlayContainerZindex(HIGHER_CDK_OVERLAY_CONTAINER_ZINDEX);
this._setCdkOverlayContainerZIndex(HIGHER_CDK_OVERLAY_CONTAINER_ZINDEX);
}
ngOnDestroy(): void {
this._setCdkOverlayContainerZindex(DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX);
this._setCdkOverlayContainerZIndex(DEFAULT_CDK_OVERLAY_CONTAINER_ZINDEX);
this.#backdropClickSubscription.unsubscribe();
}
private _setCdkOverlayContainerZindex(zIndex: string): void {
setDoNotShowAgainOption(checked: boolean): void {
this.doNotShowAgainOption = checked;
}
private _setCdkOverlayContainerZIndex(zIndex: string): void {
const cdkOverlayContainer = document.querySelector<HTMLElement>('.cdk-overlay-container');
if (cdkOverlayContainer) {
cdkOverlayContainer.style.zIndex = zIndex;

View File

@ -1,26 +0,0 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
import { HelpModeComponent } from './help-mode/help-mode.component';
import { HelpButtonComponent } from './help-button/help-button.component';
import { HelpModeKey, HelpModeService } from './help-mode.service';
import { MatDialogModule } from '@angular/material/dialog';
import { CircleButtonComponent } from '../buttons';
import { HELP_MODE_KEYS } from './tokens';
const components = [HelpModeComponent, HelpModeDialogComponent, HelpButtonComponent];
@NgModule({
declarations: [...components],
imports: [CommonModule, MatDialogModule, TranslateModule, CircleButtonComponent],
exports: [...components],
})
export class IqserHelpModeModule {
static forRoot(helpModeKeys: HelpModeKey[]): ModuleWithProviders<IqserHelpModeModule> {
return {
ngModule: IqserHelpModeModule,
providers: [{ provide: HELP_MODE_KEYS, useValue: helpModeKeys }, HelpModeService],
};
}
}

View File

@ -1,9 +1,12 @@
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { getConfig } from '../services';
import { IqserUserPreferenceService } from '../services';
import { HelpModeDialogComponent } from './help-mode-dialog/help-mode-dialog.component';
import { HELP_MODE_KEYS, MANUAL_BASE_URL } from './tokens';
import { HelpModeKey } from './types';
import {
DOCUMINE_THEME_CLASS,
HELP_HIGHLIGHT_CLASS,
@ -17,7 +20,7 @@ import {
ScrollableParentViews,
WEB_VIEWER_ELEMENTS,
} from './utils/constants';
import { getConfig } from '../services';
import { toSignal } from '@angular/core/rxjs-interop';
export interface Helper {
readonly element: HTMLElement;
@ -28,25 +31,17 @@ export interface Helper {
readonly iframeElement?: boolean;
}
export interface HelpModeKey {
readonly elementKey: string;
readonly documentKey: string;
readonly scrollableParentView?: ScrollableParentView;
readonly overlappingElements?: OverlappingElement[];
readonly dialogElement?: boolean;
}
@Injectable()
export class HelpModeService {
helpButtonKey: string | undefined;
readonly #isHelpModeActive$ = new BehaviorSubject(false);
readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable();
readonly #helpModeDialogIsOpened$ = new BehaviorSubject(false);
readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable();
readonly #renderer: Renderer2;
readonly #isDocumine = getConfig().IS_DOCUMINE;
#helpers: Record<string, Helper> = {};
#dialogMode = false;
readonly isHelpModeActive$ = this.#isHelpModeActive$.asObservable();
readonly isHelpModeActive = toSignal(this.isHelpModeActive$, { initialValue: false });
readonly helpModeDialogIsOpened$ = this.#helpModeDialogIsOpened$.asObservable();
constructor(
@Inject(HELP_MODE_KEYS) private readonly _keys: HelpModeKey[],
@ -54,33 +49,34 @@ export class HelpModeService {
private readonly _dialog: MatDialog,
private readonly _rendererFactory: RendererFactory2,
private readonly _translateService: TranslateService,
private readonly _iqserUserPreferenceService: IqserUserPreferenceService,
) {
this.#renderer = this._rendererFactory.createRenderer(null, null);
}
get isHelpModeActive(): boolean {
return this.#isHelpModeActive$.getValue();
}
get helpModeDialogIsOpened(): boolean {
return this.#helpModeDialogIsOpened$.getValue();
}
openHelpModeDialog(): MatDialogRef<HelpModeDialogComponent> {
this.#helpModeDialogIsOpened$.next(true);
async openHelpModeDialog() {
if (!this._iqserUserPreferenceService.getHelpModeDialog()) {
this.#helpModeDialogIsOpened$.next(true);
const ref = this._dialog.open(HelpModeDialogComponent, {
width: '600px',
});
const ref = this._dialog.open(HelpModeDialogComponent, {
width: '600px',
});
firstValueFrom(ref.afterClosed()).then(() => {
this.#helpModeDialogIsOpened$.next(false);
});
return ref;
firstValueFrom(ref.afterClosed()).then(result => {
this.#helpModeDialogIsOpened$.next(false);
if (result) {
this._iqserUserPreferenceService.toggleHelpModeDialog();
}
});
}
}
activateHelpMode(dialogMode: boolean = false): void {
if (!this.isHelpModeActive) {
if (!this.isHelpModeActive()) {
document.body.style.setProperty('overflow', 'unset');
this.#isHelpModeActive$.next(true);
this.openHelpModeDialog();
@ -94,7 +90,7 @@ export class HelpModeService {
}
deactivateHelpMode(): void {
if (this.isHelpModeActive) {
if (this.isHelpModeActive()) {
document.body.style.removeProperty('overflow');
this.#isHelpModeActive$.next(false);
this.#disableHelperElements();
@ -102,6 +98,19 @@ export class HelpModeService {
}
}
highlightHelperElements(): void {
Object.values(this.#helpers).forEach(helper => {
this.#renderer.addClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
setTimeout(() => {
this.#renderer.removeClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
}, 500);
});
}
updateHelperElements() {
Object.values(this.#helpers).forEach(helper => this.#updateHelperElement(helper));
}
#createHelpers() {
for (const key of Object.values(this._keys)) {
const elements = document.querySelectorAll(`[help-mode-key='${key.elementKey}']`);
@ -137,7 +146,7 @@ export class HelpModeService {
#getHelperElement(element: HTMLElement, key: string): HTMLElement {
const helperElement = this.#renderer.createElement('a') as HTMLElement;
this.#renderer.setAttribute(helperElement, 'href', this.generateDocsLink(key));
this.#renderer.setAttribute(helperElement, 'href', this.#generateDocsLink(key));
this.#renderer.setAttribute(helperElement, 'target', '_blank');
this.#renderer.addClass(helperElement, HELP_MODE_CLASS);
if (this.#isDocumine) {
@ -150,24 +159,11 @@ export class HelpModeService {
return Math.random().toString(36).substring(2, 9);
}
generateDocsLink(key: string) {
#generateDocsLink(key: string) {
const currentLang = this._translateService.currentLang;
return `${this._manualBaseURL}/${currentLang}/index-${currentLang}.html?contextId=${key}`;
}
highlightHelperElements(): void {
Object.values(this.#helpers).forEach(helper => {
this.#renderer.addClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
setTimeout(() => {
this.#renderer.removeClass(helper.helperElement, HELP_HIGHLIGHT_CLASS);
}, 500);
});
}
updateHelperElements() {
Object.values(this.#helpers).forEach(helper => this.#updateHelperElement(helper));
}
#isElementVisible(helper: Helper): boolean {
if (helper.iframeElement && !this.#isFilePreviewPage()) {
return false;
@ -246,6 +242,7 @@ export class HelpModeService {
const iframe: HTMLIFrameElement = document.getElementById(PDF_TRON_IFRAME_ID) as HTMLIFrameElement;
const iframeRect = iframe.getBoundingClientRect();
dimensions.y += iframeRect.top;
dimensions.x += iframeRect.left;
}
helper.helperElement.style.cssText = `

View File

@ -1,20 +1,23 @@
<div *ngIf="helpModeService.isHelpModeActive$ | async">
<div class="help-mode-border"></div>
<div class="bottom small-label full-opacity">
<strong>{{ 'help-mode.bottom-text' | translate }}</strong>
<a (click)="helpModeService.openHelpModeDialog()" *ngIf="(helpModeService.helpModeDialogIsOpened$ | async) === false">
{{ 'help-mode.instructions' | translate }}
</a>
<div class="close">
(esc)
<iqser-circle-button
(click)="helpModeService.deactivateHelpMode()"
[iconSize]="10"
[size]="20"
[type]="circleButtonTypes.help"
class="ml-8"
icon="iqser:close"
></iqser-circle-button>
@if (helpModeService.isHelpModeActive$ | async) {
<div>
<div class="help-mode-border"></div>
<div class="bottom small-label full-opacity">
<strong>{{ 'help-mode.bottom-text' | translate }}</strong>
@if ((helpModeService.helpModeDialogIsOpened$ | async) === false) {
<a (click)="helpModeService.openHelpModeDialog()">
{{ 'help-mode.instructions' | translate }}
</a>
}
<div class="close">
(esc)
<iqser-circle-button
(click)="helpModeService.deactivateHelpMode()"
[iconSize]="10"
[size]="20"
class="ml-8"
icon="iqser:close"
></iqser-circle-button>
</div>
</div>
</div>
</div>
}

View File

@ -2,13 +2,16 @@ import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core'
import { HelpModeService } from '../help-mode.service';
import { IqserEventTarget } from '../../utils';
import { MatDialog } from '@angular/material/dialog';
import { CircleButtonTypes } from '../../buttons';
import { CircleButtonComponent, CircleButtonTypes } from '../../buttons';
import { AsyncPipe } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'iqser-help-mode',
templateUrl: './help-mode.component.html',
styleUrls: ['./help-mode.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe, TranslateModule, CircleButtonComponent],
})
export class HelpModeComponent {
readonly circleButtonTypes = CircleButtonTypes;
@ -20,7 +23,7 @@ export class HelpModeComponent {
@HostListener('document:keydown.escape', ['$event'])
onEscKeydownHandler(event: KeyboardEvent): void {
if (!this.helpModeService.helpModeDialogIsOpened && this.helpModeService.isHelpModeActive) {
if (!this.helpModeService.helpModeDialogIsOpened && this.helpModeService.isHelpModeActive()) {
event?.stopPropagation();
this.helpModeService.deactivateHelpMode();
}
@ -29,12 +32,7 @@ export class HelpModeComponent {
@HostListener('document:keydown.h', ['$event'])
onHKeydownHandler(event: KeyboardEvent): void {
const node = (event.target as IqserEventTarget).localName;
if (!this.helpModeService.isHelpModeActive && node !== 'input' && node !== 'textarea') {
if (this.helpModeService.helpButtonKey) {
const url = this.helpModeService.generateDocsLink(this.helpModeService.helpButtonKey);
window.open(url, '_blank');
return;
}
if (!this.helpModeService.isHelpModeActive() && node !== 'input' && node !== 'textarea') {
const dialogMode = !!this._dialog.openDialogs.length;
this.helpModeService.activateHelpMode(dialogMode);
}
@ -42,14 +40,14 @@ export class HelpModeComponent {
@HostListener('click')
onClick(): void {
if (this.helpModeService.isHelpModeActive) {
if (this.helpModeService.isHelpModeActive()) {
this.helpModeService.highlightHelperElements();
}
}
@HostListener('window:resize')
onResize() {
if (this.helpModeService.isHelpModeActive) {
if (this.helpModeService.isHelpModeActive()) {
this.helpModeService.updateHelperElements();
}
}

View File

@ -1,7 +1,7 @@
export * from './tokens';
export * from './help-mode.module';
export * from './help-mode.service';
export * from './help-mode/help-mode.component';
export * from './help-button/help-button.component';
export * from './help-mode-dialog/help-mode-dialog.component';
export * from './utils/constants';
export * from './utils/help-mode.provider';

View File

@ -1,6 +1,6 @@
import { inject, InjectionToken } from '@angular/core';
import { IqserConfigService } from '../services';
import { HelpModeKey } from './help-mode.service';
import { IqserConfigService } from '../services/iqser-config.service';
import { HelpModeKey } from './types';
export const HELP_MODE_KEYS = new InjectionToken<HelpModeKey>('Help mode keys');
export const MANUAL_BASE_URL = new InjectionToken<string>('Base manual URL', {

View File

@ -0,0 +1,9 @@
import { OverlappingElement, ScrollableParentView } from './utils/constants';
export interface HelpModeKey {
readonly elementKey: string;
readonly documentKey: string;
readonly scrollableParentView?: ScrollableParentView;
readonly overlappingElements?: OverlappingElement[];
readonly dialogElement?: boolean;
}

View File

@ -9,7 +9,7 @@ export const PDF_TRON_IFRAME_ID = 'webviewer-1';
export const WEB_VIEWER_ELEMENTS = [
{
querySelector: '.HeaderItems',
documentKey: 'pdf_features',
documentKey: 'document_viewer_features',
},
];
@ -18,6 +18,7 @@ export const ScrollableParentViews = {
ANNOTATIONS_LIST: 'ANNOTATIONS_LIST',
SCM_EDIT_DIALOG: 'SCM_EDIT_DIALOG',
WORKFLOW_VIEW: 'WORKFLOW_VIEW',
COMPONENTS_VIEW: 'COMPONENTS_VIEW',
} as const;
export const SCROLLABLE_PARENT_VIEWS_IDS = {
@ -25,6 +26,7 @@ export const SCROLLABLE_PARENT_VIEWS_IDS = {
ANNOTATIONS_LIST: 'annotations-list',
SCM_EDIT_DIALOG: 'scm-edit',
WORKFLOW_VIEW: 'workflow-view',
COMPONENTS_VIEW: 'components-view',
} as const;
export type ScrollableParentView = keyof typeof ScrollableParentViews;

View File

@ -0,0 +1,7 @@
import { HelpModeService } from '../help-mode.service';
import { HELP_MODE_KEYS } from '../tokens';
import { HelpModeKey } from '../types';
export function provideHelpMode(helpModeKeys: HelpModeKey[]) {
return [{ provide: HELP_MODE_KEYS, useValue: helpModeKeys }, HelpModeService];
}

View File

@ -1,18 +1,29 @@
interface ExtraOption {
interface AdditionalField {
label: string;
checked: boolean;
description?: string;
}
interface AdditionalCheck extends AdditionalField {
checked?: boolean;
hidden?: boolean;
disabled?: boolean;
}
interface AdditionalInput extends AdditionalField {
value: string;
placeholder?: string;
errorCode?: string;
}
export interface DetailsRadioOption<I> {
id?: string;
label: string;
description: string;
descriptionParams?: Record<string, string>;
descriptionParams?: Record<string, string | number>;
icon?: string;
value: I;
disabled?: boolean;
tooltip?: string;
extraOption?: ExtraOption;
additionalCheck?: AdditionalCheck;
additionalInput?: AdditionalInput;
}

View File

@ -1,45 +1,83 @@
<div [class.row]="displayInRow" class="iqser-input-group">
<div
(click)="toggleOption(option)"
*ngFor="let option of options"
[class.active]="option.value === value?.value"
[class.disabled]="option.disabled"
[id]="groupId(option)"
[matTooltipPosition]="'above'"
[matTooltip]="option.tooltip || '' | translate"
[ngClass]="{ 'mb-8': !displayInRow, 'mr-8': displayInRow }"
class="option pointer"
>
<div *ngIf="option.icon; else withoutIcon" class="icon-option">
<mat-icon [svgIcon]="option.icon" class="icon"></mat-icon>
<div [class.row]="displayInRow()" class="iqser-input-group">
@for (option of options(); track option) {
<div
(click)="toggleOption(option)"
[class.active]="isSelected(option)"
[class.disabled]="option.disabled"
[id]="groupId(option)"
[matTooltipPosition]="'above'"
[matTooltip]="option.tooltip || '' | translate"
[ngClass]="{ 'mb-8': !displayInRow(), 'mr-8': displayInRow() }"
class="option pointer"
>
@if (option.icon) {
<div class="icon-option">
<mat-icon [svgIcon]="option.icon" class="icon"></mat-icon>
<div class="text">
<label class="details-radio-label pointer">{{
option.label | translate: option.descriptionParams | replaceNbsp
}}</label>
<div class="text">
<label class="details-radio-label pointer">{{ option.label | translate: option.descriptionParams }}</label>
<span class="hint">{{ option.description | translate: option.descriptionParams | replaceNbsp }}</span>
<span class="hint">{{ option.description | translate: option.descriptionParams | replaceNbsp }}</span>
<div *ngIf="option.extraOption && !option.extraOption.hidden && isSelected(option)" class="iqser-input-group">
<mat-checkbox
(change)="emitExtraOption()"
[(ngModel)]="option.extraOption.checked"
[checked]="option.extraOption.checked"
[disabled]="!!option.extraOption.disabled"
color="primary"
>
{{ option.extraOption.label | translate }}
</mat-checkbox>
@if (isSelected(option)) {
@if (option.additionalCheck && !option.additionalCheck.hidden) {
<div class="iqser-input-group w-450">
<mat-checkbox
(change)="emitExtraOption()"
[(ngModel)]="option.additionalCheck.checked"
[checked]="option.additionalCheck.checked"
[disabled]="!!option.additionalCheck.disabled"
color="primary"
>
{{ option.additionalCheck.label | translate | replaceNbsp }}
</mat-checkbox>
@if (option.additionalCheck.description) {
<span
[innerHTML]="option.additionalCheck.description | translate"
class="hint additional-check-description"
></span>
}
</div>
}
@if (option.additionalInput) {
<div class="iqser-input-group w-full additional-input">
<span class="label"> {{ option.additionalInput.label | translate }} </span>
<div class="flex-column">
<input
[(ngModel)]="option.additionalInput.value"
[ngClass]="{ error: additionalInputTouched && hasError(option.additionalInput.errorCode) }"
[placeholder]="
option.additionalInput.placeholder ? (option.additionalInput.placeholder | translate) : ''
"
(blur)="additionalInputTouched = true"
(focus)="additionalInputTouched = false"
(keydown)="emitExtraOption()"
/>
@if (option.additionalInput.description) {
<span class="hint" [innerHTML]="option.additionalInput.description | translate"></span>
}
</div>
</div>
}
}
</div>
@if (isSelected(option)) {
<mat-icon class="checked" svgIcon="iqser:radio-selected"></mat-icon>
}
</div>
</div>
} @else {
<div class="flex-align-items-center mb-8">
<iqser-round-checkbox [active]="isSelected(option)" class="mr-6"></iqser-round-checkbox>
<mat-icon *ngIf="isSelected(option)" class="checked" svgIcon="iqser:radio-selected"></mat-icon>
<label class="details-radio-label pointer">{{ option.label | translate | replaceNbsp }}</label>
</div>
<span class="hint">{{ option.description | translate | replaceNbsp }}</span>
}
</div>
<ng-template #withoutIcon>
<div class="flex-align-items-center mb-8">
<iqser-round-checkbox [active]="isSelected(option)" class="mr-6"></iqser-round-checkbox>
<label class="details-radio-label pointer">{{ option.label | translate }}</label>
</div>
<span class="hint">{{ option.description | translate }}</span>
</ng-template>
</div>
}
</div>

View File

@ -42,6 +42,45 @@ label {
color: var(--iqser-primary);
}
}
.additional-check-description {
margin-left: 23px;
opacity: 0.49;
}
.additional-input {
display: flex;
flex-direction: row;
gap: 10px;
span {
margin-top: 8px;
font-size: 12px;
}
div {
.error {
border-color: var(--iqser-red-1);
}
display: flex;
span {
font-size: 10px;
margin-top: 4px;
}
}
.flex-column {
flex: 1;
input {
width: 232px;
min-height: 30px;
height: 30px;
}
}
}
}
.row {

View File

@ -1,5 +1,5 @@
import { NgClass, NgForOf, NgIf } from '@angular/common';
import { booleanAttribute, Component, EventEmitter, Input, Output } from '@angular/core';
import { NgClass } from '@angular/common';
import { booleanAttribute, Component, input, output } from '@angular/core';
import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
@ -14,7 +14,6 @@ import { DetailsRadioOption } from './details-radio-option';
selector: 'iqser-details-radio',
templateUrl: './details-radio.component.html',
styleUrls: ['./details-radio.component.scss'],
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -28,12 +27,10 @@ import { DetailsRadioOption } from './details-radio-option';
},
],
imports: [
NgForOf,
NgClass,
RoundCheckboxComponent,
TranslateModule,
MatIconModule,
NgIf,
FormsModule,
MatCheckboxModule,
ReactiveFormsModule,
@ -42,12 +39,12 @@ import { DetailsRadioOption } from './details-radio-option';
],
})
export class DetailsRadioComponent<I> extends FormFieldComponent<DetailsRadioOption<I>> {
@Input({ required: true }) options: DetailsRadioOption<I>[] = [];
@Input({ transform: booleanAttribute }) displayInRow = false;
readonly options = input.required<DetailsRadioOption<I>[]>();
readonly displayInRow = input(false, { transform: booleanAttribute });
readonly extraOptionChanged = output<DetailsRadioOption<I>>();
additionalInputTouched = false;
@Output() readonly extraOptionChanged: EventEmitter<DetailsRadioOption<I>> = new EventEmitter();
toggleOption(option: DetailsRadioOption<I>): void {
toggleOption(option: DetailsRadioOption<I>) {
if (option.value !== this._value?.value && !option.disabled) {
this.markAsTouched();
const currentlyChecked = this.value?.value === option.value;
@ -56,15 +53,20 @@ export class DetailsRadioComponent<I> extends FormFieldComponent<DetailsRadioOpt
}
}
groupId(option: DetailsRadioOption<I>): string {
groupId(option: DetailsRadioOption<I>) {
return (option.id ?? option.label.replace('.', '-')) + '-checkbox-details-input';
}
isSelected(option: DetailsRadioOption<I>): boolean {
isSelected(option: DetailsRadioOption<I>) {
return option.value === this.value?.value;
}
emitExtraOption(): void {
emitExtraOption() {
if (!this.value) {
console.error('Extra option selected but the value is undefined');
return;
}
this.extraOptionChanged.emit(this.value);
}
}

View File

@ -1,40 +1,46 @@
<div [class.datepicker-wrapper]="isDate" [ngClass]="classList" class="iqser-input-group">
<label *ngIf="label"> {{ label }} </label>
<div [class.datepicker-wrapper]="isDate()" [ngClass]="classList()" class="iqser-input-group">
@if (label()) {
<label> {{ label() }} </label>
}
<ng-container *ngIf="isDate">
@if (isDate()) {
<input
(ngModelChange)="onChange($event)"
[(ngModel)]="input"
[disabled]="disabled"
[id]="id"
[id]="id()"
[matDatepicker]="picker"
[placeholder]="placeholder || 'dd/mm/yy'"
[placeholder]="placeholder() || 'dd/mm/yy'"
iqserStopPropagation
/>
<mat-datepicker-toggle [for]="picker" matSuffix>
<mat-icon matDatepickerToggleIcon svgIcon="iqser:calendar"></mat-icon>
</mat-datepicker-toggle>
<mat-datepicker #picker (closed)="onCloseDatepicker()" (opened)="onOpenDatepicker()"></mat-datepicker>
</ng-container>
}
<input
(ngModelChange)="onChange($event)"
*ngIf="isText"
[(ngModel)]="input"
[disabled]="disabled"
[id]="id"
[placeholder]="placeholder || ''"
iqserStopPropagation
type="text"
/>
@if (isText()) {
<input
(ngModelChange)="onChange($event)"
[(ngModel)]="input"
[disabled]="disabled"
[id]="id()"
[placeholder]="placeholder() || ''"
iqserStopPropagation
type="text"
/>
}
<input
(ngModelChange)="onChange($event)"
*ngIf="isNumber"
[(ngModel)]="input"
[disabled]="disabled"
[id]="id"
iqserStopPropagation
type="number"
/>
@if (isNumber()) {
<input
(ngModelChange)="onChange($event)"
[(ngModel)]="input"
[disabled]="disabled"
[id]="id()"
iqserStopPropagation
type="number"
/>
}
</div>

View File

@ -1,13 +1,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { NgClass } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core';
import { FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FormFieldComponent } from '../form-field/form-field-component.directive';
import { NgClass, NgIf } from '@angular/common';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { StopPropagationDirective } from '../../directives';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { StopPropagationDirective } from '../../directives';
import { FormFieldComponent } from '../form-field/form-field-component.directive';
const InputTypes = {
export const InputTypes = {
DATE: 'DATE',
NUMBER: 'NUMBER',
TEXT: 'TEXT',
@ -23,7 +23,6 @@ type DynamicInput = number | string | Date;
templateUrl: './dynamic-input.component.html',
styleUrls: ['./dynamic-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -36,31 +35,23 @@ type DynamicInput = number | string | Date;
useExisting: DynamicInputComponent,
},
],
imports: [NgClass, NgIf, FormsModule, MatDatepickerModule, StopPropagationDirective, MatIconModule, MatInputModule],
imports: [NgClass, FormsModule, MatDatepickerModule, StopPropagationDirective, MatIconModule, MatInputModule],
})
export class DynamicInputComponent extends FormFieldComponent<DynamicInput> {
@Input() label?: string;
@Input({ required: true }) type!: InputType;
@Input() placeholder?: string;
@Input() id?: string;
@Input() classList = '';
@Input() input!: DynamicInput;
@Output() readonly closedDatepicker = new EventEmitter<boolean>();
readonly label = input<string>();
readonly type = input.required<InputType>();
readonly placeholder = input<string>();
readonly id = input<string>();
readonly classList = input('');
readonly input = model<DynamicInput>();
readonly closedDatepicker = output<boolean>();
get isDate() {
return this.type === InputTypes.DATE;
}
readonly isDate = computed(() => this.type() === InputTypes.DATE);
readonly isNumber = computed(() => this.type() === InputTypes.NUMBER);
readonly isText = computed(() => this.type() === InputTypes.TEXT);
get isNumber() {
return this.type === InputTypes.NUMBER;
}
get isText() {
return this.type === InputTypes.TEXT;
}
writeValue(input: DynamicInput): void {
this.input = input;
override writeValue(input: DynamicInput): void {
this.input.set(input);
}
onCloseDatepicker() {

Some files were not shown because too many files have changed in this diff Show More