Compare commits

..

39 Commits

Author SHA1 Message Date
Valentin Mihai
79149ea45f RED-9417 - updated the display condition for excluded page indicator 2024-11-13 17:32:57 +02:00
Nicoleta Panaghiu
06a5d2a844 RED-9337: display reanalyse button for bulk select. 2024-06-25 10:42:17 +03:00
Dan Percic
db1a618061 add pdftron version to assets 2024-06-20 15:24:28 +03:00
Nicoleta Panaghiu
1d68267288 RED-9337: owner and file assignee can reanalyse error file. 2024-06-19 17:02:34 +03:00
Nicoleta Panaghiu
73f6ad7321 RED-9125: upgrade webviewer. 2024-06-07 16:18:01 +03:00
Nicoleta Panaghiu
1eb1512d67 RED-9175: prevent search re-trigger upon navigating the file pages. 2024-06-03 15:42:25 +03:00
Valentin Mihai
ee0d544d94 RED-8342 - Component Editor not showing all values for a multi-value-component 2024-05-27 13:21:34 +03:00
Dan Percic
843dcde601 filter entries without positions 2024-03-07 15:22:57 +02:00
Nicoleta Panaghiu
8ba4721828 RED-8126: hidden sourceMaps. 2024-01-16 09:27:40 +02:00
Dan Percic
0bb0d2591f RED-8128 add frame ancestors CSP 2024-01-15 20:10:57 +02:00
Valentin Mihai
c70d8d75c0 RED-8072 - Do not allow any annotations or annotation changes while auto-analysis is disabled 2023-12-13 12:41:17 +02:00
Valentin Mihai
23a0c0940d RED-8072 - Do not allow any annotations or annotation changes while auto-analysis is disabled 2023-12-12 15:07:11 +02:00
Dan Percic
14e5c53272 Merge branch 'hotfix/4.488.x-filename' into 'release/4.488.x'
Hotfix: Fix filename for export of redaction log json/xml

See merge request redactmanager/red-ui!221
2023-12-07 17:50:39 +01:00
Kresnadi Budisantoso
9e797e6398 Hotfix: Fix filename for export of redaction log json/xml 2023-12-07 16:24:59 +01:00
Valentin Mihai
551445c522 DM-588 - Do not allow any annotations or annotation changes while auto-analysis is disabled 2023-12-07 13:56:53 +02:00
Nicoleta Panaghiu
53f64d0279 RED-8032: backported new way of managing comments. 2023-12-06 16:48:46 +02:00
Dan Percic
d073980d41 RED-7990 fix rule based annotations cannot be resized 2023-12-04 19:07:28 +02:00
Dan Percic
de31be252b RED-7989 update edit entity form 2023-12-04 18:54:38 +02:00
Dan Percic
f1f3bffcf1 RED-7990 fix rule based annotations cannot be resized 2023-11-30 15:52:13 +02:00
Dan Percic
0ad2966a5c DM-604 fix actions show when file is done 2023-11-30 13:49:47 +02:00
Dan Percic
5d97da46cb DM-604 hide watermark options & skip dossier_redaction dictionary 2023-11-29 14:35:51 +02:00
Valentin Mihai
e8fe6f37de RED-7989 - Case Sensitive field not updated correctly in UI 2023-11-29 11:05:35 +02:00
Adina Țeudan
0d7c5efd7d RED-7692: Add linebreak if comment is too long 2023-11-29 00:30:44 +02:00
Valentin Mihai
6493422688 DM-598 - Component values cannot be changed manually 2023-11-28 14:20:47 +02:00
Nicoleta Panaghiu
44dc6519d3 RED-7935: fixed internal server error. 2023-11-28 14:00:19 +02:00
Valentin Mihai
9c951712ae DM-588 - Do not allow any annotations or annotation changes while auto-analysis is disabled 2023-11-27 16:56:59 +02:00
Valentin Mihai
9bdacfcfdb DM-598 - Component values cannot be changed manually 2023-11-27 12:48:58 +02:00
Nicoleta Panaghiu
643541d20c RED-7953: fixed cannot save force annotation. 2023-11-21 14:20:13 +02:00
Valentin Mihai
e7001e1e2b DM-536 - Extraction output bulk download for dossiers 2023-11-20 21:58:10 +02:00
Adina Țeudan
19c3154130 RED-6435: Removed add to dict character limit 2023-11-14 17:49:07 +02:00
Adina Țeudan
52dfb13a15 RED-6435: Cleanup unecessary code 2023-11-14 17:43:49 +02:00
Adina Țeudan
534f84bdba RED-6435: Removed add to dict character limit 2023-11-14 17:43:40 +02:00
Marius Schummer
a7be372068 DM-556: Update en.json to change workload label for dict-based annotations (backport) 2023-11-13 15:48:43 +01:00
Valentin Mihai
28cdf481ea DM-579 - Help Mode Link for Component Download 2023-11-10 20:30:05 +02:00
Valentin Mihai
64a446ebaf DM-536 - Extraction output bulk download for dossiers 2023-11-10 20:17:00 +02:00
Dan Percic
7afbc38e03 DM-536 addition: Change tooltip for standard download button 2023-11-09 18:04:58 +02:00
Valentin Mihai
a881da891d DM-536 - Extraction output bulk download for dossiers 2023-11-08 19:34:06 +02:00
Adina Țeudan
9e6c466c4b RED-7858: Remove Email Report button in License Information screen 2023-11-07 13:50:48 +02:00
Dan Percic
db294fc909 DM-540 backport to documine 2023-11-07 13:50:20 +02:00
984 changed files with 22448 additions and 25277 deletions

5
.dev/export_summary.csv Normal file
View File

@ -0,0 +1,5 @@
PMRA Document Number,GAP Active Ingredient,Regulatory Utility,Document Comment,SIC Number,Protocol Number,Protocol Number,Data Evaluation Record,Audience,Original Retirement Date,Regulatory Released,Regulatory Authority,Regulatory Authority (legacy to remove),Source Owner,Legacy Primary OECD Code,Legacy Source Organisation,Regulator Comments,Contains Registered Composition?,File Name,FTP Source Location,Submission Format Needed,Legacy Report Number,Sales Unit,Address,ANVISA Process Number,Source Number,Legacy Migration Path,Review Completion Date,Registration Number,Agency Dossier Registration Type,Submission Version,Media Comments,Literature Citation,Document Language,Notes,Study Location,Materials,Approver,Subtype,Test Facility,Is Latest Version,Consumer,Classification,Guideline,TRP Type,Legacy System Date,Function,Artist,Document Title,PI Number,Legacy Version Description,PI Number,Incoming from,Annotations (Unresolved),Work To Date,Copyright Clearance Obtained?,Summary Type,Annotations (Claim Links),Latest Source Minor Version,Regulatory Category,SYN Letter Number,Work From Date,Product,Version Creation Date,Governance Committee,Test Facility (legacy to remove),Regulatory Finalization Date,Imported file?,Status,Information Tracking Submission Package,Approval Type,Media Description,Duration,Legacy System,Archive Date,Exporting country,Regulatory Tracking Number,Annotations (Anchors),Information Tracking,Document Number,GAP Usage Information,Report Number,Legacy Reg Document Number,Legal Representative 2,Registering Company,File Created By,Legacy EPA Decision Code,Meeting Minutes,Product Safety Finalization Date,Report Type,Archived Date,Legacy Other PMRA Data Codes,Template Document Type,Color Space,Legal Representative 1,Document Author,CrossLink,Annotations (Notes),Suggested Links,Assessment Type,Legacy Species Commodity,TK Number,Legacy Test Facilities,Planned Completion Date,PRF Number,Registration Item Country,Legacy Target Species,Legacy Owning Organisation,Archive Number,Protocol Type,Submission Output Format,Submission Date,Coordinator,Regulatory Reporting Date,Annotations (Lines),Field Trial Number,Batch Number,Legacy Version Number,External ID,Author Names,Sponsor Organisation (legacy to remove),Literature Type,Sponsor Organisation,Legacy Crop Host,Document Edition,Agreement Expiry Date,TRP Usage,Legacy EPA Submission Type,Security Classification,Source Document Name,Syngenta Address,Outgoing to (legacy to remove),Link Status,Legacy System Document ID,Copyright,PMRA Registration Number,Annotations (All),SU,Source Vault Name,Latest Source Major Version,Legacy Other OECD Codes,Version Created By,Subarea,Reviewer,Owning Organisation (legacy to remove),Project,Viewer,Regulatory Territory,Lifecycle,Claims,Bound Source Minor Version,Global ID,Legacy Regulatory Authorities,Outgoing to,Additional Information,Year,Organism,File Last Modified By,Requires Attachment of a Signature Page?,Source Binding Rule,Destination country,Cross-Reference Submission Number,Name,Issue Topic,Pages,Type,Reason for Retiring,Rule,Type of Review,Original Finalization Date,Trade Name,Transmittal Document,Pages OCR'ed (%),Legacy Object ID(s),Protocol Number,Last Modified By,Annotations (Approved Links),Request Type,Binder,Author,Last Modified Date,Non-English Document Title,Media Location,Created By,Editor,Owner,CRO Report Number,Format,Legacy Discipline,Major Version Number,Created From,Active Ingredients,Archive Location,EPA Registration Number,Retirement Date,Annotations (Links),TRP Document,Finalization Date,Legacy Task Number,Legal Details,OCR Requested,Global Version ID,Publicly Published,Document Date,MRID Number,Legacy Reg Topic,Requires Review and Approval?,Annotations (Auto Links),Subareas,Product Safety Reporting Date,Media Title,Minor Version Number,Legacy Primary PMRA Data Code,Requestor,Owning Organisation,Zone,File Last Modified Date,Contact Person,File Created Date,Amendment Justification,CDPR Number,Legacy EPA Submission Date,Supports Agency Dossier,Field Trial Year,Reason for Un-retiring,TRP Topic,Bound Source Major Version,Keywords,Legacy Author Name(s),Export File Name,Size,Source Link,This content is a translation?,Quality Assurance Standard,Legacy Fed Reg Number,Checksum,Created Date,Annotations (Resolved),User Task,Review Start Date,test list,Incoming from (legacy to remove),Rendition Profile,Path,URL
,,Country Specific,,,,,No,,,Yes,,,,,Syngenta,,,Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the context in which the dossier is submit.doc,,,,,,,,,,,,,,,English,,,PP321,,Authority Form / Document,,TRUE,"All Internal Users, Syngenta Read Only Restricted, Syngenta Read Only Non Restricted",,,,16/07/2019,,,Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the context in which the dossier is submitted,PI0015818,,,,0,,,,0,,,,,,26/03/2020 14:06 CET,,Syngenta Crop Protection AG (Switzerland),,No,Final,,,,,SmartDoc - EAME,,,,0,IT-624180,VV-731623,,N/A,PP321_12074,,,,,,,,,,,,,B Sochard,No,0,0,,,,,,,,,,,,,,,,0,,,1.0|CURRENT,090100b881afdd91,,Syngenta Crop Protection AG (Switzerland),,Syngenta Crop Protection AG,,Original,,,,Internal Use Only,,,ETL_blank,,090100b881afdd91,,,0,,,,,Berangere Sochard,,,Syngenta Crop Protection AG (Switzerland),,,European Union,General Lifecycle,,,40037_850361,,ETL_blank,,,,,,,,,Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the context in which the dossier is submit,,2,Registration Supporting Documentation,,,,,,No,,090100b881afdd91 (cv),,CP Vault Support,0,,No,,24/02/2021 15:20 CET,,,Vault Migration,"Global Reg Ops, NA Reg Ops, PS Ops, EAME Reg Ops, Syngenta Business Administrator, Berangere Sochard, Simon Baker, Lily Williams, Clive Boxwell, Paul Parsons, Richard MacKenzie, Dan Pickford, Laurence Hand",Vault Migration,,application/msword,Regulatory,2,,PP321,,,,0,No,,PI0015818,,No,40037_850361_1958105,No,01/09/2020,,Regulatory,No,0,,,,0,,,Syngenta Crop Protection AG,,,,,,,,,,,,,,Sochard Berangere,,49152,,No,,,86d75e5da397fdadcd332f92234a4f62,29/11/2019 08:51 CET,0,,,,,,Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the.pdf,
,,Country Specific,,,,,No,,,Yes,,,,,Syngenta,,,Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology (1).docx,,,,,,,,,,,,,,,English,,,PP321,,Summary / Overview,,TRUE,"All Internal Users, Syngenta Read Only Non Restricted, Syngenta Read Only Restricted",,,,16/07/2019,,,Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology,PI0015818,,,,0,,,OECD Tier 2 Summary,0,,,,,,17/02/2021 22:02 CET,,Syngenta Crop Protection AG (Switzerland),,Yes,Draft,,,,,SmartDoc - EAME,,,,0,IT-615930,VV-729845,,N/A,PP321_12086,,,Syngenta,,,,,,,,,,S Lloyd,No,0,0,,,,,,,,,,,,,,,,0,,,1.0|CURRENT,090100b881afde22,,Syngenta Crop Protection AG (Switzerland),,Syngenta Crop Protection AG,,Original,,,,Internal Use Only,,,,,090100b881afde22,,,0,,,,,Sonia Ellis,,,Syngenta Crop Protection AG (Switzerland),,,European Union,General Lifecycle,,,40037_848583,,,,,,Ellis Sonia (ext) GBGU,,,,,Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology,,893,Summary / Assessment,,,,,,No,,090100b881afde22 (cv),,CP Vault Support,0,,No,Syngenta,24/02/2021 15:20 CET,,,Vault Migration,"EAME Reg Ops, Syngenta Business Administrator, PS Ops, NA Reg Ops, Global Reg Ops, Berangere Sochard, Simon Baker, Lily Williams, Clive Boxwell, Paul Parsons, Richard MacKenzie, Dan Pickford, Laurence Hand, Claire McCombie, Elaine Buss",Vault Migration,,application/vnd.openxmlformats-officedocument.wordprocessingml.document,Toxicology,3,,PP321,,,,0,No,,PI0015818,,No,40037_848583_2033983,No,01/09/2020,,Tox,No,0,,,,1,,,Syngenta Crop Protection AG,,17/02/2021 21:35 CET,,04/09/2020 14:34 CEST,,,,,,,,,VV-729845,Lloyd Sara,,5089990,,No,,,3b13f57d1244e77844eba73ce0ae9813,29/11/2019 08:47 CET,0,,,,,,Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology.pdf,
,,Country Specific,,,,,No,,,Yes,,,,,Syngenta,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List (1).doc,,,,,,,,,,,,,,,English,,,PP321,,Summary / Overview,,TRUE,"All Internal Users, Syngenta Read Only Restricted, Syngenta Read Only Non Restricted",,,,16/07/2019,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List,PI0015818,,,,0,,,OECD Tier 1 Summary,0,,,,,,12/09/2020 15:48 CEST,,Syngenta Crop Protection AG (Switzerland),,Yes,Final,,,,,SmartDoc - EAME,,,,0,IT-615436,VV-729844,,N/A,PP321_12084,,,,,,,,,,,,,S Ellis,No,0,0,,,,,,,,,,,,,,,,0,,,1.0|CURRENT,090100b881afde20,,Syngenta Crop Protection AG (Switzerland),,Syngenta Crop Protection AG,,Original,,,,Internal Use Only,,,,,090100b881afde20,,,0,,,,,Sonia Ellis,,,Syngenta Crop Protection AG (Switzerland),,,European Union,General Lifecycle,,,40037_848582,,,,,,,,,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List,,147,Summary / Assessment,,,,,,No,,090100b881afde20 (cv),,CP Vault Support,0,,No,,24/02/2021 15:20 CET,,,Vault Migration,"NA Reg Ops, Global Reg Ops, EAME Reg Ops, PS Ops, Syngenta Business Administrator, Berangere Sochard, Paul Parsons",Vault Migration,,application/msword,Toxicology,2,,PP321,,,,0,No,,PI0015818,,No,40037_848582_2001744,No,01/09/2020,,Tox,No,0,,,,0,,,Syngenta Crop Protection AG,,,,,,,,,,,,,,Ellis Sonia,,651264,,No,,,eb0fdf698b528b67f54635395ec7fb52,29/11/2019 08:47 CET,0,,,,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List.pdf,
,,Country Specific,,,,,No,,,Yes,,,,,Syngenta,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List (5).doc,,,,,,,,,,,,,,,English,,,PP321,,Summary / Overview,,TRUE,"All Internal Users, Syngenta Read Only Restricted, Syngenta Read Only Non Restricted",,,,16/07/2019,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List,PI0015818,,,,0,,,OECD Tier 1 Summary,0,,,,,,21/09/2020 15:21 CEST,,Syngenta Crop Protection AG (Switzerland),,Yes,Final,,,,,SmartDoc - EAME,,,,0,IT-618210,VV-729843,,N/A,PP321_12083,,,,,,,,,,,,,S Ellis,No,0,0,,,,,,,,,,,,,,,,0,,,1.0|CURRENT,090100b881afde1f,,Syngenta Crop Protection AG (Switzerland),,Syngenta Crop Protection AG,,Original,,,,Internal Use Only,,,,,090100b881afde1f,,,0,,,,,Sonia Ellis,,,Syngenta Crop Protection AG (Switzerland),,,European Union,General Lifecycle,,,40037_848581,,,,,,,,,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List,,151,Summary / Assessment,,,,,,No,,090100b881afde1f (cv),,CP Vault Support,0,,No,,24/02/2021 15:20 CET,,,Vault Migration,"EAME Reg Ops, Global Reg Ops, PS Ops, Syngenta Business Administrator, NA Reg Ops, Berangere Sochard, Melanie Bottoms, Dan Pickford, Lynda Farrelly",Vault Migration,,application/msword,Chemistry - Regulatory,2,,PP321,,,,0,No,,PI0015818,,No,40037_848581_2003177,No,01/09/2020,,Chem - Analytical Methods,No,0,,,,0,,,Syngenta Crop Protection AG,,,,,,,,,,,,,,Ellis Sonia,,699392,,No,,,2c476f477f6e2b55ecb537683f5d1cee,29/11/2019 08:47 CET,0,,,,,,Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List.pdf,
1 PMRA Document Number GAP Active Ingredient Regulatory Utility Document Comment SIC Number Protocol Number Protocol Number Data Evaluation Record Audience Original Retirement Date Regulatory Released Regulatory Authority Regulatory Authority (legacy to remove) Source Owner Legacy Primary OECD Code Legacy Source Organisation Regulator Comments Contains Registered Composition? File Name FTP Source Location Submission Format Needed Legacy Report Number Sales Unit Address ANVISA Process Number Source Number Legacy Migration Path Review Completion Date Registration Number Agency Dossier Registration Type Submission Version Media Comments Literature Citation Document Language Notes Study Location Materials Approver Subtype Test Facility Is Latest Version Consumer Classification Guideline TRP Type Legacy System Date Function Artist Document Title PI Number Legacy Version Description PI Number Incoming from Annotations (Unresolved) Work To Date Copyright Clearance Obtained? Summary Type Annotations (Claim Links) Latest Source Minor Version Regulatory Category SYN Letter Number Work From Date Product Version Creation Date Governance Committee Test Facility (legacy to remove) Regulatory Finalization Date Imported file? Status Information Tracking Submission Package Approval Type Media Description Duration Legacy System Archive Date Exporting country Regulatory Tracking Number Annotations (Anchors) Information Tracking Document Number GAP Usage Information Report Number Legacy Reg Document Number Legal Representative 2 Registering Company File Created By Legacy EPA Decision Code Meeting Minutes Product Safety Finalization Date Report Type Archived Date Legacy Other PMRA Data Codes Template Document Type Color Space Legal Representative 1 Document Author CrossLink Annotations (Notes) Suggested Links Assessment Type Legacy Species Commodity TK Number Legacy Test Facilities Planned Completion Date PRF Number Registration Item Country Legacy Target Species Legacy Owning Organisation Archive Number Protocol Type Submission Output Format Submission Date Coordinator Regulatory Reporting Date Annotations (Lines) Field Trial Number Batch Number Legacy Version Number External ID Author Names Sponsor Organisation (legacy to remove) Literature Type Sponsor Organisation Legacy Crop Host Document Edition Agreement Expiry Date TRP Usage Legacy EPA Submission Type Security Classification Source Document Name Syngenta Address Outgoing to (legacy to remove) Link Status Legacy System Document ID Copyright PMRA Registration Number Annotations (All) SU Source Vault Name Latest Source Major Version Legacy Other OECD Codes Version Created By Subarea Reviewer Owning Organisation (legacy to remove) Project Viewer Regulatory Territory Lifecycle Claims Bound Source Minor Version Global ID Legacy Regulatory Authorities Outgoing to Additional Information Year Organism File Last Modified By Requires Attachment of a Signature Page? Source Binding Rule Destination country Cross-Reference Submission Number Name Issue Topic Pages Type Reason for Retiring Rule Type of Review Original Finalization Date Trade Name Transmittal Document Pages OCR'ed (%) Legacy Object ID(s) Protocol Number Last Modified By Annotations (Approved Links) Request Type Binder Author Last Modified Date Non-English Document Title Media Location Created By Editor Owner CRO Report Number Format Legacy Discipline Major Version Number Created From Active Ingredients Archive Location EPA Registration Number Retirement Date Annotations (Links) TRP Document Finalization Date Legacy Task Number Legal Details OCR Requested Global Version ID Publicly Published Document Date MRID Number Legacy Reg Topic Requires Review and Approval? Annotations (Auto Links) Subareas Product Safety Reporting Date Media Title Minor Version Number Legacy Primary PMRA Data Code Requestor Owning Organisation Zone File Last Modified Date Contact Person File Created Date Amendment Justification CDPR Number Legacy EPA Submission Date Supports Agency Dossier Field Trial Year Reason for Un-retiring TRP Topic Bound Source Major Version Keywords Legacy Author Name(s) Export File Name Size Source Link This content is a translation? Quality Assurance Standard Legacy Fed Reg Number Checksum Created Date Annotations (Resolved) User Task Review Start Date test list Incoming from (legacy to remove) Rendition Profile Path URL
2 Country Specific No Yes Syngenta Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the context in which the dossier is submit.doc English PP321 Authority Form / Document TRUE All Internal Users, Syngenta Read Only Restricted, Syngenta Read Only Non Restricted 16/07/2019 Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the context in which the dossier is submitted PI0015818 0 0 26/03/2020 14:06 CET Syngenta Crop Protection AG (Switzerland) No Final SmartDoc - EAME 0 IT-624180 VV-731623 N/A PP321_12074 B Sochard No 0 0 0 1.0|CURRENT 090100b881afdd91 Syngenta Crop Protection AG (Switzerland) Syngenta Crop Protection AG Original Internal Use Only ETL_blank 090100b881afdd91 0 Berangere Sochard Syngenta Crop Protection AG (Switzerland) European Union General Lifecycle 40037_850361 ETL_blank Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the context in which the dossier is submit 2 Registration Supporting Documentation No 090100b881afdd91 (cv) CP Vault Support 0 No 24/02/2021 15:20 CET Vault Migration Global Reg Ops, NA Reg Ops, PS Ops, EAME Reg Ops, Syngenta Business Administrator, Berangere Sochard, Simon Baker, Lily Williams, Clive Boxwell, Paul Parsons, Richard MacKenzie, Dan Pickford, Laurence Hand Vault Migration application/msword Regulatory 2 PP321 0 No PI0015818 No 40037_850361_1958105 No 01/09/2020 Regulatory No 0 0 Syngenta Crop Protection AG Sochard Berangere 49152 No 86d75e5da397fdadcd332f92234a4f62 29/11/2019 08:51 CET 0 Lambda-cyhalothrin - EU AIR5 - Document A - Statement of the.pdf
3 Country Specific No Yes Syngenta Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology (1).docx English PP321 Summary / Overview TRUE All Internal Users, Syngenta Read Only Non Restricted, Syngenta Read Only Restricted 16/07/2019 Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology PI0015818 0 OECD Tier 2 Summary 0 17/02/2021 22:02 CET Syngenta Crop Protection AG (Switzerland) Yes Draft SmartDoc - EAME 0 IT-615930 VV-729845 N/A PP321_12086 Syngenta S Lloyd No 0 0 0 1.0|CURRENT 090100b881afde22 Syngenta Crop Protection AG (Switzerland) Syngenta Crop Protection AG Original Internal Use Only 090100b881afde22 0 Sonia Ellis Syngenta Crop Protection AG (Switzerland) European Union General Lifecycle 40037_848583 Ellis Sonia (ext) GBGU Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology 893 Summary / Assessment No 090100b881afde22 (cv) CP Vault Support 0 No Syngenta 24/02/2021 15:20 CET Vault Migration EAME Reg Ops, Syngenta Business Administrator, PS Ops, NA Reg Ops, Global Reg Ops, Berangere Sochard, Simon Baker, Lily Williams, Clive Boxwell, Paul Parsons, Richard MacKenzie, Dan Pickford, Laurence Hand, Claire McCombie, Elaine Buss Vault Migration application/vnd.openxmlformats-officedocument.wordprocessingml.document Toxicology 3 PP321 0 No PI0015818 No 40037_848583_2033983 No 01/09/2020 Tox No 0 1 Syngenta Crop Protection AG 17/02/2021 21:35 CET 04/09/2020 14:34 CEST VV-729845 Lloyd Sara 5089990 No 3b13f57d1244e77844eba73ce0ae9813 29/11/2019 08:47 CET 0 Lambda-cyhalothrin - EU AIR5 - MCA Section 5 - Toxicology.pdf
4 Country Specific No Yes Syngenta Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List (1).doc English PP321 Summary / Overview TRUE All Internal Users, Syngenta Read Only Restricted, Syngenta Read Only Non Restricted 16/07/2019 Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List PI0015818 0 OECD Tier 1 Summary 0 12/09/2020 15:48 CEST Syngenta Crop Protection AG (Switzerland) Yes Final SmartDoc - EAME 0 IT-615436 VV-729844 N/A PP321_12084 S Ellis No 0 0 0 1.0|CURRENT 090100b881afde20 Syngenta Crop Protection AG (Switzerland) Syngenta Crop Protection AG Original Internal Use Only 090100b881afde20 0 Sonia Ellis Syngenta Crop Protection AG (Switzerland) European Union General Lifecycle 40037_848582 Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List 147 Summary / Assessment No 090100b881afde20 (cv) CP Vault Support 0 No 24/02/2021 15:20 CET Vault Migration NA Reg Ops, Global Reg Ops, EAME Reg Ops, PS Ops, Syngenta Business Administrator, Berangere Sochard, Paul Parsons Vault Migration application/msword Toxicology 2 PP321 0 No PI0015818 No 40037_848582_2001744 No 01/09/2020 Tox No 0 0 Syngenta Crop Protection AG Ellis Sonia 651264 No eb0fdf698b528b67f54635395ec7fb52 29/11/2019 08:47 CET 0 Lambda-cyhalothrin - EU AIR5 - LCA Section 5 Reference List.pdf
5 Country Specific No Yes Syngenta Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List (5).doc English PP321 Summary / Overview TRUE All Internal Users, Syngenta Read Only Restricted, Syngenta Read Only Non Restricted 16/07/2019 Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List PI0015818 0 OECD Tier 1 Summary 0 21/09/2020 15:21 CEST Syngenta Crop Protection AG (Switzerland) Yes Final SmartDoc - EAME 0 IT-618210 VV-729843 N/A PP321_12083 S Ellis No 0 0 0 1.0|CURRENT 090100b881afde1f Syngenta Crop Protection AG (Switzerland) Syngenta Crop Protection AG Original Internal Use Only 090100b881afde1f 0 Sonia Ellis Syngenta Crop Protection AG (Switzerland) European Union General Lifecycle 40037_848581 Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List 151 Summary / Assessment No 090100b881afde1f (cv) CP Vault Support 0 No 24/02/2021 15:20 CET Vault Migration EAME Reg Ops, Global Reg Ops, PS Ops, Syngenta Business Administrator, NA Reg Ops, Berangere Sochard, Melanie Bottoms, Dan Pickford, Lynda Farrelly Vault Migration application/msword Chemistry - Regulatory 2 PP321 0 No PI0015818 No 40037_848581_2003177 No 01/09/2020 Chem - Analytical Methods No 0 0 Syngenta Crop Protection AG Ellis Sonia 699392 No 2c476f477f6e2b55ecb537683f5d1cee 29/11/2019 08:47 CET 0 Lambda-cyhalothrin - EU AIR5 - LCA Section 4 Reference List.pdf

View File

@ -1,14 +1,3 @@
node_modules/
.idea/
dist/
.angular
.husky
.editorconfig
.dockerignore
.eslintignore
.eslintrc.json
.gitignore
.prettierignore
.prettierrc
renovate.json
Running

View File

@ -5,6 +5,3 @@ dist
coverage
node_modules
bamboo-specs
docker
paligo-styles
paligo-theme

View File

@ -1,10 +1,46 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [
"@services/**",
"@components/**",
"@guards/**",
"@users/**",
"@i18n/**",
"@utils/**",
"@models/**",
"@environments/**",
"@shared/**",
"@upload-download/**",
"@translations/**"
],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts"],
"extends": ["plugin:@nx/typescript"]
},
{
"files": ["*.ts"],
"extends": [
"plugin:@nx/angular",
"eslint:recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
@ -16,7 +52,6 @@
},
"rules": {
"rxjs/no-ignored-subscription": "warn",
"@angular-eslint/prefer-standalone": "off",
"@angular-eslint/no-conflicting-lifecycle": "error",
"@angular-eslint/no-host-metadata-property": "error",
"@angular-eslint/no-input-rename": "error",
@ -38,7 +73,6 @@
"@typescript-eslint/indent": "off",
"@typescript-eslint/lines-between-class-members": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
@ -249,7 +283,7 @@
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"]
"extends": ["plugin:@nx/angular-template", "plugin:@angular-eslint/template/recommended"]
},
{
"files": ["*.html"],

6
.gitignore vendored
View File

@ -18,7 +18,6 @@
*.launch
.settings/
*.sublime-workspace
redaction.iml
# IDE - VSCode
.vscode/*
@ -47,4 +46,7 @@ paligo-styles/style.css*
migrations.json
*.iml
/.nx/
docker-compose.yml
.nx/cache/

View File

@ -4,32 +4,14 @@ variables:
PROJECT: red-ui
DOCKERFILELOCATION: 'docker/$PROJECT/Dockerfile'
workflow:
rules:
- when: always
include:
- project: 'gitlab/gitlab'
ref: 'main'
file: 'ci-templates/docker_build_nexus_v2.yml'
rules:
- if: $CI_PIPELINE_SOURCE != "schedule"
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/"
localazy update:
image: node:20.5
@ -40,24 +22,21 @@ localazy update:
paths:
- .yarn-cache/
script:
# - git config user.email "${CI_EMAIL}"
# - git config user.name "${CI_USERNAME}"
# - git remote add gitlab_origin https://${CI_USERNAME}:${CI_ACCESS_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- git config user.email "${CI_EMAIL}"
- git config user.name "${CI_USERNAME}"
- git remote add gitlab_origin https://${CI_USERNAME}:${CI_ACCESS_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
- cd tools/localazy
- yarn install --cache-folder .yarn-cache
- yarn start
- cd ../..
- git add .
- |-
CHANGES=$(git status --porcelain | wc -l)
if [ "$CHANGES" -gt "0" ]
then
git status
git commit -m "push back localazy update"
git push gitlab_origin HEAD:${CI_COMMIT_REF_NAME}
# git push https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.knecon.com/redactmanager/red-ui.git
# git push
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
CHANGES=$(git status --porcelain | wc -l)
if [ "$CHANGES" -gt "0" ]
then
git status
git commit -m "push back localazy update"
git push gitlab_origin HEAD:${CI_COMMIT_REF_NAME} -o ci.skip
fi
only:
- scheduled

View File

@ -2,12 +2,13 @@
### To Create a new Stack in rancher check [this Wiki page](https://wiki.iqser.com/pages/viewpage.action?spaceKey=RED&title=Work+with+kubectl)
# Dependencies update guide
* When updating @pdftron/webviewer, make sure to change the version also in the angular.json and everywhere where the path to /assets/wv-recources is used
* Make sure the keycloak.js version is the same with the keycloak version from helm chart
## Code style
* Don't use `setInterval` without calling `clearInterval` in `ngOnDestroy` or in `destroyRef.onDestroy(() => clearInterval(intervalId))`
* Always use `trackBy` in `*ngFor` loops (see shorthand below)
```typescript
readonly trackBy = trackByFactory();
```
* Don't use `setInterval` without calling `clearInterval` in `ngOnDestroy`
* Never call getters in HTML templates
## Keycloak Staging Config

View File

@ -1,133 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "yarn",
"schematicCollections": ["@angular-eslint/schematics"],
"analytics": "2bccdff1-3aff-4f10-b233-211065aa25d9"
},
"newProjectRoot": "projects",
"projects": {
"red-ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "apps/red-ui/src",
"prefix": "redaction",
"targets": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": {
"base": "dist/apps/red-ui"
},
"index": "apps/red-ui/src/index.html",
"polyfills": ["apps/red-ui/src/polyfills.ts"],
"tsConfig": "tsconfig.json",
"baseHref": "/ui/",
"assets": [
"apps/red-ui/src/favicon.ico",
{
"glob": "**/*",
"input": "node_modules/@pdftron/webviewer/public/",
"output": "/assets/wv-resources/11.1.0/"
},
{
"glob": "**/*",
"input": "apps/red-ui/src/assets/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "libs/common-ui/src/assets/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "node_modules/monaco-editor",
"output": "/assets/monaco-editor/"
},
"apps/red-ui/src/manifest.webmanifest"
],
"styles": ["apps/red-ui/src/styles.scss", "libs/common-ui/src/assets/styles/common-styles.scss"],
"stylePreprocessorOptions": {
"includePaths": ["./apps/red-ui/src/assets/styles", "./libs/common-ui/src/assets/styles"]
},
"scripts": ["node_modules/chart.js/auto/auto.cjs"],
"extractLicenses": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true,
"browser": "apps/red-ui/src/main.ts"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/red-ui/src/environments/environment.ts",
"with": "apps/red-ui/src/environments/environment.prod.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "20kb"
}
],
"serviceWorker": "ngsw-config.json"
}
}
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"buildTarget": "red-ui:build"
},
"configurations": {
"production": {
"buildTarget": "red-ui:build:production"
}
}
}
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"extends": ["plugin:@typescript-eslint/recommended"],
"rules": {
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-floating-promises": "off"
}
}
]
}

View File

@ -0,0 +1,26 @@
import type { Config } from 'jest';
import { defaults } from 'jest-config';
export default {
...defaults,
displayName: 'red-ui',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['jest-preset-angular/setup-jest.js', 'jest-extended/all'],
coverageDirectory: '../../coverage/apps/angular-jest',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '../../tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
testEnvironment: 'jest-environment-jsdom',
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
} as Config;

View File

@ -1,5 +1,5 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"$schema": "../../node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
@ -17,15 +17,5 @@
"files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"]
}
}
],
"navigationUrls": [
"/**",
"!/**/*.*",
"!/**/*__*",
"!/**/*__*/**",
"!/ui/assets/wv-resources/**/webviewer-core.min.js",
"!/ui/assets/wv-resources/**/webviewer-ui.min.js",
"!/assets/wv-resources/**/webviewer-core.min.js",
"!/assets/wv-resources/**/webviewer-ui.min.js"
]
}

132
apps/red-ui/project.json Normal file
View File

@ -0,0 +1,132 @@
{
"name": "red-ui",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"generators": {
"@schematics/angular:component": {
"style": "scss",
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"sourceRoot": "apps/red-ui/src",
"prefix": "redaction",
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/apps/red-ui",
"index": "apps/red-ui/src/index.html",
"main": "apps/red-ui/src/main.ts",
"polyfills": "apps/red-ui/src/polyfills.ts",
"tsConfig": "apps/red-ui/tsconfig.json",
"baseHref": "/ui/",
"assets": [
"apps/red-ui/src/favicon.ico",
{
"glob": "**/*",
"input": "node_modules/@pdftron/webviewer/public/",
"output": "/assets/wv-resources/10.9.0"
},
{
"glob": "**/*",
"input": "apps/red-ui/src/assets/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "libs/common-ui/src/assets/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "node_modules/monaco-editor",
"output": "/assets/monaco-editor/"
},
"apps/red-ui/src/manifest.webmanifest"
],
"styles": ["apps/red-ui/src/styles.scss", "libs/common-ui/src/assets/styles/common-styles.scss"],
"stylePreprocessorOptions": {
"includePaths": ["./apps/red-ui/src/assets/styles", "./libs/common-ui/src/assets/styles"]
},
"scripts": ["node_modules/@pdftron/webviewer/webviewer.min.js", "node_modules/chart.js/dist/chart.js"],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": false,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/red-ui/src/environments/environment.ts",
"with": "apps/red-ui/src/environments/environment.prod.ts"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "20kb"
}
],
"serviceWorker": true,
"ngswConfigPath": "apps/red-ui/ngsw-config.json"
}
},
"outputs": ["{options.outputPath}"]
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "red-ui:build"
},
"configurations": {
"production": {
"browserTarget": "red-ui:build:production"
}
}
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "red-ui:build"
}
}
}
}

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { RouteReuseStrategy, RouterModule } from '@angular/router';
import { ifNotLoggedIn } from '@common-ui/tenants/guards/if-not-logged-in.guard';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
import { DownloadsListScreenComponent } from '@components/downloads-list-screen/downloads-list-screen.component';
import { DashboardGuard } from '@guards/dashboard-guard.service';
@ -8,21 +8,21 @@ import { DossierFilesGuard } from '@guards/dossier-files-guard';
import { templateExistsWhenEnteringDossierList } from '@guards/dossier-template-exists.guard';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { loadActiveDossiersGuard, loadAllDossiersGuard, loadArchivedDossiersGuard } from '@guards/dossiers.guard';
import { isNotEditingFileAttributeGuard } from '@guards/file-attribute.guard';
import { editAttributeGuard } from '@guards/edit-attribute.guard';
import { FeaturesGuard } from '@guards/features-guard.service';
import { ifLoggedIn } from '@guards/if-logged-in.guard';
import { ifNotLoggedIn } from '@guards/if-not-logged-in.guard';
import { TrashGuard } from '@guards/trash.guard';
import { CompositeRouteGuard, DEFAULT_REDIRECT_KEY, IqserPermissionsGuard, IqserRoutes, orderedAsyncGuards } from '@iqser/common-ui';
import { TenantSelectComponent } from '@iqser/common-ui/lib/tenants';
import { doesNotHaveAnyRole, hasAnyRole, IqserAuthGuard } from '@iqser/common-ui/lib/users';
import { CustomRouteReuseStrategy } from '@iqser/common-ui/lib/utils';
import { ARCHIVE_ROUTE, BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIERS_ROUTE, FILE_ID } from '@red/domain';
import { ARCHIVE_ROUTE, BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE, FILE_ID } from '@red/domain';
import { RedRoleGuard } from '@users/red-role.guard';
import { Roles } from '@users/roles';
import { mainGuard } from '@utils/main.guard';
import { webViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard';
import { ACTIVE_DOSSIERS_SERVICE } from './tokens';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
const dossierTemplateIdRoutes: IqserRoutes = [
{
@ -38,7 +38,7 @@ const dossierTemplateIdRoutes: IqserRoutes = [
{
path: `:${DOSSIER_ID}`,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [isNotEditingFileAttributeGuard],
canDeactivate: [editAttributeGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier],
@ -49,7 +49,7 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
skeleton: 'dossier',
},
loadChildren: () => import('./modules/dossier-overview/dossier-overview.routes'),
loadChildren: () => import('./modules/dossier-overview/dossier-overview.module').then(m => m.DossierOverviewModule),
},
{
path: `:${DOSSIER_ID}/file/:${FILE_ID}`,
@ -63,12 +63,12 @@ const dossierTemplateIdRoutes: IqserRoutes = [
redirectTo: '/auth-error',
},
},
loadChildren: () => import('./modules/file-preview/file-preview.routes'),
loadChildren: () => import('./modules/file-preview/file-preview.module').then(m => m.FilePreviewModule),
},
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./modules/dossiers-listing/dossiers-listing.routes'),
loadChildren: () => import('./modules/dossiers-listing/dossiers-listing.module').then(m => m.DossiersListingModule),
data: {
breadcrumbs: [BreadcrumbTypes.dossierTemplate],
},
@ -77,10 +77,11 @@ const dossierTemplateIdRoutes: IqserRoutes = [
},
{
path: `${ARCHIVE_ROUTE}`,
loadChildren: () => import('./modules/archive/archive.routes'),
canActivate: [CompositeRouteGuard, loadArchivedDossiersGuard()],
loadChildren: () => import('./modules/archive/archive.module').then(m => m.ArchiveModule),
canActivate: [CompositeRouteGuard, webViewerLoadedGuard(), loadArchivedDossiersGuard()],
data: {
routeGuards: [FeaturesGuard],
features: [DOSSIERS_ARCHIVE],
},
},
{
@ -98,16 +99,16 @@ const mainRoutes: IqserRoutes = [
},
{
path: 'account',
loadChildren: () => import('./modules/account/account.routes'),
loadChildren: () => import('./modules/account/account.module').then(m => m.AccountModule),
},
{
path: 'admin',
loadChildren: () => import('./modules/admin/admin.routes'),
loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule),
canActivate: [RedRoleGuard],
},
{
path: 'dashboard',
loadChildren: () => import('./modules/dashboard/dashboard.routes'),
loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, IqserPermissionsGuard, DossierTemplatesGuard, DashboardGuard],
@ -146,7 +147,7 @@ const mainRoutes: IqserRoutes = [
},
{
path: 'search',
loadComponent: () => import('./modules/search/search-screen/search-screen.component'),
loadChildren: () => import('./modules/search/search.module').then(m => m.SearchModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard, loadAllDossiersGuard()],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
@ -158,7 +159,7 @@ const mainRoutes: IqserRoutes = [
},
{
path: 'trash',
loadChildren: () => import('./modules/trash/trash.routes'),
loadChildren: () => import('./modules/trash/trash.module').then(m => m.TrashModule),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, TrashGuard],
@ -204,13 +205,18 @@ const routes: IqserRoutes = [
component: TenantSelectComponent,
},
{
path: 'main',
path: ':tenant',
redirectTo: ':tenant/main',
pathMatch: 'full',
},
{
path: ':tenant/main',
canActivate: [orderedAsyncGuards([ifLoggedIn(), hasAnyRole(), mainGuard()])],
component: BaseScreenComponent,
children: mainRoutes,
},
{
path: 'auth-error',
path: ':tenant/auth-error',
component: AuthErrorComponent,
canActivate: [doesNotHaveAnyRole()],
},
@ -222,13 +228,7 @@ const routes: IqserRoutes = [
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'enabled',
bindToComponentInputs: true,
paramsInheritanceStrategy: 'always',
}),
],
imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'enabled', bindToComponentInputs: true })],
providers: [{ provide: RouteReuseStrategy, useExisting: CustomRouteReuseStrategy }],
exports: [RouterModule],
})

View File

@ -4,27 +4,26 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { getConfig } from '@iqser/common-ui';
import { AppConfig } from '@red/domain';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { APP_TYPE_PATHS } from '@common-ui/utils/constants';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import { TenantsService } from '@common-ui/tenants';
import { filter, map, switchMap, take } from 'rxjs/operators';
export function loadCustomTheme(cssFileName: string) {
const head = document.getElementsByTagName('head')[0];
const link = document.createElement('link');
link.id = cssFileName;
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'assets/styles/themes/' + cssFileName + '.css';
link.media = 'all';
head.appendChild(link);
function loadCustomTheme() {
const cssFileName = getConfig<AppConfig>().THEME;
if (cssFileName) {
const head = document.getElementsByTagName('head')[0];
const link = document.createElement('link');
link.id = cssFileName;
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = 'assets/styles/themes/' + cssFileName + '.css';
link.media = 'all';
head.appendChild(link);
}
}
@Component({
selector: 'redaction-root',
templateUrl: './app.component.html',
standalone: false,
})
export class AppComponent {
constructor(
@ -35,12 +34,9 @@ export class AppComponent {
userPreferenceService: UserPreferenceService,
renderer: Renderer2,
private readonly _router: Router,
private readonly _iconRegistry: MatIconRegistry,
private readonly _sanitizer: DomSanitizer,
private readonly _tenantsService: TenantsService,
) {
const config = getConfig<AppConfig>();
renderer.addClass(document.body, userPreferenceService.getTheme());
loadCustomTheme();
const removeQueryParams = _router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
@ -51,25 +47,9 @@ export class AppComponent {
);
removeQueryParams.subscribe();
this._tenantsService
.waitForSettingTenant()
.pipe(
tap(() => {
const isDocumine = this._tenantsService.activeTenant.documine;
const logo = isDocumine ? 'documine' : 'redaction';
_iconRegistry.addSvgIconInNamespace(
'iqser',
'logo',
_sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${logo}-logo.svg`),
);
if (isDocumine) {
document.getElementById('favicon').setAttribute('href', 'assets/icons/documine-logo.ico');
}
loadCustomTheme(isDocumine ? APP_TYPE_PATHS.SCM : APP_TYPE_PATHS.REDACT);
}),
take(1),
)
.subscribe();
if (getConfig().IS_DOCUMINE) {
document.getElementById('favicon').setAttribute('href', 'assets/icons/documine-logo.ico');
}
}
#removeKeycloakQueryParams() {

View File

@ -1,20 +1,11 @@
import { APP_BASE_HREF, DatePipe as BaseDatePipe } from '@angular/common';
import { DatePipe as BaseDatePipe } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorHandler, inject, NgModule, provideEnvironmentInitializer } from '@angular/core';
import { ENVIRONMENT_INITIALIZER, ErrorHandler, inject, NgModule } from '@angular/core';
import { MatDividerModule } from '@angular/material/divider';
import { MatIcon } from '@angular/material/icon';
import { MatMenu, MatMenuContent, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltip } from '@angular/material/tooltip';
import { MAT_TOOLTIP_DEFAULT_OPTIONS } from '@angular/material/tooltip';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
import { ChevronButtonComponent } from '@common-ui/buttons/chevron-button';
import { EmptyStateComponent } from '@common-ui/empty-state';
import { HelpModeKey } from '@common-ui/help-mode/types';
import { InputWithActionComponent } from '@common-ui/inputs/input-with-action/input-with-action.component';
import { RoundCheckboxComponent } from '@common-ui/inputs/round-checkbox/round-checkbox.component';
import { GET_TENANT_FROM_PATH_FN, UI_ROOT } from '@common-ui/utils';
import { AuthErrorComponent } from '@components/auth-error/auth-error.component';
import { BaseScreenComponent } from '@components/base-screen/base-screen.component';
import { BreadcrumbsComponent } from '@components/breadcrumbs/breadcrumbs.component';
@ -30,49 +21,51 @@ import { UserMenuComponent } from '@components/user-menu/user-menu.component';
import { environment } from '@environments/environment';
import {
CachingModule,
ChevronButtonComponent,
CircleButtonComponent,
HelpButtonComponent,
HelpModeComponent,
EmptyStateComponent,
HelpModeKey,
HiddenActionDirective,
IconButtonComponent,
InputWithActionComponent,
IqserAllowDirective,
IqserDenyDirective,
IqserHelpModeModule,
IqserListingModule,
IqserLoadingModule,
IqserTranslateModule,
LanguageService,
MAX_RETRIES_ON_SERVER_ERROR,
RoundCheckboxComponent,
SERVER_ERROR_SKIP_PATHS,
ServerErrorInterceptor,
StopPropagationDirective,
} from '@iqser/common-ui';
import { CommonUiModule } from '@iqser/common-ui/lib/common-ui.module';
import { provideHelpMode } from '@iqser/common-ui/lib/help-mode/utils/help-mode.provider';
import { LogoComponent, SkeletonComponent, ToastComponent } from '@iqser/common-ui/lib/shared';
import { TenantsModule } from '@iqser/common-ui/lib/tenants';
import { InitialsAvatarComponent, IqserUsersModule } from '@iqser/common-ui/lib/users';
import { TenantPipe, TenantsModule } from '@iqser/common-ui/lib/tenants';
import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { TranslateModule } from '@ngx-translate/core';
import { MissingTranslationHandler } from '@ngx-translate/core';
import { AppConfig, ILoggerConfig } from '@red/domain';
import { ConfigService } from '@services/config.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service';
import { GlobalErrorHandler } from '@services/global-error-handler.service';
import { LoggerRulesService } from '@services/logger-rules.service';
import { provideCustomDateFormatter } from '@shared/custom-date-formatting.provider';
import { NavigateLastDossiersScreenDirective } from '@shared/directives/navigate-last-dossiers-screen.directive';
import { DatePipe } from '@shared/pipes/date.pipe';
import { SharedModule } from '@shared/shared.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { RedRoleGuard } from '@users/red-role.guard';
import { UserPreferenceService } from '@users/user-preference.service';
import { UserService } from '@users/user.service';
import { UI_CACHES } from '@utils/constants';
import { ColorPickerService } from 'ngx-color-picker';
import { LoggerModule, NGXLogger, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_SERVICE } from 'ngx-logger';
import { REDMissingTranslationHandler } from '@utils/missing-translations-handler';
import { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_SERVICE } from 'ngx-logger';
import { ToastrModule } from 'ngx-toastr';
import helpModeKeys from '../assets/help-mode/help-mode-keys.json';
import * as helpModeKeys from '../assets/help-mode/help-mode-keys.json';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { IconsModule } from './modules/icons/icons.module';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE } from './tokens';
@ -96,6 +89,8 @@ export const appModuleFactory = (config: AppConfig) => {
imports: [
BrowserModule,
BrowserAnimationsModule,
SharedModule,
FileUploadDownloadModule,
AppRoutingModule,
MonacoEditorModule,
CommonUiModule.forRoot({
@ -108,6 +103,7 @@ export const appModuleFactory = (config: AppConfig) => {
existingRoleGuard: RedRoleGuard,
}),
CachingModule.forRoot(UI_CACHES),
IqserHelpModeModule.forRoot(helpModeKeys as HelpModeKey[]),
PdfViewerModule,
ToastrModule.forRoot({
closeButton: true,
@ -117,7 +113,7 @@ export const appModuleFactory = (config: AppConfig) => {
resetTimeoutOnDuplicate: true,
}),
TenantsModule.forRoot(),
IqserTranslateModule.forRoot({ pathPrefix: config.BASE_TRANSLATIONS_DIRECTORY }),
IqserTranslateModule.forRoot({ pathPrefix: config.BASE_TRANSLATIONS_DIRECTORY || '/assets/i18n/redact/' }),
IqserLoadingModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
LoggerModule.forRoot(undefined, {
@ -135,7 +131,7 @@ export const appModuleFactory = (config: AppConfig) => {
features: {
ANNOTATIONS: {
color: 'aqua',
enabled: false,
enabled: true,
level: NgxLoggerLevel.DEBUG,
},
FILTERS: {
@ -145,10 +141,10 @@ export const appModuleFactory = (config: AppConfig) => {
enabled: false,
},
ROUTES: {
enabled: false,
enabled: true,
},
PDF: {
enabled: false,
enabled: true,
},
FILE: {
enabled: false,
@ -160,7 +156,7 @@ export const appModuleFactory = (config: AppConfig) => {
enabled: false,
},
REDACTION_LOG: {
enabled: true,
enabled: false,
},
VIEWED_PAGES: {
enabled: false,
@ -171,9 +167,6 @@ export const appModuleFactory = (config: AppConfig) => {
DOSSIERS_CHANGES: {
enabled: false,
},
GUARDS: {
enabled: false,
},
},
} as ILoggerConfig,
},
@ -190,40 +183,11 @@ export const appModuleFactory = (config: AppConfig) => {
IqserDenyDirective,
IqserListingModule,
IconButtonComponent,
TenantPipe,
MatDividerModule,
ChevronButtonComponent,
InitialsAvatarComponent,
HelpModeComponent,
HelpButtonComponent,
MatMenuTrigger,
MatMenuItem,
MatIcon,
MatMenu,
MatMenuContent,
MatTooltip,
MatProgressSpinner,
IconsModule,
NavigateLastDossiersScreenDirective,
DatePipe,
TranslateModule,
],
providers: [
{
provide: UI_ROOT,
useValue: '/ui',
},
{
provide: APP_BASE_HREF,
useFactory: () => {
const uiRoot = inject(UI_ROOT);
const tenant = inject(GET_TENANT_FROM_PATH_FN)();
console.log(tenant);
const appBaseHref = uiRoot + '/' + tenant;
inject(NGXLogger).info('Provide APP_BASE_HREF:', appBaseHref);
return appBaseHref;
},
},
{
provide: HTTP_INTERCEPTORS,
multi: true,
@ -233,10 +197,18 @@ export const appModuleFactory = (config: AppConfig) => {
provide: ErrorHandler,
useClass: GlobalErrorHandler,
},
provideEnvironmentInitializer(async () => {
const languageService = inject(LanguageService);
return languageService.setInitialLanguage();
}),
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: async () => {
const languageService = inject(LanguageService);
return languageService.setInitialLanguage();
},
},
{
provide: MissingTranslationHandler,
useClass: REDMissingTranslationHandler,
},
{
provide: MAX_RETRIES_ON_SERVER_ERROR,
useFactory: () => config.MAX_RETRIES_ON_SERVER_ERROR,
@ -257,14 +229,10 @@ export const appModuleFactory = (config: AppConfig) => {
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {
disableTooltipInteractivity: true,
showDelay: 1,
},
},
BaseDatePipe,
DatePipe,
...provideCustomDateFormatter(),
...provideHelpMode(helpModeKeys as HelpModeKey[]),
ColorPickerService,
],
bootstrap: [AppComponent],
})

View File

@ -2,17 +2,17 @@
<p *ngIf="!adminName && !adminUrl" class="heading-xl" translate="auth-error.heading"></p>
<p
*ngIf="adminName && adminUrl"
[innerHTML]="'auth-error.heading-with-name-and-link' | translate: { adminName: adminName, adminUrl: adminUrl }"
[innerHTML]="'auth-error.heading-with-name-and-link' | translate : { adminName: adminName, adminUrl: adminUrl }"
class="heading-xl"
></p>
<p
*ngIf="adminName && !adminUrl"
[innerHTML]="'auth-error.heading-with-name' | translate: { adminName: adminName }"
[innerHTML]="'auth-error.heading-with-name' | translate : { adminName: adminName }"
class="heading-xl"
></p>
<p
*ngIf="!adminName && adminUrl"
[innerHTML]="'auth-error.heading-with-link' | translate: { adminName: adminName }"
[innerHTML]="'auth-error.heading-with-link' | translate : { adminName: adminName }"
class="heading-xl"
></p>
<a (click)="userService.logout()" translate="auth-error.logout"></a>

View File

@ -7,7 +7,6 @@ import { AppConfig } from '@red/domain';
selector: 'redaction-auth-error',
templateUrl: './auth-error.component.html',
styleUrls: ['./auth-error.component.scss'],
standalone: false,
})
export class AuthErrorComponent {
readonly #config = getConfig<AppConfig>();

View File

@ -9,7 +9,7 @@
<redaction-breadcrumbs></redaction-breadcrumbs>
</div>
<a [matTooltip]="'top-bar.navigation-items.back-to-dashboard' | translate" [routerLink]="['/main']" class="logo">
<a [matTooltip]="'top-bar.navigation-items.back-to-dashboard' | translate" [routerLink]="['/'] | tenant" class="logo">
<div [attr.help-mode-key]="'home'" class="actions">
<iqser-logo (iqserHiddenAction)="userPreferenceService.toggleDevFeatures()" icon="iqser:logo"></iqser-logo>
<div class="app-name">{{ titleService.getTitle() }}</div>
@ -25,7 +25,7 @@
[placeholder]="'search.placeholder' | translate"
></redaction-spotlight-search>
<iqser-help-button [dialogButton]="false"></iqser-help-button>
<iqser-help-button [attr.help-mode-key]="'help_mode'" id="help-mode-button"></iqser-help-button>
<redaction-notifications
*ngIf="currentUser.isUser || currentUser.isManager"

View File

@ -8,11 +8,13 @@ import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-se
import { filter, map, startWith } from 'rxjs/operators';
import { getConfig, IqserPermissionsService } from '@iqser/common-ui';
import { BreadcrumbsService } from '@services/breadcrumbs.service';
import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '@red/domain';
import { FeaturesService } from '@services/features.service';
import { ARCHIVE_ROUTE, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@red/domain';
import { Roles } from '@users/roles';
import { REDDocumentViewer } from '../../modules/pdf-viewer/services/document-viewer.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { List, shareDistinctLast } from '@iqser/common-ui/lib/utils';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
const isNavigationStart = (event: unknown): event is NavigationStart => event instanceof NavigationStart;
const isSearchScreen: (url: string) => boolean = url => url.includes('/search');
@ -20,9 +22,14 @@ const isSearchScreen: (url: string) => boolean = url => url.includes('/search');
@Component({
templateUrl: './base-screen.component.html',
styleUrls: ['./base-screen.component.scss'],
standalone: false,
})
export class BaseScreenComponent {
readonly #navigationStart$ = this._router.events.pipe(
filter(isNavigationStart),
map(event => event.url),
startWith(this._router.url),
shareDistinctLast(),
);
readonly roles = Roles;
readonly documentViewer = inject(REDDocumentViewer);
readonly currentUser = this.userService.currentUser;
@ -36,6 +43,7 @@ export class BaseScreenComponent {
{
text: this._translateService.instant('search.active-dossiers'),
icon: 'red:enter',
hide: () => !this._featuresService.isEnabled(DOSSIERS_ARCHIVE),
action: (query): void => this.#search(query, [], true),
},
{
@ -44,19 +52,15 @@ export class BaseScreenComponent {
action: (query): void => this.#search(query, []),
},
];
readonly config = getConfig();
readonly #navigationStart$ = this._router.events.pipe(
filter(isNavigationStart),
map(event => event.url),
startWith(this._router.url),
shareDistinctLast(),
);
readonly isSearchScreen$ = this.#navigationStart$.pipe(map(isSearchScreen));
readonly config = getConfig();
constructor(
private readonly _router: Router,
activatedRoute: ActivatedRoute,
private readonly _translateService: TranslateService,
private readonly _featuresService: FeaturesService,
protected readonly _tenantsService: TenantsService,
readonly permissionsService: IqserPermissionsService,
readonly userService: UserService,
readonly userPreferenceService: UserPreferenceService,
@ -90,7 +94,7 @@ export class BaseScreenComponent {
#search(query: string, dossierIds: string[], onlyActive = false) {
const queryParams = { query, dossierIds: dossierIds.join(','), onlyActive };
this._router.navigate(['/main/search'], { queryParams }).then();
this._router.navigate([`/${this._tenantsService.activeTenantId}/main/search`], { queryParams }).then();
}
#searchThisDossier(query: string) {

View File

@ -14,7 +14,7 @@
[id]="first ? 'navigateToActiveDossiers' : ''"
[matTooltip]="breadcrumb.options.clamp && (breadcrumb.name$ | async)"
[routerLinkActiveOptions]="breadcrumb.options.routerLinkActiveOptions || { exact: false }"
[routerLink]="breadcrumb.options.routerLink"
[routerLink]="breadcrumb.options.routerLink | tenant"
class="breadcrumb"
routerLinkActive="active"
>
@ -32,7 +32,7 @@
<div id="breadcrumbs-menu-items">
<a
*ngFor="let option of breadcrumb.options.options"
[routerLink]="option.options.routerLink"
[routerLink]="option.options.routerLink | tenant"
mat-menu-item
routerLinkActive="active"
>

View File

@ -7,7 +7,6 @@ import { Breadcrumb, BreadcrumbDisplayType, BreadcrumbsService } from '@services
selector: 'redaction-breadcrumbs',
templateUrl: './breadcrumbs.component.html',
styleUrls: ['./breadcrumbs.component.scss'],
standalone: false,
})
export class BreadcrumbsComponent {
constructor(readonly breadcrumbsService: BreadcrumbsService) {}

View File

@ -13,7 +13,6 @@ import { firstValueFrom } from 'rxjs';
entitiesService: FileDownloadService,
component: DownloadsListScreenComponent,
}),
standalone: false,
})
export class DownloadsListScreenComponent extends ListingComponent<DownloadStatus> implements OnDestroy {
readonly #interval: number;

View File

@ -32,18 +32,19 @@
</div>
<div
(click)="handleMarkReadEvent($event, [notification], true)"
(click)="markRead([notification], true)"
*ngFor="let notification of group.notifications; trackBy: trackBy"
[class.unread]="!notification.readDate"
[id]="'notifications-mark-as-read-' + notification.id + '-btn'"
class="notification"
iqserStopPropagation
mat-menu-item
>
<iqser-initials-avatar [user]="notification.userId"></iqser-initials-avatar>
<div class="notification-content">
<div [innerHTML]="notification.message"></div>
<div class="small-label mt-2">{{ notification.creationDate | date: 'exactDate' }}</div>
<div class="small-label mt-2">{{ notification.creationDate | date : 'exactDate' }}</div>
</div>
<div
@ -51,7 +52,7 @@
[id]="'notifications-mark-' + notification.id"
class="dot"
iqserStopPropagation
matTooltip="{{ 'notifications.mark-as' | translate: { type: notification.readDate ? 'unread' : 'read' } }}"
matTooltip="{{ 'notifications.mark-as' | translate : { type: notification.readDate ? 'unread' : 'read' } }}"
matTooltipPosition="before"
></div>
</div>

View File

@ -22,7 +22,7 @@
}
.mat-mdc-menu-item.notification {
padding: 8px 26px 10px 8px !important;
padding: 8px 26px 10px 8px;
margin: 2px 0 0 0;
height: fit-content;
position: relative;

View File

@ -25,7 +25,6 @@ function chronologically(first: string, second: string) {
selector: 'redaction-notifications',
templateUrl: './notifications.component.html',
styleUrls: ['./notifications.component.scss'],
standalone: false,
})
export class NotificationsComponent {
readonly hasUnreadNotifications$: Observable<boolean>;
@ -59,15 +58,6 @@ export class NotificationsComponent {
await this._notificationsService.toggleNotificationRead(notificationsIds, isRead);
}
async handleMarkReadEvent(event: MouseEvent, notifications: Notification[] = this._notificationsService.all, isRead = true) {
if (!(event.target as HTMLBaseElement).href) {
event.stopPropagation();
event.preventDefault();
}
await this.markRead(notifications, isRead);
}
#groupNotifications(notifications: Notification[]): NotificationsGroup[] {
const todayTranslation = this._translateService.instant('today');
const groupedMap = notifications.groupBy(n => {

View File

@ -9,7 +9,7 @@
.container {
padding: 32px;
width: 1000px;
width: 900px;
max-width: 100%;
box-sizing: border-box;
}

View File

@ -5,6 +5,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
templateUrl: './dashboard-skeleton.component.html',
styleUrls: ['./dashboard-skeleton.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class DashboardSkeletonComponent {}

View File

@ -5,7 +5,6 @@ import { ChangeDetectionStrategy, Component, OnInit, TemplateRef, ViewChild } fr
templateUrl: './dossier-skeleton.component.html',
styleUrls: ['./dossier-skeleton.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class DossierSkeletonComponent implements OnInit {
@ViewChild('workload1', { static: true }) workload1: TemplateRef<unknown>;

View File

@ -5,6 +5,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
templateUrl: './skeleton-stats.component.html',
styleUrls: ['./skeleton-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class SkeletonStatsComponent {}

View File

@ -6,7 +6,6 @@ import { Title } from '@angular/platform-browser';
templateUrl: './skeleton-top-bar.component.html',
styleUrls: ['./skeleton-top-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class SkeletonTopBarComponent {
constructor(readonly titleService: Title) {}

View File

@ -9,7 +9,6 @@ import { MatMenuTrigger } from '@angular/material/menu';
templateUrl: './spotlight-search.component.html',
styleUrls: ['./spotlight-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class SpotlightSearchComponent {
@Input() actions: readonly SpotlightSearchAction[];

View File

@ -9,7 +9,6 @@ interface ITenant extends IStoredTenantId {
selector: 'app-tenants-menu',
templateUrl: './tenants-menu.component.html',
styleUrls: ['./tenants-menu.component.scss'],
standalone: false,
})
export class TenantsMenuComponent {
readonly tenantsService = inject(TenantsService);

View File

@ -1,6 +1,6 @@
<div id="user-menu-items">
<ng-container *ngFor="let item of userMenuItems; trackBy: trackBy">
<a (click)="item.action?.()" *ngIf="item.show" [id]="item.id" [routerLink]="item.routerLink" mat-menu-item>
<a (click)="item.action?.()" *ngIf="item.show" [id]="item.id" [routerLink]="item.routerLink | tenant" mat-menu-item>
{{ item.name | translate }}
</a>
</ng-container>

View File

@ -6,6 +6,7 @@ import { User } from '@red/domain';
import { UserService } from '@users/user.service';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { List } from '@iqser/common-ui/lib/utils';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
interface MenuItem {
readonly id: string;
@ -20,12 +21,12 @@ interface MenuItem {
selector: 'app-user-menu',
templateUrl: './user-menu.component.html',
styleUrls: ['./user-menu.component.scss'],
standalone: false,
})
export class UserMenuComponent {
readonly currentUser = getCurrentUser<User>();
readonly userService = inject(UserService);
readonly #permissionsService = inject(IqserPermissionsService);
readonly currentUser = getCurrentUser<User>();
readonly tenantsService = inject(TenantsService);
readonly userService = inject(UserService);
readonly userMenuItems: List<MenuItem> = [
{
id: 'account',

View File

@ -1,9 +1,8 @@
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
import { firstValueFrom } from 'rxjs';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
// TODO: Remove this and use a CanActivateFn instead
@Injectable({ providedIn: 'root' })
export class DashboardGuard implements CanActivate {
constructor(private readonly _dashboardStatsService: DashboardStatsService) {}

View File

@ -1,6 +1,7 @@
import { Injectable, Injector, ProviderToken } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
import { getConfig } from '@iqser/common-ui';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
import { DOSSIER_ID, DOSSIER_TEMPLATE_ID } from '@red/domain';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
@ -15,6 +16,7 @@ export class DossierFilesGuard implements CanActivate {
constructor(
private readonly _injector: Injector,
private readonly _tenantsService: TenantsService,
private readonly _filesMapService: FilesMapService,
private readonly _filesService: FilesService,
private readonly _dictionaryService: DictionaryService,
@ -35,7 +37,7 @@ export class DossierFilesGuard implements CanActivate {
}
if (!dossiersService.has(dossierId)) {
await this._router.navigate(['/main', dossierTemplateId]);
await this._router.navigate([`/${this._tenantsService.activeTenantId}/main`, dossierTemplateId]);
return false;
}
@ -47,7 +49,7 @@ export class DossierFilesGuard implements CanActivate {
const promises = [];
if (!this._dictionaryMapService.has(dossierId) && !this.isDocumine) {
const dictionaryPromise = firstValueFrom(this._dictionaryService.loadDictionaryDataForDossier(dossierTemplateId, dossierId));
const dictionaryPromise = this._dictionaryService.loadDossierDictionary(dossierTemplateId, dossierId);
promises.push(dictionaryPromise);
}

View File

@ -1,5 +1,6 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
import { DOSSIER_TEMPLATE_ID } from '@red/domain';
import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
@ -7,35 +8,14 @@ import { DossierTemplateStatsService } from '@services/entity-services/dossier-t
import { NGXLogger } from 'ngx-logger';
import { firstValueFrom } from 'rxjs';
import { UserPreferenceService } from '@users/user-preference.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { getConfig, Toaster } from '@iqser/common-ui';
import { RulesService } from '../modules/admin/services/rules.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export function templateExistsWhenEnteringAdmin(): CanActivateFn {
return async function (route: ActivatedRouteSnapshot): Promise<boolean> {
const dossierTemplateId: string = route.paramMap.get(DOSSIER_TEMPLATE_ID);
const fileAttributesService = inject(FileAttributesService);
const dictionaryService = inject(DictionaryService);
const defaultColorsService = inject(DefaultColorsService);
const watermarksService = inject(WatermarkService);
const router = inject(Router);
const rulesService = inject(RulesService);
const isDocumine = getConfig().IS_DOCUMINE;
const dossierTemplate = inject(DossierTemplateStatsService).get(dossierTemplateId);
await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId));
await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId));
await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId));
await firstValueFrom(rulesService.getFor(dossierTemplateId));
if (!isDocumine) {
await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId));
}
if (!dossierTemplate) {
await router.navigate(['main', 'admin', 'dossier-templates']);
await inject(Router).navigate([inject(TenantsService).activeTenantId, 'main', 'admin', 'dossier-templates']);
return false;
}
return true;
@ -49,34 +29,18 @@ export function templateExistsWhenEnteringDossierList(): CanActivateFn {
const dossierTemplatesService = inject(DossierTemplatesService);
const logger = inject(NGXLogger);
const router = inject(Router);
const tenantsService = inject(TenantsService);
const userPreferencesService = inject(UserPreferenceService);
const fileAttributesService = inject(FileAttributesService);
const dictionaryService = inject(DictionaryService);
const defaultColorsService = inject(DefaultColorsService);
const watermarksService = inject(WatermarkService);
const rulesService = inject(RulesService);
const toaster = inject(Toaster);
const isDocumine = getConfig().IS_DOCUMINE;
await firstValueFrom(dashboardStatsService.loadForTemplate(dossierTemplateId));
await firstValueFrom(dossierTemplatesService.loadDossierTemplate(dossierTemplateId));
await firstValueFrom(dashboardStatsService.loadAll());
await firstValueFrom(dossierTemplatesService.loadAll());
const dossierTemplateStats = dashboardStatsService.find(dossierTemplateId);
if (!dossierTemplateStats || dossierTemplateStats.isEmpty) {
logger.warn(`[ROUTES] Dossier template ${dossierTemplateId} not found, redirecting to main`);
await userPreferencesService.saveLastDossierTemplate(null);
await router.navigate(['main']);
await router.navigate([tenantsService.activeTenantId, 'main']);
return false;
}
await firstValueFrom(fileAttributesService.loadFileAttributesConfig(dossierTemplateId));
await firstValueFrom(dictionaryService.loadDictionaryDataForDossierTemplate(dossierTemplateId));
await firstValueFrom(defaultColorsService.loadForDossierTemplate(dossierTemplateId));
const rules = await firstValueFrom(rulesService.getFor(dossierTemplateId));
if (rules.timeoutDetected) {
toaster.error(_('dossier-listing.rules.timeoutError'));
}
if (!isDocumine) {
await firstValueFrom(watermarksService.loadForDossierTemplate(dossierTemplateId));
}
return true;
};
}

View File

@ -1,7 +1,7 @@
import { inject } from '@angular/core';
import { CanDeactivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import DossierOverviewScreenComponent from '../modules/dossier-overview/screen/dossier-overview-screen.component';
import { DossierOverviewScreenComponent } from '../modules/dossier-overview/screen/dossier-overview-screen.component';
export const isNotEditingFileAttributeGuard: CanDeactivateFn<DossierOverviewScreenComponent> = () =>
export const editAttributeGuard: CanDeactivateFn<DossierOverviewScreenComponent> = () =>
!inject(FileAttributesService).isEditingFileAttribute();

View File

@ -1,13 +1,12 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { GET_TENANT_FROM_PATH_FN } from '@common-ui/utils';
import { AsyncGuard } from '@iqser/common-ui';
import { keycloakInitializer, KeycloakStatusService, TenantsService } from '@iqser/common-ui/lib/tenants';
import { LicenseService } from '@services/license.service';
import { UserService } from '@users/user.service';
import { jwtDecode } from 'jwt-decode';
import { KeycloakService } from 'keycloak-angular';
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { keycloakInitializer, KeycloakStatusService, TenantsService } from '@iqser/common-ui/lib/tenants';
import { KeycloakService } from 'keycloak-angular';
import { UserService } from '@users/user.service';
import { LicenseService } from '@services/license.service';
import { AsyncGuard } from '@iqser/common-ui';
import jwt_decode from 'jwt-decode';
export interface JwtToken {
auth_time: number;
@ -25,36 +24,35 @@ export function ifLoggedIn(): AsyncGuard {
const licenseService = inject(LicenseService);
const keycloakStatusService = inject(KeycloakStatusService);
const tenant = inject(GET_TENANT_FROM_PATH_FN)();
const keycloakInstance = keycloakService.getKeycloakInstance();
const tenant = route.paramMap.get('tenant');
const queryParams = new URLSearchParams(window.location.search);
const username = queryParams.get('username');
const router = inject(Router);
if (!keycloakInstance) {
if (!tenant) {
logger.error('[ROUTES] No tenant found, something is wrong...');
return router.navigate(['/']);
return inject(Router).navigate(['/']);
}
logger.info('[KEYCLOAK] Keycloak init...');
await keycloakInitializer(tenant);
logger.info('[KEYCLOAK] Keycloak init done for tenant: ', { tenant });
logger.info('[KEYCLOAK] Keycloak init done!');
await tenantsService.selectTenant(tenant);
await usersService.initialize();
await licenseService.loadLicenses();
const token = await keycloakService.getToken();
if (token) {
const jwtToken = jwtDecode(token) as JwtToken;
const jwtToken = jwt_decode(token) as JwtToken;
const authTime = (jwtToken.auth_time || jwtToken.iat).toString();
localStorage.setItem('authTime', authTime);
localStorage.setItem('token', token);
}
}
if (keycloakService.isLoggedIn()) {
const isLoggedIn = await keycloakService.isLoggedIn();
if (isLoggedIn) {
logger.info('[ROUTES] Is logged in, continuing');
return true;
}

View File

@ -0,0 +1,29 @@
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { KeycloakService } from 'keycloak-angular';
export function ifNotLoggedIn(): CanActivateFn {
return async (route: ActivatedRouteSnapshot) => {
const logger = inject(NGXLogger);
const router = inject(Router);
const keycloakService = inject(KeycloakService);
const isLoggedIn = await keycloakService.isLoggedIn();
if (!isLoggedIn) {
logger.info('[ROUTES] Not logged in, continuing to selected route');
return true;
}
const tenant = route.paramMap.get('tenant') || keycloakService.getKeycloakInstance().realm;
if (!tenant) {
logger.error('[ROUTES] Tenant not found in route or keycloak realm');
return false;
}
logger.warn('[ROUTES] Is logged in for ' + tenant + ', redirecting to /' + tenant);
await router.navigate([tenant]);
return false;
};
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { EntityPermissionsService } from '@services/entity-permissions/entity-permissions.service';
import { firstValueFrom } from 'rxjs';
import { EntityPermissionsService } from '@services/entity-permissions/entity-permissions.service';
@Injectable({ providedIn: 'root' })
export class PermissionsGuard implements CanActivate {

View File

@ -1,73 +1,48 @@
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Dictionary, SuperTypes } from '@red/domain';
import { Dictionary } from '@red/domain';
export const canUndo = (annotation: AnnotationWrapper, isApprover: boolean) => !isApprover && annotation.pending;
export const canUndo = (annotation: AnnotationWrapper, isApprover: boolean) =>
!isApprover && (annotation.isSuggestion || annotation.pending);
export const canForceHint = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && annotation.isIgnoredHint && !annotation.pending;
export const canForceRedaction = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction &&
(annotation.isSkipped || (annotation.IMAGE_HINT && annotation.superType === SuperTypes.Hint)) &&
!annotation.isFalsePositive &&
!annotation.pending;
canAddRedaction && annotation.isSkipped && !annotation.isFalsePositive && !annotation.pending;
export const canAcceptRecommendation = (annotation: AnnotationWrapper) => annotation.isRecommendation && !annotation.pending;
export const canMarkAsFalsePositive = (annotation: AnnotationWrapper, annotationEntity: Dictionary) =>
annotation.canBeMarkedAsFalsePositive && !annotation.hasBeenResizedLocally && annotationEntity?.hasDictionary;
annotation.canBeMarkedAsFalsePositive && annotationEntity.hasDictionary;
export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean, autoAnalysisDisabled: boolean) =>
canAddRedaction &&
(autoAnalysisDisabled || !annotation.pending) &&
(annotation.isRedacted || (annotation.isHint && !annotation.isImage));
export const canRemoveOnlyHere = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && !annotation.pending && (annotation.isRedacted || (annotation.isHint && !annotation.isImage));
export const canRemoveFromDictionary = (annotation: AnnotationWrapper, autoAnalysisDisabled: boolean) =>
(annotation.isModifyDictionary || annotation.engines.includes('DICTIONARY') || annotation.engines.includes('DOSSIER_DICTIONARY')) &&
(annotation.isRedacted || annotation.isSkipped || annotation.isHint || (annotation.isIgnoredHint && !annotation.isRuleBased)) &&
(autoAnalysisDisabled || !annotation.pending) &&
annotation.isDictBased;
export const canRemoveFromDictionary = (annotation: AnnotationWrapper) =>
annotation.isModifyDictionary &&
(annotation.isRedacted || annotation.isSkipped || annotation.isHint) &&
!annotation.pending &&
!annotation.hasBeenResized;
export const canRemoveRedaction = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) =>
(!annotation.isIgnoredHint || !annotation.isRuleBased) &&
!annotation.isIgnoredHint &&
(permissions.canRemoveOnlyHere || permissions.canRemoveFromDictionary || permissions.canMarkAsFalsePositive);
export const canChangeLegalBasis = (annotation: AnnotationWrapper, canAddRedaction: boolean, autoAnalysisDisabled: boolean) =>
canAddRedaction && annotation.isRedacted && (autoAnalysisDisabled || !annotation.pending);
export const canChangeLegalBasis = (annotation: AnnotationWrapper, canAddRedaction: boolean) =>
canAddRedaction && annotation.isRedacted && !annotation.pending;
export const canRecategorizeAnnotation = (annotation: AnnotationWrapper, canRecategorize: boolean, autoAnalysisDisabled: boolean) =>
export const canRecategorizeAnnotation = (annotation: AnnotationWrapper, canRecategorize: boolean) =>
canRecategorize &&
(annotation.isImage || (annotation.isHint && !annotation.isRuleBased)) &&
(autoAnalysisDisabled || !annotation.pending);
((annotation.isImage && !annotation.isSuggestion) || annotation.isSuggestionRecategorizeImage || annotation.hintDictionary) &&
!annotation.pending;
export const canResizeAnnotation = (
annotation: AnnotationWrapper,
canAddRedaction: boolean,
autoAnalysisDisabled: boolean,
hasDictionary = false,
) =>
(canAddRedaction &&
!annotation.isSkipped &&
!annotation.isIgnoredHint &&
(autoAnalysisDisabled || !annotation.pending) &&
(annotation.isRedacted ||
annotation.isImage ||
(annotation.isHint && !annotation.isRuleBased) ||
(!annotation.isHint && hasDictionary && annotation.isRuleBased))) ||
annotation.isRecommendation;
export const canResizeInDictionary = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) =>
permissions.canResizeAnnotation &&
annotation.isModifyDictionary &&
!annotation.hasBeenResizedLocally &&
!(annotation.hasBeenForcedHint || annotation.hasBeenForcedRedaction);
export const canEditAnnotation = (annotation: AnnotationWrapper) => (annotation.isRedacted || annotation.isSkipped) && !annotation.isImage;
export const canEditHint = (annotation: AnnotationWrapper) =>
((annotation.isHint && !annotation.isRuleBased) || annotation.isIgnoredHint) && !annotation.isImage;
export const canEditImage = (annotation: AnnotationWrapper) => annotation.isImage;
export const canRevertChanges = (annotation: AnnotationWrapper) => annotation.hasRedactionChanges;
export const canResizeAnnotation = (annotation: AnnotationWrapper, canAddRedaction: boolean, hasDictionary = false) =>
canAddRedaction &&
!annotation.isSkipped &&
!annotation.pending &&
(((annotation.isRedacted || annotation.isImage) && !annotation.isSuggestion) ||
annotation.isSuggestionResize ||
annotation.isDictBasedHint ||
annotation.isRecommendation ||
(hasDictionary && annotation.isRuleBased));

View File

@ -2,12 +2,10 @@ import { IqserPermissionsService } from '@iqser/common-ui';
import { Dictionary } from '@red/domain';
import { Roles } from '@users/roles';
import { isArray } from 'lodash-es';
import { IMAGE_CATEGORIES } from '../../modules/file-preview/utils/constants';
import {
canAcceptRecommendation,
canChangeLegalBasis,
canEditAnnotation,
canEditHint,
canEditImage,
canForceHint,
canForceRedaction,
canMarkAsFalsePositive,
@ -16,8 +14,6 @@ import {
canRemoveOnlyHere,
canRemoveRedaction,
canResizeAnnotation,
canResizeInDictionary,
canRevertChanges,
canUndo,
} from './annotation-permissions.utils';
import { AnnotationWrapper } from './annotation.wrapper';
@ -32,20 +28,17 @@ export class AnnotationPermissions {
canForceRedaction = true;
canChangeLegalBasis = true;
canResizeAnnotation = true;
canResizeInDictionary = true;
canRecategorizeAnnotation = true;
canForceHint = true;
canEditAnnotations = true;
canEditHints = true;
canEditImages = true;
canRevertChanges = true;
static forUser(
isApprover: boolean,
annotations: AnnotationWrapper | AnnotationWrapper[],
entities: Dictionary[],
permissionsService: IqserPermissionsService,
autoAnalysisDisabled: boolean,
) {
if (!isArray(annotations)) {
annotations = [annotations];
@ -62,22 +55,18 @@ export class AnnotationPermissions {
permissions.canForceRedaction = canForceRedaction(annotation, canAddRedaction);
permissions.canAcceptRecommendation = canAcceptRecommendation(annotation);
permissions.canMarkAsFalsePositive = canMarkAsFalsePositive(annotation, annotationEntity);
permissions.canRemoveOnlyHere = canRemoveOnlyHere(annotation, canAddRedaction, autoAnalysisDisabled);
permissions.canRemoveFromDictionary = canRemoveFromDictionary(annotation, autoAnalysisDisabled);
permissions.canRemoveOnlyHere = canRemoveOnlyHere(annotation, canAddRedaction);
permissions.canRemoveFromDictionary = canRemoveFromDictionary(annotation);
permissions.canRemoveRedaction = canRemoveRedaction(annotation, permissions);
permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canAddRedaction, autoAnalysisDisabled);
permissions.canRecategorizeAnnotation = canRecategorizeAnnotation(annotation, canAddRedaction, autoAnalysisDisabled);
permissions.canResizeAnnotation = canResizeAnnotation(
annotation,
canAddRedaction,
autoAnalysisDisabled,
annotationEntity?.hasDictionary,
);
permissions.canResizeInDictionary = canResizeInDictionary(annotation, permissions);
permissions.canEditAnnotations = canEditAnnotation(annotation);
permissions.canEditHints = canEditHint(annotation);
permissions.canEditImages = canEditImage(annotation);
permissions.canRevertChanges = canRevertChanges(annotation);
permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canAddRedaction);
permissions.canRecategorizeAnnotation = canRecategorizeAnnotation(annotation, canAddRedaction);
permissions.canResizeAnnotation = canResizeAnnotation(annotation, canAddRedaction, annotationEntity.hasDictionary);
permissions.canEditAnnotations = (annotation.isSkipped || annotation.isRedacted) && !annotation.isImage;
permissions.canEditHints =
annotation.isIgnoredHint || annotation.isDictBasedHint || annotation.isHint || annotation.isSuggestionForceHint;
permissions.canEditImages = [...IMAGE_CATEGORIES, 'ocr'].includes(annotation.type);
summedPermissions._merge(permissions);
}
return summedPermissions;
@ -86,7 +75,6 @@ export class AnnotationPermissions {
static reduce(permissions: AnnotationPermissions[]): AnnotationPermissions {
const result = new AnnotationPermissions();
result.canResizeAnnotation = permissions.length === 1 && permissions[0].canResizeAnnotation;
result.canResizeInDictionary = permissions.length === 1 && permissions[0].canResizeInDictionary;
result.canChangeLegalBasis = permissions.reduce((acc, next) => acc && next.canChangeLegalBasis, true);
result.canRecategorizeAnnotation = permissions.reduce((acc, next) => acc && next.canRecategorizeAnnotation, true);
result.canRemoveFromDictionary = permissions.reduce((acc, next) => acc && next.canRemoveFromDictionary, true);
@ -100,7 +88,6 @@ export class AnnotationPermissions {
result.canEditAnnotations = permissions.reduce((acc, next) => acc && next.canEditAnnotations, true);
result.canEditHints = permissions.reduce((acc, next) => acc && next.canEditHints, true);
result.canEditImages = permissions.reduce((acc, next) => acc && next.canEditImages, true);
result.canRevertChanges = permissions.reduce((acc, next) => acc && next.canRevertChanges, true);
return result;
}

View File

@ -5,99 +5,83 @@ import {
annotationEntityColorConfig,
AnnotationIconType,
ChangeType,
ChangeTypes,
DefaultColors,
Dictionary,
Earmark,
EntityTypes,
EntryStates,
FalsePositiveSuperTypes,
IEntityLogEntry,
ILegalBasis,
IManualChange,
IPoint,
IRectangle,
IRedactionLogEntry,
LogEntryEngine,
LogEntryEngines,
LogEntryStatuses,
LowLevelFilterTypes,
ManualRedactionType,
ManualRedactionTypes,
SuggestionAddSuperTypes,
SuggestionRemoveSuperTypes,
SuggestionsSuperTypes,
SuperType,
SuperTypeMapper,
SuperTypes,
} from '@red/domain';
import { annotationTypesTranslations } from '@translations/annotation-types-translations';
interface AnnotationContent {
translation: string;
params: { [key: string]: string };
untranslatedContent: string;
}
import { annotationTypesTranslations, SuggestionAddFalsePositive } from '@translations/annotation-types-translations';
import { chronologicallyBy, timestampOf } from '../../modules/file-preview/services/file-data.service';
export class AnnotationWrapper implements IListable {
id: string;
[x: string]: unknown;
superType: SuperType;
superTypeLabel: string;
type: string;
typeLabel?: string;
typeValue: string;
recategorizationType: string;
color: string;
entity: Dictionary;
numberOfComments = 0;
firstBottomLeftPoint: IPoint;
firstTopLeftPoint: IPoint;
id: string;
shortContent: string;
content: AnnotationContent;
content: string;
value: string;
typeLabel: string;
pageNumber: number;
dictionaryOperation = false;
positions: IRectangle[] = [];
hint: boolean;
redaction: boolean;
status: string;
dictionaryOperation: boolean;
positions: IRectangle[];
recommendationType: string;
legalBasisValue: string;
// AREA === rectangle
AREA = false;
HINT = false;
IMAGE = false;
IMAGE_HINT = false;
legalBasisChangeValue?: string;
rectangle?: boolean;
section?: string;
reference: string[] = [];
imported = false;
manual = false;
reference: string[];
imported?: boolean;
image?: boolean;
manual?: boolean;
pending = false;
hintDictionary = false;
textAfter?: string;
textBefore?: string;
isChangeLogEntry = false;
engines: LogEntryEngine[] = [];
changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED';
engines?: LogEntryEngine[];
hasBeenResized: boolean;
hasBeenResizedLocally: boolean;
hasBeenRecategorized: boolean;
hasLegalBasisChanged: boolean;
hasBeenForcedHint: boolean;
hasBeenForcedRedaction: boolean;
hasBeenRemovedByManualOverride: boolean;
isRemoved = false;
isRemovedLocally = false;
hiddenInWorkload = false;
lastManualChange: ManualRedactionType;
entry: IEntityLogEntry;
get isRuleBased() {
return this.engines.includes(LogEntryEngines.RULE);
}
get isDictBased() {
return [LogEntryEngines.DICTIONARY, LogEntryEngines.DOSSIER_DICTIONARY].some(engine => this.engines.includes(engine));
}
get isRedactedImageHint() {
return (
(this.IMAGE_HINT && this.superType === SuperTypes.Redaction) ||
(this.IMAGE_HINT && this.superType === SuperTypes.ManualRedaction)
);
}
get isSkippedImageHint() {
return this.IMAGE_HINT && this.superType === SuperTypes.Hint;
}
legalBasisList: ILegalBasis[] = [];
get searchKey(): string {
return this.id;
}
get isChangeLogRemoved() {
return this.changeLogType === 'REMOVED';
}
get descriptor() {
return this.isModifyDictionary ? _('dictionary') : _('type');
}
@ -112,10 +96,7 @@ export class AnnotationWrapper implements IListable {
get canBeMarkedAsFalsePositive() {
return (
(this.isRecommendation ||
this.superType === SuperTypes.Redaction ||
(this.isSkipped && this.isDictBased) ||
(this.isRemovedLocally && this.isDictBased)) &&
(this.isRecommendation || this.superType === SuperTypes.Redaction) &&
!this.isImage &&
!this.imported &&
!this.pending &&
@ -123,18 +104,30 @@ export class AnnotationWrapper implements IListable {
);
}
get isSuperTypeBasedColor() {
return this.isSuggestion || this.isDeclinedSuggestion;
}
get isSkipped() {
return this.superType === SuperTypes.Skipped;
}
get isImage() {
return this.type?.toLowerCase() === 'image' || this.IMAGE || this.IMAGE_HINT;
return this.type?.toLowerCase() === 'image' || this.image;
}
get isNotSignatureImage() {
return this.isImage && this.recategorizationType === 'signature';
}
get isOCR() {
return this.type?.toLowerCase() === 'ocr';
}
get type() {
return this.recategorizationType || this.typeValue;
}
get topLevelFilter() {
return !LowLevelFilterTypes[this.superType];
}
@ -151,12 +144,20 @@ export class AnnotationWrapper implements IListable {
return this.type?.toLowerCase() === 'false_positive' && !!FalsePositiveSuperTypes[this.superType];
}
get isHint() {
return this.superType === SuperTypes.Hint || this.superType === SuperTypes.ManualHint;
get isSuggestionAddToFalsePositive() {
return this.typeLabel === annotationTypesTranslations[SuggestionAddFalsePositive];
}
get isIgnoredHint() {
return this.superType === SuperTypes.IgnoredHint;
get isDeclinedSuggestion() {
return this.superType === SuperTypes.DeclinedSuggestion;
}
get isApproved() {
return this.status === 'APPROVED';
}
get isHint() {
return this.superType === SuperTypes.Hint;
}
get isEarmark() {
@ -168,17 +169,69 @@ export class AnnotationWrapper implements IListable {
return 'hexagon';
}
if (this.HINT) {
if (this.isHint || this.isIgnoredHint) {
return 'circle';
}
if (this.isSuggestion || this.isDeclinedSuggestion) {
return 'rhombus';
}
return 'square';
}
get isDictBasedHint() {
return this.isHint && this.engines.includes(LogEntryEngines.DICTIONARY);
}
get isIgnoredHint() {
return this.superType === SuperTypes.IgnoredHint;
}
get isRedacted() {
return this.superType === SuperTypes.Redaction || this.superType === SuperTypes.ManualRedaction;
}
get isSuggestion() {
return !!SuggestionsSuperTypes[this.superType];
}
get isRuleBased() {
return this.engines.includes(LogEntryEngines.RULE);
}
get isSuggestionResize() {
return this.superType === SuperTypes.SuggestionResize;
}
get isSuggestionRecategorizeImage() {
return this.superType === SuperTypes.SuggestionRecategorizeImage;
}
get isSuggestionForceHint() {
return this.superType === SuperTypes.SuggestionForceHint;
}
get isSuggestionAdd() {
return !!SuggestionAddSuperTypes[this.superType];
}
get isSuggestionAddDictionary() {
return this.superType === SuperTypes.SuggestionAddDictionary;
}
get isSuggestionRemove() {
return !!SuggestionRemoveSuperTypes[this.superType];
}
get isSuggestionRemoveDictionary() {
return this.superType === SuperTypes.SuggestionRemoveDictionary;
}
get isSuggestionLegalBasisChange() {
return this.superType === SuperTypes.SuggestionChangeLegalBasis;
}
get isModifyDictionary() {
return this.dictionaryOperation;
}
@ -199,15 +252,15 @@ export class AnnotationWrapper implements IListable {
}
get x() {
return this.firstBottomLeftPoint.x;
return this.firstTopLeftPoint.x;
}
get y() {
return this.firstBottomLeftPoint.y;
return this.firstTopLeftPoint.y;
}
get legalBasis() {
return this.legalBasisValue;
return this.legalBasisChangeValue || this.legalBasisValue;
}
get width(): number {
@ -218,167 +271,201 @@ export class AnnotationWrapper implements IListable {
return Math.floor(this.positions[0].height);
}
get previewAnnotation() {
return (
this.isRedacted ||
this.isSuggestionAdd ||
this.isSuggestionRemove ||
this.isSuggestionResize ||
this.isSuggestionLegalBasisChange ||
this.isSuggestionRecategorizeImage
);
}
static fromEarmark(earmark: Earmark) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.id = earmark.id;
annotationWrapper.pageNumber = earmark.positions[0].page;
annotationWrapper.superType = SuperTypes.TextHighlight;
annotationWrapper.type = SuperTypes.TextHighlight;
annotationWrapper.typeValue = SuperTypes.TextHighlight;
annotationWrapper.value = 'Imported';
annotationWrapper.color = earmark.hexColor;
annotationWrapper.positions = earmark.positions;
annotationWrapper.firstBottomLeftPoint = earmark.positions[0]?.topLeft;
annotationWrapper.superTypeLabel = annotationTypesTranslations[annotationWrapper.superType];
annotationWrapper.firstTopLeftPoint = earmark.positions[0]?.topLeft;
annotationWrapper.typeLabel = annotationTypesTranslations[annotationWrapper.superType];
return annotationWrapper;
}
static fromData(
logEntry: IEntityLogEntry,
dictionary: Dictionary,
redactionLogEntry: IRedactionLogEntry,
dictionaries: Dictionary[],
defaultColors: DefaultColors,
changeLogType: ChangeType,
legalBasisList: ILegalBasis[],
hintDictionary: boolean,
isDocumine: boolean,
defaultColors: DefaultColors,
) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.pending = logEntry.state === EntryStates.PENDING;
annotationWrapper.id = logEntry.id + (annotationWrapper.pending ? '-pending' : '');
annotationWrapper.isChangeLogEntry = logEntry.state === EntryStates.REMOVED || !!changeLogType;
annotationWrapper.type = logEntry.type;
annotationWrapper.value = logEntry.value;
annotationWrapper.firstBottomLeftPoint = { x: logEntry.positions[0].rectangle[0], y: logEntry.positions[0].rectangle[1] };
annotationWrapper.pageNumber = logEntry.positions[0].pageNumber;
annotationWrapper.positions = logEntry.positions.map(p => ({
page: p.pageNumber,
height: p.rectangle[3],
width: p.rectangle[2],
topLeft: { x: p.rectangle[0], y: p.rectangle[1] },
}));
annotationWrapper.textBefore = logEntry.textBefore;
annotationWrapper.textAfter = logEntry.textAfter;
annotationWrapper.dictionaryOperation = logEntry.dictionaryEntry;
annotationWrapper.HINT = logEntry.entryType === EntityTypes.HINT;
annotationWrapper.IMAGE = logEntry.entryType === EntityTypes.IMAGE;
annotationWrapper.AREA = logEntry.entryType === EntityTypes.AREA;
annotationWrapper.IMAGE_HINT = logEntry.entryType === EntityTypes.IMAGE_HINT;
annotationWrapper.numberOfComments = logEntry.numberOfComments;
annotationWrapper.imported = logEntry.imported;
annotationWrapper.legalBasisValue = logEntry.legalBasis;
annotationWrapper.manual = logEntry.manualChanges?.length > 0;
annotationWrapper.engines = logEntry.engines ?? [];
annotationWrapper.section = logEntry.section;
annotationWrapper.reference = logEntry.reference || [];
annotationWrapper.hasBeenResized = !!logEntry.manualChanges?.find(c => c.manualRedactionType === ManualRedactionTypes.RESIZE);
annotationWrapper.hasBeenResizedLocally =
annotationWrapper.hasBeenResized && annotationWrapper.engines.includes(LogEntryEngines.MANUAL);
annotationWrapper.hasBeenRecategorized = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RECATEGORIZE,
annotationWrapper.id = redactionLogEntry.id;
annotationWrapper.isChangeLogEntry = !!changeLogType;
annotationWrapper.changeLogType = changeLogType;
annotationWrapper.legalBasisList = legalBasisList;
annotationWrapper.redaction = redactionLogEntry.redacted;
annotationWrapper.hint = redactionLogEntry.hint;
annotationWrapper.typeValue = redactionLogEntry.type;
annotationWrapper.value = redactionLogEntry.value;
annotationWrapper.firstTopLeftPoint = redactionLogEntry.positions[0]?.topLeft;
annotationWrapper.pageNumber = redactionLogEntry.positions[0]?.page;
annotationWrapper.positions = redactionLogEntry.positions;
annotationWrapper.textBefore = redactionLogEntry.textBefore;
annotationWrapper.textAfter = redactionLogEntry.textAfter;
annotationWrapper.dictionaryOperation = redactionLogEntry.dictionaryEntry;
annotationWrapper.image = redactionLogEntry.image;
annotationWrapper.imported = redactionLogEntry.imported;
annotationWrapper.legalBasisValue = redactionLogEntry.legalBasis;
annotationWrapper.manual = redactionLogEntry.manualChanges?.length > 0;
annotationWrapper.engines = redactionLogEntry.engines;
annotationWrapper.section = redactionLogEntry.section;
annotationWrapper.reference = redactionLogEntry.reference || [];
annotationWrapper.rectangle = redactionLogEntry.rectangle;
annotationWrapper.hintDictionary = hintDictionary;
annotationWrapper.hasBeenResized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RESIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasLegalBasisChanged = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE,
annotationWrapper.hasBeenRecategorized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RECATEGORIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedHint =
!!logEntry.manualChanges?.find(c => c.manualRedactionType === ManualRedactionTypes.FORCE) && annotationWrapper.HINT;
annotationWrapper.hasBeenForcedRedaction =
!!logEntry.manualChanges?.find(c => c.manualRedactionType === ManualRedactionTypes.FORCE) && !annotationWrapper.HINT;
annotationWrapper.hasBeenRemovedByManualOverride = !!logEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.REMOVE,
annotationWrapper.hasLegalBasisChanged = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedHint = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.FORCE_HINT && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedRedaction = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.FORCE_REDACT && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenRemovedByManualOverride = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.REMOVE_LOCALLY && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.legalBasisChangeValue = redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatuses.REQUESTED,
)?.propertyChanges.legalBasis;
const content = this.#createContent(annotationWrapper, logEntry, isDocumine);
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper, legalBasisList) || content.untranslatedContent;
annotationWrapper.content = content;
this.#createContent(annotationWrapper, redactionLogEntry, isDocumine);
this.#setSuperType(annotationWrapper, redactionLogEntry);
this.#handleRecommendations(annotationWrapper, redactionLogEntry);
annotationWrapper.typeLabel = this.#getTypeLabel(redactionLogEntry, annotationWrapper);
const lastRelevantManualChange = logEntry.manualChanges?.at(-1);
annotationWrapper.lastManualChange = lastRelevantManualChange?.manualRedactionType;
annotationWrapper.superType = SuperTypeMapper[logEntry.entryType][logEntry.state](logEntry);
annotationWrapper.superTypeLabel = annotationTypesTranslations[annotationWrapper.superType];
const entity = dictionaries.find(d => d.type === annotationWrapper.typeValue);
annotationWrapper.entity = entity?.virtual ? null : entity;
annotationWrapper.isRemoved = logEntry.state === EntryStates.REMOVED;
annotationWrapper.isRemovedLocally =
lastRelevantManualChange?.manualRedactionType === ManualRedactionTypes.REMOVE &&
logEntry.engines.includes(LogEntryEngines.MANUAL);
let colorKey = annotationWrapper.isSuperTypeBasedColor
? annotationDefaultColorConfig[annotationWrapper.superType]
: annotationEntityColorConfig[annotationWrapper.superType];
annotationWrapper.typeLabel = dictionary?.virtual ? undefined : dictionary?.label;
if (annotationWrapper.pending) {
annotationWrapper.color = defaultColors[annotationDefaultColorConfig.analysis] as string;
} else {
const colorKey = annotationEntityColorConfig[annotationWrapper.superType];
const defaultColor = annotationDefaultColorConfig[annotationWrapper.superType];
annotationWrapper.color =
dictionary && !annotationWrapper.isRedactedImageHint
? (dictionary[colorKey] as string)
: (defaultColors[defaultColor] as string);
if (annotationWrapper.isSuperTypeBasedColor && annotationWrapper.isSuggestionResize && !annotationWrapper.isModifyDictionary) {
colorKey = annotationDefaultColorConfig[SuperTypes.SuggestionAdd];
}
annotationWrapper.entry = logEntry;
annotationWrapper.color = annotationWrapper.isSuperTypeBasedColor ? defaultColors[colorKey] : (entity[colorKey] as string);
return annotationWrapper;
}
static #createContent(annotationWrapper: AnnotationWrapper, logEntry: IEntityLogEntry, isDocumine: boolean) {
let untranslatedContent = '';
const params: { [key: string]: string } = {};
if (logEntry.matchedRule) {
params['hasRule'] = 'true';
params['matchedRule'] = logEntry.matchedRule.replace(/(^[, ]*)|([, ]*$)/g, '');
params['ruleSymbol'] = isDocumine ? ':' : '';
static #getTypeLabel(redactionLogEntry: IRedactionLogEntry, annotation: AnnotationWrapper): string {
if (redactionLogEntry.reason?.toLowerCase() === 'false positive') {
return annotationTypesTranslations[SuggestionAddFalsePositive];
}
if (annotation.superType === SuperTypes.ManualRedaction && annotation.hintDictionary) {
return _('annotation-type.manual-hint');
}
return annotationTypesTranslations[annotation.superType];
}
untranslatedContent += `Rule ${logEntry.matchedRule} matched${isDocumine ? ':' : ''} \n\n`;
static #handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: IRedactionLogEntry) {
if (annotationWrapper.superType === SuperTypes.Recommendation) {
annotationWrapper.recommendationType = redactionLogEntry.type;
}
}
static #setSuperType(annotationWrapper: AnnotationWrapper, entry: IRedactionLogEntry) {
if (entry.manualChanges?.length) {
const lastRelevantManualChange = entry.manualChanges?.at(-1);
const viableChanges = entry.changes.filter(c => c.analysisNumber > 1);
const lastChange = viableChanges.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const lastChangeOccurredAfterLastManualChange =
lastChange && timestampOf(lastChange.dateTime) > timestampOf(lastRelevantManualChange.processedDate);
if (lastChangeOccurredAfterLastManualChange && lastChange.type === ChangeTypes.ADDED && entry.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
return;
}
annotationWrapper.pending = !lastRelevantManualChange.processed;
annotationWrapper.superType = this.#selectSuperType(entry, lastRelevantManualChange, annotationWrapper.hintDictionary);
if (lastRelevantManualChange.annotationStatus === LogEntryStatuses.REQUESTED) {
annotationWrapper.recategorizationType = lastRelevantManualChange.propertyChanges.type;
}
} else {
if (entry.recommendation) {
annotationWrapper.superType = SuperTypes.Recommendation;
} else if (entry.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
} else if (entry.hint) {
annotationWrapper.superType = SuperTypes.Hint;
} else {
annotationWrapper.superType = SuperTypes.Skipped;
}
}
}
static #createContent(annotationWrapper: AnnotationWrapper, entry: IRedactionLogEntry, isDocumine: boolean) {
let content = '';
if (entry.matchedRule) {
content += `Rule ${entry.matchedRule} matched${isDocumine ? ':' : ''} \n\n`;
}
if (logEntry.reason) {
params['hasReason'] = 'true';
if (isDocumine && logEntry.reason.slice(-1) === '.') {
logEntry.reason = logEntry.reason.slice(0, -1);
if (entry.reason) {
if (isDocumine && entry.reason.slice(-1) === '.') {
entry.reason = entry.reason.slice(0, -1);
}
if (!params['hasRule']) {
params['reason'] = logEntry.reason.substring(0, 1).toUpperCase() + logEntry.reason.substring(1);
} else {
params['reason'] = logEntry.reason;
}
params['reason'] = params['reason'].replace(/(^[, ]*)|([, ]*$)/g, '');
untranslatedContent += logEntry.reason + '\n\n';
content += entry.reason + '\n\n';
//remove leading and trailing commas and whitespaces
untranslatedContent = untranslatedContent.replace(/(^[, ]*)|([, ]*$)/g, '');
untranslatedContent = untranslatedContent.substring(0, 1).toUpperCase() + untranslatedContent.substring(1);
content = content.replace(/(^[, ]*)|([, ]*$)/g, '');
content = content.substring(0, 1).toUpperCase() + content.substring(1);
}
if (annotationWrapper.legalBasis && !isDocumine) {
params['hasLb'] = 'true';
params['legalBasis'] = annotationWrapper.legalBasis;
untranslatedContent += 'Legal basis: ' + annotationWrapper.legalBasis + '\n\n';
content += 'Legal basis: ' + annotationWrapper.legalBasis + '\n\n';
}
if (annotationWrapper.hasBeenRemovedByManualOverride) {
params['hasOverride'] = 'true';
untranslatedContent += 'Removed by manual override';
content += 'Removed by manual override';
}
if (logEntry.section) {
params['hasSection'] = 'true';
params['sectionSymbol'] = isDocumine ? '' : ':';
params['shouldLower'] = untranslatedContent.length.toString();
params['section'] = logEntry.section;
if (entry.section) {
let prefix = `In section${isDocumine ? '' : ':'} `;
if (untranslatedContent.length) {
if (content.length) {
prefix = ` ${prefix.toLowerCase()}`;
}
untranslatedContent += `${prefix} "${logEntry.section}"`;
content += `${prefix} "${entry.section}"`;
}
return { translation: _('annotation-content'), params: params, untranslatedContent: untranslatedContent };
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper) || content;
annotationWrapper.content = content;
}
static #getShortContent(annotationWrapper: AnnotationWrapper, legalBasisList: ILegalBasis[]) {
static #getShortContent(annotationWrapper: AnnotationWrapper) {
if (annotationWrapper.legalBasis) {
const lb = legalBasisList.find(lbm => lbm.technicalName?.toLowerCase().includes(annotationWrapper.legalBasis.toLowerCase()));
const lb = annotationWrapper.legalBasisList.find(
lbm => lbm.reason?.toLowerCase().includes(annotationWrapper.legalBasis.toLowerCase()),
);
if (lb) {
return lb.name;
}
@ -386,4 +473,154 @@ export class AnnotationWrapper implements IListable {
return annotationWrapper.legalBasis;
}
static #selectSuperType(redactionLogEntry: IRedactionLogEntry, lastManualChange: IManualChange, isHintDictionary: boolean): SuperType {
switch (lastManualChange.manualRedactionType) {
case ManualRedactionTypes.ADD_LOCALLY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.ManualRedaction;
case LogEntryStatuses.DECLINED:
return SuperTypes.DeclinedSuggestion;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionAdd;
}
break;
case ManualRedactionTypes.ADD_TO_DICTIONARY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Redaction;
case LogEntryStatuses.DECLINED:
return SuperTypes.DeclinedSuggestion;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionAddDictionary;
}
break;
case ManualRedactionTypes.REMOVE_LOCALLY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatuses.DECLINED: {
if (isHintDictionary) {
return SuperTypes.Hint;
}
if (redactionLogEntry.redacted) {
return SuperTypes.Redaction;
}
return SuperTypes.Skipped;
}
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemove;
}
break;
case ManualRedactionTypes.REMOVE_FROM_DICTIONARY:
if (redactionLogEntry.redacted) {
if (lastManualChange.processed) {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.Skipped;
case LogEntryStatuses.DECLINED:
return SuperTypes.Redaction;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
} else {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return SuperTypes.Redaction;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
}
} else {
if (lastManualChange.processed) {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return redactionLogEntry.recommendation ? SuperTypes.Recommendation : SuperTypes.Skipped;
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
} else {
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
}
}
break;
case ManualRedactionTypes.FORCE_REDACT:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.Redaction;
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionForceRedaction;
}
break;
case ManualRedactionTypes.FORCE_HINT:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
return SuperTypes.Hint;
case LogEntryStatuses.DECLINED:
return SuperTypes.IgnoredHint;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionForceHint;
}
break;
case ManualRedactionTypes.RECATEGORIZE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED: {
if (redactionLogEntry.recommendation) {
return SuperTypes.Recommendation;
} else if (redactionLogEntry.redacted) {
return SuperTypes.Redaction;
} else if (redactionLogEntry.hint) {
return SuperTypes.Hint;
}
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
}
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRecategorizeImage;
}
break;
case ManualRedactionTypes.LEGAL_BASIS_CHANGE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return redactionLogEntry.type === SuperTypes.ManualRedaction ? SuperTypes.ManualRedaction : SuperTypes.Redaction;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionChangeLegalBasis;
}
break;
case ManualRedactionTypes.RESIZE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
if (redactionLogEntry.recommendation) {
return SuperTypes.Recommendation;
} else if (redactionLogEntry.redacted) {
return redactionLogEntry.type === SuperTypes.ManualRedaction
? SuperTypes.ManualRedaction
: SuperTypes.Redaction;
} else if (redactionLogEntry.hint) {
return SuperTypes.Hint;
}
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionResize;
}
break;
}
}
}

View File

@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui';
import { RedRoleGuard } from '@users/red-role.guard';
import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
import { PreferencesComponent } from './screens/preferences/preferences.component';
import { Roles } from '@users/roles';
import { IqserAuthGuard } from '@iqser/common-ui/lib/users';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { NotificationPreferencesService } from './services/notification-preferences.service';
export default [
const routes: IqserRoutes = [
{ path: '', redirectTo: 'user-profile', pathMatch: 'full' },
{
path: 'user-profile',
@ -16,13 +16,12 @@ export default [
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
loadChildren: () => import('./screens/user-profile/user-profile.routes'),
loadChildren: () => import('./screens/user-profile/user-profile.module').then(m => m.UserProfileModule),
},
{
path: 'notifications',
component: BaseAccountScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
@ -30,8 +29,7 @@ export default [
redirectTo: '/',
},
},
providers: [NotificationPreferencesService],
loadChildren: () => import('./screens/notifications/notifications.routes'),
loadChildren: () => import('./screens/notifications/notifications.module').then(m => m.NotificationsModule),
},
{
path: 'preferences',
@ -63,4 +61,10 @@ export default [
},
],
},
] satisfies IqserRoutes;
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AccountRoutingModule {}

View File

@ -4,10 +4,6 @@ import { IqserPermissionsService } from '@iqser/common-ui';
import { Roles } from '@users/roles';
import { User } from '@red/domain';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { SideNavComponent } from '@common-ui/shared';
import { TranslateModule } from '@ngx-translate/core';
import { NgForOf, NgIf } from '@angular/common';
import { RouterLink, RouterLinkActive } from '@angular/router';
interface NavItem {
readonly label: string;
@ -21,7 +17,6 @@ interface NavItem {
templateUrl: './account-side-nav.component.html',
styleUrls: ['./account-side-nav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SideNavComponent, TranslateModule, NgForOf, NgIf, RouterLinkActive, RouterLink],
})
export class AccountSideNavComponent {
readonly currentUser = getCurrentUser<User>();

View File

@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { AccountRoutingModule } from './account-routing.module';
import { AccountSideNavComponent } from './account-side-nav/account-side-nav.component';
import { BaseAccountScreenComponent } from './base-account-screen/base-account-screen-component';
import { NotificationPreferencesService } from './services/notification-preferences.service';
import { TranslateModule } from '@ngx-translate/core';
import { IconButtonComponent, IqserAllowDirective, IqserHelpModeModule } from '@iqser/common-ui';
import { PreferencesComponent } from './screens/preferences/preferences.component';
import { SideNavComponent } from '@iqser/common-ui/lib/shared';
@NgModule({
declarations: [AccountSideNavComponent, BaseAccountScreenComponent, PreferencesComponent],
imports: [
CommonModule,
SharedModule,
AccountRoutingModule,
TranslateModule,
IqserHelpModeModule,
IconButtonComponent,
SideNavComponent,
IqserAllowDirective,
],
providers: [NotificationPreferencesService],
})
export class AccountModule {}

View File

@ -7,12 +7,11 @@
<div class="content-inner">
<div class="content-container full-height">
<div class="overlay-shadow"></div>
<div [ngClass]="!isWarningsScreen && 'dialog'">
@if (!isWarningsScreen) {
<div class="dialog-header">
<div class="heading-l" [translate]="translations[path]"></div>
</div>
}
<div class="dialog">
<div class="dialog-header">
<div class="heading-l" [translate]="translations[path]"></div>
</div>
<router-outlet></router-outlet>
</div>
</div>

View File

@ -1,35 +1,26 @@
import { ChangeDetectionStrategy, Component, OnInit, ViewContainerRef } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { Router } from '@angular/router';
import { accountTranslations } from '@translations/account-translations';
import { NgClass } from '@angular/common';
import { AccountSideNavComponent } from '../account-side-nav/account-side-nav.component';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'redaction-base-account-screen',
templateUrl: './base-account-screen-component.html',
styleUrls: ['./base-account-screen-component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgClass, RouterOutlet, AccountSideNavComponent, TranslateModule],
})
export class BaseAccountScreenComponent implements OnInit {
readonly translations = accountTranslations;
readonly path: string;
readonly isWarningsScreen: boolean;
constructor(
private readonly _router: Router,
private readonly _hostRef: ViewContainerRef,
) {
constructor(private readonly _router: Router, private readonly _hostRef: ViewContainerRef) {
this.path = this._router.url.split('/').pop();
this.isWarningsScreen = this.path === 'warnings-preferences';
}
ngOnInit(): void {
this.#setDialogWidth();
this._setDialogWidth();
}
#setDialogWidth() {
private _setDialogWidth() {
const element = this._hostRef.element.nativeElement as HTMLElement;
element.style.setProperty('--width', this.path === 'user-profile' ? 'unset' : '100%');
}

View File

@ -2,12 +2,11 @@
<div class="dialog-content">
<div *ngFor="let category of notificationCategories">
<div class="iqser-input-group header w-full">
<mat-slide-toggle color="primary" formControlName="{{ category }}Enabled"
>{{ translations[category] | translate }}
</mat-slide-toggle>
<mat-slide-toggle color="primary" formControlName="{{ category }}Enabled">{{
translations[category] | translate
}}</mat-slide-toggle>
</div>
<!-- TODO: This lots of getters-->
<div *ngIf="isCategoryActive(category)" class="options-content">
<div [translate]="'notifications-screen.options-title'" class="statement"></div>
@ -16,8 +15,8 @@
<div class="iqser-input-group">
<ng-container *ngFor="let preference of getRssFilteredSettings(notificationGroupsValues[i])">
<mat-checkbox
(change)="addRemovePreference($event.checked, category, preference)"
*ngIf="!skipPreference(preference)"
(change)="addRemovePreference($event.checked, category, preference)"
[checked]="isPreferenceChecked(category, preference)"
color="primary"
>

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
import { UntypedFormBuilder } from '@angular/forms';
import { NotificationPreferencesService } from '../../../services/notification-preferences.service';
import { BaseFormComponent, getConfig, IconButtonComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseFormComponent, getConfig, LoadingService, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
DocumentNotificationsTypes,
@ -14,10 +14,6 @@ import {
import { firstValueFrom } from 'rxjs';
import { notificationsSettingsTranslations } from '@translations/notifications-settings-translations';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { NgForOf, NgIf } from '@angular/common';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { TranslateModule } from '@ngx-translate/core';
import { MatCheckbox } from '@angular/material/checkbox';
const RSS_EXCLUDED_SETTINGS = ['USER_PROMOTED_TO_APPROVER', 'USER_DEGRADED_TO_REVIEWER', 'ASSIGN_REVIEWER'];
@ -25,7 +21,6 @@ const RSS_EXCLUDED_SETTINGS = ['USER_PROMOTED_TO_APPROVER', 'USER_DEGRADED_TO_RE
templateUrl: './notifications-screen.component.html',
styleUrls: ['./notifications-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, NgForOf, MatSlideToggle, TranslateModule, NgIf, MatCheckbox, IconButtonComponent],
})
export class NotificationsScreenComponent extends BaseFormComponent implements OnInit {
readonly #toaster = inject(Toaster);

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { TranslateModule } from '@ngx-translate/core';
import { IconButtonComponent } from '@iqser/common-ui';
const routes = [{ path: '', component: NotificationsScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [NotificationsScreenComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule, IconButtonComponent],
})
export class NotificationsModule {}

View File

@ -1,11 +0,0 @@
import { NotificationsScreenComponent } from './notifications-screen/notifications-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { IqserRoutes } from '@iqser/common-ui';
export default [
{
path: '',
component: NotificationsScreenComponent,
canDeactivate: [PendingChangesGuard],
},
] satisfies IqserRoutes;

View File

@ -1,79 +0,0 @@
<div class="dialog">
<form [formGroup]="form">
<div class="dialog-content">
<h1>{{ 'dialog-defaults-form.title' | translate }}</h1>
<h3>{{ 'dialog-defaults-form.redaction.title' | translate }}</h3>
<div class="iqser-input-group w-450">
<label translate="dialog-defaults-form.redaction.add-dialog"></label>
<mat-form-field>
<mat-select formControlName="addRedaction">
<mat-option *ngFor="let option of redactionAddOptions" [value]="option.value">{{
option.label | translate
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox *ngIf="displayExtraOptionAddRedaction" formControlName="addRedactionApplyToAll">{{
'dialog-defaults-form.extra-option-label' | translate
}}</mat-checkbox>
</div>
<div class="iqser-input-group w-450">
<label translate="dialog-defaults-form.redaction.remove-dialog"></label>
<mat-form-field>
<mat-select formControlName="removeRedaction">
<mat-option *ngFor="let option of redactionRemoveOptions" [value]="option.value">{{
option.label | translate
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox *ngIf="displayExtraOptionRemoveRedaction" formControlName="removeRedactionApplyToAll">{{
'dialog-defaults-form.extra-option-label' | translate
}}</mat-checkbox>
</div>
<h3>{{ 'dialog-defaults-form.recommendation.title' | translate }}</h3>
<div class="iqser-input-group w-450">
<label translate="dialog-defaults-form.recommendation.remove-dialog"></label>
<mat-form-field>
<mat-select formControlName="removeRecommendation">
<mat-option *ngFor="let option of recommendationRemoveOptions" [value]="option.value">{{
option.label | translate
}}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox *ngIf="displayExtraOptionRemoveRecommendation" formControlName="removeRecommendationApplyToAll">{{
'dialog-defaults-form.extra-option-label' | translate
}}</mat-checkbox>
</div>
<h3>{{ 'dialog-defaults-form.hint.title' | translate }}</h3>
<div class="iqser-input-group w-450">
<label translate="dialog-defaults-form.hint.add-dialog"></label>
<mat-form-field>
<mat-select formControlName="addHint">
<mat-option *ngFor="let option of hintAddOptions" [value]="option.value">{{ option.label | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox *ngIf="displayExtraOptionAddHint" formControlName="addHintApplyToAll">{{
'dialog-defaults-form.extra-option-label' | translate
}}</mat-checkbox>
</div>
<div class="iqser-input-group w-450">
<label translate="dialog-defaults-form.hint.remove-dialog"></label>
<mat-form-field>
<mat-select formControlName="removeHint">
<mat-option *ngFor="let option of removeOptions" [value]="option.value">{{ option.label | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox *ngIf="displayExtraOptionRemoveHint" formControlName="removeHintApplyToAll">{{
'dialog-defaults-form.extra-option-label' | translate
}}</mat-checkbox>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="save()"
[disabled]="!valid || !changed"
[label]="'preferences-screen.actions.save' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
</div>
</form>
</div>

View File

@ -1,16 +0,0 @@
mat-checkbox {
margin: 8px 0 0 8px;
}
h3 {
margin-top: 24px;
margin-bottom: 4px;
}
form .iqser-input-group:not(first-of-type) {
margin-top: 8px;
}
.dialog {
margin-bottom: 0;
}

View File

@ -1,179 +0,0 @@
import { NgForOf, NgIf } from '@angular/common';
import { ChangeDetectorRef, Component, inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { BaseFormComponent } from '@common-ui/form';
import { AsControl } from '@common-ui/utils';
import { IconButtonComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import {
RedactOrHintOption,
RedactOrHintOptions,
RemoveRedactionOption,
RemoveRedactionOptions,
} from '../../../../file-preview/utils/dialog-types';
import {
hintAddOptions,
recommendationRemoveOptions,
redactionAddOptions,
redactionRemoveOptions,
removeOptions,
SystemDefaultType,
} from '../../../utils/dialog-defaults';
interface DefaultOptionsForm {
addRedaction: RedactOrHintOption | SystemDefaultType;
addHint: RedactOrHintOption | SystemDefaultType;
removeRedaction: RemoveRedactionOption | SystemDefaultType;
removeRecommendation: RemoveRedactionOption | SystemDefaultType;
removeHint: RemoveRedactionOption | SystemDefaultType;
addRedactionApplyToAll: boolean;
removeRedactionApplyToAll: boolean;
removeRecommendationApplyToAll: boolean;
addHintApplyToAll: boolean;
removeHintApplyToAll: boolean;
}
@Component({
selector: 'redaction-dialog-defaults',
templateUrl: './dialog-defaults.component.html',
styleUrl: './dialog-defaults.component.scss',
imports: [ReactiveFormsModule, TranslateModule, MatFormField, MatSelect, MatOption, NgForOf, MatCheckbox, NgIf, IconButtonComponent],
})
export class DialogDefaultsComponent extends BaseFormComponent {
readonly #formBuilder = inject(FormBuilder);
readonly #userPreferences = inject(UserPreferenceService);
readonly #changeDetectorRef = inject(ChangeDetectorRef);
form: FormGroup<AsControl<DefaultOptionsForm>> = this.#formBuilder.group({
addRedaction: this.#userPreferences.getAddRedactionDefaultOption(),
addHint: this.#userPreferences.getAddHintDefaultOption(),
removeRedaction: this.#userPreferences.getRemoveRedactionDefaultOption(),
removeRecommendation: this.#userPreferences.getRemoveRecommendationDefaultOption(),
removeHint: this.#userPreferences.getRemoveHintDefaultOption(),
addRedactionApplyToAll: this.#userPreferences.getBool(PreferencesKeys.addRedactionDefaultExtraOption),
removeRedactionApplyToAll: this.#userPreferences.getBool(PreferencesKeys.removeRedactionDefaultExtraOption),
removeRecommendationApplyToAll: this.#userPreferences.getBool(PreferencesKeys.removeRecommendationDefaultExtraOption),
addHintApplyToAll: this.#userPreferences.getBool(PreferencesKeys.addHintDefaultExtraOption),
removeHintApplyToAll: this.#userPreferences.getBool(PreferencesKeys.removeHintDefaultExtraOption),
});
initialFormValue = this.form.getRawValue();
readonly redactionAddOptions = redactionAddOptions;
readonly hintAddOptions = hintAddOptions;
readonly removeOptions = removeOptions;
readonly redactionRemoveOptions = redactionRemoveOptions;
readonly recommendationRemoveOptions = recommendationRemoveOptions;
constructor() {
super();
}
get displayExtraOptionAddRedaction() {
return RedactOrHintOptions.IN_DOSSIER === this.form.controls.addRedaction.value;
}
get displayExtraOptionAddHint() {
return RedactOrHintOptions.IN_DOSSIER === this.form.controls.addHint.value;
}
get displayExtraOptionRemoveRedaction() {
return (
[RemoveRedactionOptions.IN_DOSSIER, RemoveRedactionOptions.FALSE_POSITIVE] as Partial<
RemoveRedactionOption | SystemDefaultType
>[]
).includes(this.form.controls.removeRedaction.value);
}
get displayExtraOptionRemoveHint() {
return RemoveRedactionOptions.IN_DOSSIER === this.form.controls.removeHint.value;
}
get displayExtraOptionRemoveRecommendation() {
return RemoveRedactionOptions.DO_NOT_RECOMMEND === this.form.controls.removeRecommendation.value;
}
async save(): Promise<any> {
const formValue = this.form.value;
if (this.initialFormValue.addRedaction !== this.form.controls.addRedaction.value) {
await this.#userPreferences.saveAddRedactionDefaultOption(this.form.controls.addRedaction.value);
}
if (this.initialFormValue.addHint !== this.form.controls.addHint.value) {
await this.#userPreferences.saveAddHintDefaultOption(this.form.controls.addHint.value);
}
if (this.initialFormValue.removeRedaction !== this.form.controls.removeRedaction.value) {
await this.#userPreferences.saveRemoveRedactionDefaultOption(this.form.controls.removeRedaction.value);
}
if (this.initialFormValue.removeRecommendation !== this.form.controls.removeRecommendation.value) {
await this.#userPreferences.saveRemoveRecommendationDefaultOption(this.form.controls.removeRecommendation.value);
}
if (this.initialFormValue.removeHint !== this.form.controls.removeHint.value) {
await this.#userPreferences.saveRemoveHintDefaultOption(this.form.controls.removeHint.value);
}
if (this.displayExtraOptionAddRedaction) {
if (this.initialFormValue.addRedactionApplyToAll !== this.form.controls.addRedactionApplyToAll.value) {
await this.#userPreferences.saveAddRedactionDefaultExtraOption(this.form.controls.addRedactionApplyToAll.value);
}
} else {
await this.#userPreferences.saveAddRedactionDefaultExtraOption('undefined');
}
if (this.displayExtraOptionAddHint) {
if (this.initialFormValue.addHintApplyToAll !== this.form.controls.addHintApplyToAll.value) {
await this.#userPreferences.saveAddHintDefaultExtraOption(this.form.controls.addHintApplyToAll.value);
}
} else {
await this.#userPreferences.saveAddHintDefaultExtraOption('undefined');
}
if (this.displayExtraOptionRemoveRedaction) {
if (this.initialFormValue.removeRedactionApplyToAll !== this.form.controls.removeRedactionApplyToAll.value) {
await this.#userPreferences.saveRemoveRedactionDefaultExtraOption(this.form.controls.removeRedactionApplyToAll.value);
}
} else {
await this.#userPreferences.saveRemoveRedactionDefaultExtraOption('undefined');
}
if (this.displayExtraOptionRemoveHint) {
if (this.initialFormValue.removeHintApplyToAll !== this.form.controls.removeHintApplyToAll.value) {
await this.#userPreferences.saveRemoveHintDefaultExtraOption(this.form.controls.removeHintApplyToAll.value);
}
} else {
await this.#userPreferences.saveRemoveHintDefaultExtraOption('undefined');
}
if (this.displayExtraOptionRemoveRecommendation) {
if (this.initialFormValue.removeRecommendationApplyToAll !== this.form.controls.removeRecommendationApplyToAll.value) {
await this.#userPreferences.saveRemoveRecommendationDefaultExtraOption(
this.form.controls.removeRecommendationApplyToAll.value,
);
}
} else {
await this.#userPreferences.saveRemoveRecommendationDefaultExtraOption('undefined');
}
await this.#userPreferences.reload();
this.#patchValues();
this.initialFormValue = this.form.getRawValue();
this.#changeDetectorRef.markForCheck();
}
#patchValues() {
this.form.patchValue({
addRedaction: this.#userPreferences.getAddRedactionDefaultOption(),
addHint: this.#userPreferences.getAddHintDefaultOption(),
removeRedaction: this.#userPreferences.getRemoveRedactionDefaultOption(),
removeRecommendation: this.#userPreferences.getRemoveRecommendationDefaultOption(),
removeHint: this.#userPreferences.getRemoveHintDefaultOption(),
addRedactionApplyToAll: this.#userPreferences.getBool(PreferencesKeys.addRedactionDefaultExtraOption),
removeRedactionApplyToAll: this.#userPreferences.getBool(PreferencesKeys.removeRedactionDefaultExtraOption),
removeRecommendationApplyToAll: this.#userPreferences.getBool(PreferencesKeys.removeRecommendationDefaultExtraOption),
addHintApplyToAll: this.#userPreferences.getBool(PreferencesKeys.addHintDefaultExtraOption),
removeHintApplyToAll: this.#userPreferences.getBool(PreferencesKeys.removeHintDefaultExtraOption),
});
}
}

View File

@ -1,52 +1,45 @@
<redaction-dialog-defaults *ngIf="currentScreen === screens.WARNING_PREFERENCES && !config.IS_DOCUMINE"></redaction-dialog-defaults>
<form [formGroup]="form">
<div class="dialog-content">
<div *ngIf="currentScreen === screens.WARNING_PREFERENCES" class="content-delimiter"></div>
<div class="dialog-content-left">
<ng-container *ngIf="currentScreen === screens.PREFERENCES">
<div class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="autoExpandFiltersOnActions">
{{ 'preferences-screen.form.auto-expand-filters-on-action' | translate }}
</mat-slide-toggle>
</div>
<div [ngClass]="currentScreen === screens.WARNING_PREFERENCES && 'dialog'">
<form [formGroup]="form">
<div class="dialog-content">
<div class="dialog-content-left">
<ng-container *ngIf="currentScreen === screens.PREFERENCES">
<div class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="autoExpandFiltersOnActions">
{{ 'preferences-screen.form.auto-expand-filters-on-action' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="config.IS_DOCUMINE" class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="openScmDialogByDefault">
{{ 'preferences-screen.form.open-structured-view-by-default' | translate }}
</mat-slide-toggle>
</div>
<div *allow="roles.getTables" class="iqser-input-group">
<label [translate]="'preferences-screen.form.table-extraction-type'"></label>
<input formControlName="tableExtractionType" />
</div>
</ng-container>
<div *allow="roles.getTables" class="iqser-input-group">
<label [translate]="'preferences-screen.form.table-extraction-type'"></label>
<input formControlName="tableExtractionType" />
</div>
</ng-container>
<ng-container *ngIf="currentScreen === screens.WARNING_PREFERENCES">
<h1>{{ 'preferences-screen.warnings-subtitle' | translate }}</h1>
<p class="warnings-description">{{ 'preferences-screen.warnings-description' | translate }}</p>
<ng-container *ngIf="currentScreen === screens.WARNING_PREFERENCES">
<p class="warnings-subtitle">{{ 'preferences-screen.warnings-subtitle' | translate }}</p>
<p class="warnings-description">{{ 'preferences-screen.warnings-description' | translate }}</p>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="loadAllAnnotationsWarning">
{{ 'preferences-screen.form.load-all-annotations-warning' | translate }}
</mat-checkbox>
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="helpModeDialog">
{{ 'preferences-screen.form.help-mode-dialog' | translate }}
</mat-checkbox>
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="overwriteFileOption">
{{ 'preferences-screen.form.overwrite-file-option' | translate }}
</mat-checkbox>
</div>
</ng-container>
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="loadAllAnnotationsWarning">
{{ 'preferences-screen.form.load-all-annotations-warning' | translate }}
</mat-checkbox>
</div>
</ng-container>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="save()"
[disabled]="!valid || !changed"
[label]="'preferences-screen.actions.save' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
</div>
</form>
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="save()"
[disabled]="!valid || !changed"
[label]="'preferences-screen.actions.save' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
</div>
</form>

View File

@ -1,3 +1,16 @@
@use 'variables';
.content-delimiter {
border-top: 1px solid var(--iqser-separator);
margin-bottom: 15px;
}
.warnings-subtitle {
font-size: var(--iqser-font-size);
color: var(--iqser-text);
font-weight: 600;
}
.warnings-description {
width: 105%;
}

View File

@ -1,33 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import {
BaseFormComponent,
getConfig,
IconButtonComponent,
IqserAllowDirective,
IqserPermissionsService,
isIqserDevMode,
KEYS,
LoadingService,
} from '@iqser/common-ui';
import { BaseFormComponent, getConfig, IqserPermissionsService, isIqserDevMode, LoadingService } from '@iqser/common-ui';
import { AsControl } from '@iqser/common-ui/lib/utils';
import { Roles } from '@users/roles';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import { DialogDefaultsComponent } from './dialog-defaults/dialog-defaults.component';
import { NgClass, NgIf } from '@angular/common';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { TranslateModule } from '@ngx-translate/core';
import { MatCheckbox } from '@angular/material/checkbox';
interface PreferencesForm {
// preferences
autoExpandFiltersOnActions: boolean;
openScmDialogByDefault: boolean;
tableExtractionType: string;
// warnings preferences
loadAllAnnotationsWarning: boolean;
helpModeDialog: boolean;
overwriteFileOption: boolean;
[k: string]: any;
}
@ -44,25 +29,8 @@ const Screens = {
templateUrl: './preferences.component.html',
styleUrls: ['./preferences.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DialogDefaultsComponent,
NgClass,
NgIf,
ReactiveFormsModule,
MatSlideToggle,
TranslateModule,
IqserAllowDirective,
MatCheckbox,
IconButtonComponent,
],
})
export class PreferencesComponent extends BaseFormComponent implements OnInit {
readonly #formBuilder = inject(FormBuilder);
readonly #permissionsService = inject(IqserPermissionsService);
readonly #changeRef = inject(ChangeDetectorRef);
readonly #loadingService = inject(LoadingService);
readonly #userPreferenceService = inject(UserPreferenceService);
readonly form: FormGroup<AsControl<PreferencesForm>>;
readonly currentScreen: Screen;
readonly screens = Screens;
@ -71,87 +39,75 @@ export class PreferencesComponent extends BaseFormComponent implements OnInit {
readonly config = getConfig();
readonly isIqserDevMode = isIqserDevMode();
get #isOverwriteFileOptionActive() {
return !(this.#userPreferenceService.getOverwriteFileOption() === 'undefined');
}
constructor(route: ActivatedRoute) {
constructor(
route: ActivatedRoute,
readonly userPreferenceService: UserPreferenceService,
private readonly _formBuilder: FormBuilder,
private readonly _permissionsService: IqserPermissionsService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService,
) {
super();
this.form = this.#formBuilder.group({
this.form = this._formBuilder.group({
// preferences
autoExpandFiltersOnActions: [this.#userPreferenceService.getAutoExpandFiltersOnActions()],
tableExtractionType: [this.#userPreferenceService.getTableExtractionType()],
autoExpandFiltersOnActions: [this.userPreferenceService.getAutoExpandFiltersOnActions()],
openScmDialogByDefault: [this.userPreferenceService.getOpenScmDialogByDefault()],
tableExtractionType: [this.userPreferenceService.getTableExtractionType()],
// warnings preferences
loadAllAnnotationsWarning: [this.#userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)],
helpModeDialog: [this.#userPreferenceService.getBool(KEYS.helpModeDialog)],
overwriteFileOption: [this.#isOverwriteFileOptionActive],
loadAllAnnotationsWarning: [this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)],
});
if (!this.#permissionsService.has(Roles.managePreferences)) {
if (!this._permissionsService.has(Roles.managePreferences)) {
this.form.disable();
}
if (!this.#permissionsService.has(Roles.getTables)) {
if (!this._permissionsService.has(Roles.getTables)) {
this.form.controls.tableExtractionType.disable();
}
if (!this.#isOverwriteFileOptionActive) {
this.form.controls.overwriteFileOption.disable();
}
this.initialFormValue = this.form.getRawValue();
this.currentScreen = route.snapshot.data.screen;
}
ngOnInit() {
this.#loadingService.stop();
this._loadingService.stop();
}
async save(): Promise<any> {
if (this.form.controls.autoExpandFiltersOnActions.value !== this.#userPreferenceService.getAutoExpandFiltersOnActions()) {
await this.#userPreferenceService.toggleAutoExpandFiltersOnActions();
if (this.form.controls.autoExpandFiltersOnActions.value !== this.userPreferenceService.getAutoExpandFiltersOnActions()) {
await this.userPreferenceService.toggleAutoExpandFiltersOnActions();
}
if (this.form.controls.openScmDialogByDefault.value !== this.userPreferenceService.getOpenScmDialogByDefault()) {
await this.userPreferenceService.toggleOpenScmDialogByDefault();
}
if (this.form.controls.tableExtractionType.value !== this.#userPreferenceService.getTableExtractionType()) {
await this.#userPreferenceService.save(PreferencesKeys.tableExtractionType, this.form.controls.tableExtractionType.value);
if (this.form.controls.tableExtractionType.value !== this.userPreferenceService.getTableExtractionType()) {
await this.userPreferenceService.save(PreferencesKeys.tableExtractionType, this.form.controls.tableExtractionType.value);
}
if (
this.form.controls.loadAllAnnotationsWarning.value !==
this.#userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)
this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning)
) {
await this.#userPreferenceService.save(
await this.userPreferenceService.save(
PreferencesKeys.loadAllAnnotationsWarning,
String(this.form.controls.loadAllAnnotationsWarning.value),
);
}
if (this.form.controls.helpModeDialog.value !== this.#userPreferenceService.getBool(KEYS.helpModeDialog)) {
await this.#userPreferenceService.save(KEYS.helpModeDialog, String(this.form.controls.helpModeDialog.value));
}
if (this.form.controls.overwriteFileOption.enabled && !this.form.controls.overwriteFileOption.value) {
await this.#userPreferenceService.saveOverwriteFileOption('undefined');
}
await this.#userPreferenceService.reload();
await this.userPreferenceService.reload();
this.#patchValues();
this.initialFormValue = this.form.getRawValue();
this.#changeRef.markForCheck();
this._changeRef.markForCheck();
}
#patchValues() {
this.form.patchValue({
autoExpandFiltersOnActions: this.#userPreferenceService.getAutoExpandFiltersOnActions(),
tableExtractionType: this.#userPreferenceService.getTableExtractionType(),
loadAllAnnotationsWarning: this.#userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning),
helpModeDialog: this.#userPreferenceService.getBool(KEYS.helpModeDialog),
overwriteFileOption: this.#isOverwriteFileOptionActive,
autoExpandFiltersOnActions: this.userPreferenceService.getAutoExpandFiltersOnActions(),
openScmDialogByDefault: this.userPreferenceService.getOpenScmDialogByDefault(),
tableExtractionType: this.userPreferenceService.getTableExtractionType(),
loadAllAnnotationsWarning: this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning),
});
if (!this.#isOverwriteFileOptionActive) {
this.form.controls.overwriteFileOption.disable();
}
}
}

View File

@ -1,8 +1,7 @@
import { Component } from '@angular/core';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
import { BaseDialogComponent } from '@iqser/common-ui';
import { MatDialogRef } from '@angular/material/dialog';
import { AbstractControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { AbstractControl, FormGroup, Validators } from '@angular/forms';
interface FormType {
password: AbstractControl<string>;
@ -10,7 +9,6 @@ interface FormType {
@Component({
templateUrl: './confirm-password-dialog.component.html',
imports: [ReactiveFormsModule, IconButtonComponent, TranslateModule, CircleButtonComponent],
})
export class ConfirmPasswordDialogComponent extends BaseDialogComponent {
constructor(protected readonly _dialogRef: MatDialogRef<ConfirmPasswordDialogComponent>) {

View File

@ -16,37 +16,32 @@
<input formControlName="lastName" name="lastName" type="text" />
</div>
<div class="iqser-input-group">
<div *ngIf="devMode" class="iqser-input-group">
<label [translate]="'top-bar.navigation-items.my-account.children.language.label'"></label>
<mat-form-field>
<mat-select formControlName="language">
<mat-select-trigger>{{ languageSelectLabel() | translate }}</mat-select-trigger>
@for (language of languages; track language) {
<mat-option [value]="language">
{{ translations[language] | translate }}
</mat-option>
}
<mat-option *ngFor="let language of languages" [value]="language">
{{ translations[language] | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="iqser-input-group">
<a (click)="resetPassword()" target="_blank"> {{ 'user-profile-screen.actions.change-password' | translate }}</a>
<a [href]="changePasswordUrl" target="_blank"> {{ 'user-profile-screen.actions.change-password' | translate }}</a>
</div>
@if (devMode) {
<div class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="darkTheme">
{{ 'user-profile-screen.form.dark-theme' | translate }}
</mat-slide-toggle>
</div>
}
<div *ngIf="devMode" class="iqser-input-group">
<mat-slide-toggle color="primary" formControlName="darkTheme">
{{ 'user-profile-screen.form.dark-theme' | translate }}
</mat-slide-toggle>
</div>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
[disabled]="disabled"
[disabled]="form.invalid || !(profileChanged || languageChanged || themeChanged)"
[label]="'user-profile-screen.actions.save' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"

View File

@ -1,64 +1,31 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed } from '@angular/core';
import { FormGroup, ReactiveFormsModule, UntypedFormBuilder, Validators } from '@angular/forms';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DomSanitizer } from '@angular/platform-browser';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
BaseFormComponent,
IconButtonComponent,
IqserPermissionsService,
LanguageService,
LoadingService,
Toaster,
} from '@iqser/common-ui';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { IProfile } from '@red/domain';
import { BaseFormComponent, getConfig, IqserPermissionsService, LanguageService, LoadingService, Toaster } from '@iqser/common-ui';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
import { TranslateService } from '@ngx-translate/core';
import { AppConfig, IProfile } from '@red/domain';
import { languagesTranslations } from '@translations/languages-translations';
import { Roles } from '@users/roles';
import { UserPreferenceService } from '@users/user-preference.service';
import { UserService } from '@users/user.service';
import { firstValueFrom } from 'rxjs';
import { UserProfileDialogService } from '../services/user-profile-dialog.service';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { PdfViewer } from '../../../../pdf-viewer/services/pdf-viewer.service';
import { formControlToSignal } from '@utils/functions';
import { AsControl } from '@common-ui/utils';
interface UserProfileForm {
email: string;
firstName: string;
lastName: string;
language: string;
darkTheme: boolean;
}
@Component({
templateUrl: './user-profile-screen.component.html',
styleUrls: ['./user-profile-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ReactiveFormsModule,
MatFormField,
MatSelect,
MatOption,
TranslateModule,
MatSlideToggle,
IconButtonComponent,
MatSelectTrigger,
],
})
export class UserProfileScreenComponent extends BaseFormComponent {
readonly form: FormGroup<AsControl<UserProfileForm>> = this.#getForm();
initialFormValue = this.form.getRawValue();
export class UserProfileScreenComponent extends BaseFormComponent implements OnInit {
#profileModel: IProfile;
readonly translations = languagesTranslations;
readonly devMode = this._userPreferenceService.isIqserDevMode;
readonly profileKeys = ['email', 'firstName', 'lastName'];
readonly languages = this._translateService.langs;
readonly language = formControlToSignal<UserProfileForm['language']>(this.form.controls.language);
readonly languageSelectLabel = computed(() => this.translations[this.language()]);
readonly changePasswordUrl: string;
constructor(
domSanitizer: DomSanitizer,
private readonly _userService: UserService,
private readonly _loadingService: LoadingService,
private readonly _dialogService: UserProfileDialogService,
@ -69,29 +36,46 @@ export class UserProfileScreenComponent extends BaseFormComponent {
protected readonly _userPreferenceService: UserPreferenceService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _toaster: Toaster,
private readonly _pdfViewer: PdfViewer,
) {
super();
if (!this._permissionsService.has(Roles.updateMyProfile)) {
this.form.disable();
}
this._loadingService.stop();
this._loadingService.start();
const tenant = inject(TenantsService).activeTenantId;
const realmUrl = `${getConfig<AppConfig>().OAUTH_URL}/realms/${tenant}`;
this.changePasswordUrl = `${realmUrl}/account/password`;
}
get languageChanged(): boolean {
return this.initialFormValue['language'] !== this.form.controls.language.value;
return this.#profileModel['language'] !== this.form.get('language').value;
}
get themeChanged(): boolean {
return this.initialFormValue['darkTheme'] !== this.form.controls.darkTheme.value;
return this.#profileModel['darkTheme'] !== this.form.get('darkTheme').value;
}
get emailChanged(): boolean {
return this.initialFormValue['email'] !== this.form.controls.email.value;
return this.#profileModel['email'] !== this.form.get('email').value;
}
get profileChanged(): boolean {
return this.profileKeys.some(key => this.initialFormValue[key] !== this.form.get(key).value);
const keys = Object.keys(this.form.getRawValue());
keys.splice(keys.indexOf('language'), 1);
keys.splice(keys.indexOf('darkTheme'), 1);
for (const key of keys) {
if (this.#profileModel[key] !== this.form.get(key).value) {
return true;
}
}
return false;
}
get languages(): string[] {
return this._translateService.langs;
}
ngOnInit() {
this._initializeForm();
}
async save(): Promise<void> {
@ -116,34 +100,50 @@ export class UserProfileScreenComponent extends BaseFormComponent {
}
if (this.languageChanged) {
await this._languageService.change(this.form.controls.language.value);
await this._pdfViewer.instance?.UI.setLanguage(this._languageService.currentLanguage);
await this._languageService.change(this.form.get('language').value);
}
if (this.themeChanged) {
await this._userPreferenceService.saveTheme(this.form.controls.darkTheme.value ? 'dark' : 'light');
await this._userPreferenceService.saveTheme(this.form.get('darkTheme').value ? 'dark' : 'light');
}
this.initialFormValue = this.form.getRawValue();
this._changeRef.markForCheck();
this._loadingService.stop();
this._initializeForm();
this._toaster.success(_('user-profile-screen.update.success'));
} catch (e) {
this._loadingService.stop();
}
}
async resetPassword() {
await this._userService.createResetPasswordAction();
}
#getForm() {
private _getForm(): UntypedFormGroup {
return this._formBuilder.group({
email: [this._userService.currentUser.email ?? '', [Validators.required, Validators.email]],
firstName: [this._userService.currentUser.firstName ?? ''],
lastName: [this._userService.currentUser.lastName ?? ''],
language: [this._userPreferenceService.getLanguage()],
darkTheme: [this._userPreferenceService.getTheme() === 'dark'],
email: ['', [Validators.required, Validators.email]],
firstName: [''],
lastName: [''],
language: [''],
darkTheme: [false],
});
}
private _initializeForm(): void {
try {
this.form = this._getForm();
if (!this._permissionsService.has(Roles.updateMyProfile)) {
this.form.disable();
}
this.#profileModel = {
email: this._userService.currentUser.email ?? '',
firstName: this._userService.currentUser.firstName ?? '',
lastName: this._userService.currentUser.lastName ?? '',
language: this._languageService.currentLanguage ?? '',
darkTheme: this._userPreferenceService.getTheme() === 'dark',
};
this.form.patchValue(this.#profileModel, { emitEvent: false });
this.initialFormValue = this.form.getRawValue();
} catch (e) {
} finally {
this._loadingService.stop();
this._changeRef.markForCheck();
}
}
}

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { TranslateModule } from '@ngx-translate/core';
import { ConfirmPasswordDialogComponent } from './confirm-password-dialog/confirm-password-dialog.component';
import { UserProfileDialogService } from './services/user-profile-dialog.service';
import { CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
const routes = [{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [UserProfileScreenComponent, ConfirmPasswordDialogComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, TranslateModule, IconButtonComponent, CircleButtonComponent],
providers: [UserProfileDialogService],
})
export class UserProfileModule {}

View File

@ -1,8 +0,0 @@
import { UserProfileScreenComponent } from './user-profile-screen/user-profile-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { UserProfileDialogService } from './services/user-profile-dialog.service';
import { IqserRoutes } from '@iqser/common-ui';
export default [
{ path: '', component: UserProfileScreenComponent, canDeactivate: [PendingChangesGuard], providers: [UserProfileDialogService] },
] satisfies IqserRoutes;

View File

@ -1,95 +0,0 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { addHintTranslations } from '@translations/add-hint-translations';
import { redactTextTranslations } from '@translations/redact-text-translations';
import { removeRedactionTranslations } from '@translations/remove-redaction-translations';
import {
ForceAnnotationOptions,
RectangleRedactOptions,
RedactOrHintOptions,
RemoveRedactionOptions,
} from '../../file-preview/utils/dialog-types';
export const SystemDefaults = {
RECTANGLE_REDACT_DEFAULT: RectangleRedactOptions.ONLY_THIS_PAGE,
ADD_REDACTION_DEFAULT: RedactOrHintOptions.IN_DOSSIER,
ADD_HINT_DEFAULT: RedactOrHintOptions.IN_DOSSIER,
FORCE_REDACTION_DEFAULT: ForceAnnotationOptions.ONLY_HERE,
REMOVE_REDACTION_DEFAULT: RemoveRedactionOptions.ONLY_HERE,
REMOVE_HINT_DEFAULT: RemoveRedactionOptions.ONLY_HERE,
REMOVE_RECOMMENDATION_DEFAULT: RemoveRedactionOptions.DO_NOT_RECOMMEND,
} as const;
export const SystemDefaultOption = {
SYSTEM_DEFAULT: 'SYSTEM_DEFAULT',
label: _('dialog-defaults-form.system-default'),
} as const;
export type SystemDefaultType = typeof SystemDefaultOption.SYSTEM_DEFAULT;
export const redactionAddOptions = [
{
label: SystemDefaultOption.label,
value: SystemDefaultOption.SYSTEM_DEFAULT,
},
{
label: redactTextTranslations.onlyHere.label,
value: RedactOrHintOptions.ONLY_HERE,
},
{
label: redactTextTranslations.inDocument.label,
value: RedactOrHintOptions.IN_DOCUMENT,
},
{
label: redactTextTranslations.inDossier.label,
value: RedactOrHintOptions.IN_DOSSIER,
},
];
export const hintAddOptions = [
{
label: SystemDefaultOption.label,
value: SystemDefaultOption.SYSTEM_DEFAULT,
},
{
label: addHintTranslations.onlyHere.label,
value: RedactOrHintOptions.ONLY_HERE,
},
{
label: addHintTranslations.inDossier.label,
value: RedactOrHintOptions.IN_DOSSIER,
},
];
export const removeOptions = [
{
label: SystemDefaultOption.label,
value: SystemDefaultOption.SYSTEM_DEFAULT,
},
{
label: removeRedactionTranslations.ONLY_HERE.label,
value: RemoveRedactionOptions.ONLY_HERE,
},
{
label: removeRedactionTranslations.IN_DOSSIER.label,
value: RemoveRedactionOptions.IN_DOSSIER,
},
];
export const redactionRemoveOptions = [
...removeOptions,
{
label: removeRedactionTranslations.FALSE_POSITIVE.label,
value: RemoveRedactionOptions.FALSE_POSITIVE,
},
];
export const recommendationRemoveOptions = [
{
label: SystemDefaultOption.label,
value: SystemDefaultOption.SYSTEM_DEFAULT,
},
{
label: removeRedactionTranslations.DO_NOT_RECOMMEND.label,
value: RemoveRedactionOptions.DO_NOT_RECOMMEND,
},
];

View File

@ -0,0 +1,282 @@
import { NgModule } from '@angular/core';
import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui';
import { RedRoleGuard } from '@users/red-role.guard';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { RouterModule } from '@angular/router';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard';
import { entityExistsGuard } from '@guards/entity-exists-guard.service';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { PermissionsGuard } from '@guards/permissions-guard';
import { Roles } from '@users/roles';
import { IqserAuthGuard } from '@iqser/common-ui/lib/users';
const dossierTemplateIdRoutes: IqserRoutes = [
{
path: 'info',
component: BaseDossierTemplateScreenComponent,
loadChildren: () => import('./screens/info/dossier-template-info.module').then(m => m.DossierTemplateInfoModule),
},
{
path: 'entities',
children: [
{
path: '',
component: EntitiesListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: `:${ENTITY_TYPE}`,
component: BaseEntityScreenComponent,
canActivate: [CompositeRouteGuard, entityExistsGuard()],
loadChildren: () => import('./screens/entities/entities.module').then(m => m.EntitiesModule),
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
],
},
{
path: 'entity-rules',
component: BaseDossierTemplateScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.rules.read],
redirectTo: 'info',
},
type: 'ENTITY',
},
loadChildren: () => import('./screens/rules/rules.module').then(m => m.RulesModule),
},
{
path: 'component-rules',
component: BaseDossierTemplateScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.rules.read],
redirectTo: 'info',
},
type: 'COMPONENT',
},
loadChildren: () => import('./screens/rules/rules.module').then(m => m.RulesModule),
},
{
path: 'file-attributes',
component: BaseDossierTemplateScreenComponent,
loadChildren: () =>
import('./screens/file-attributes-listing/file-attributes-listing.module').then(m => m.FileAttributesListingModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'watermarks',
component: BaseDossierTemplateScreenComponent,
loadChildren: () => import('./screens/watermark/watermark.module').then(m => m.WatermarkModule),
},
{
path: 'reports',
component: BaseDossierTemplateScreenComponent,
loadChildren: () => import('./screens/reports/reports.module').then(m => m.ReportsModule),
canActivate: [IqserAuthGuard, IqserPermissionsGuard],
data: {
permissions: {
allow: [Roles.reportTemplates.read],
redirectTo: '/auth-error',
},
},
},
{
path: 'dossier-attributes',
component: BaseDossierTemplateScreenComponent,
loadChildren: () =>
import('./screens/dossier-attributes-listing/dossier-attributes-listing.module').then(m => m.DossierAttributesListingModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'dossier-states',
component: BaseDossierTemplateScreenComponent,
loadChildren: () =>
import('./screens/dossier-states-listing/dossier-states-listing.module').then(m => m.DossierStatesListingModule),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'default-colors',
component: DefaultColorsScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'justifications',
component: BaseDossierTemplateScreenComponent,
loadChildren: () => import('./screens/justifications/justifications.module').then(m => m.JustificationsModule),
canActivate: [IqserPermissionsGuard],
data: {
permissions: {
allow: [Roles.legalBasis.read],
redirectTo: '/auth-error',
},
},
},
{ path: '', redirectTo: 'info', pathMatch: 'full' },
];
const dossierTemplatesRoutes: IqserRoutes = [
{
path: '',
component: BaseAdminScreenComponent,
canActivate: [IqserAuthGuard],
loadChildren: () =>
import('./screens/dossier-templates-listing/dossier-templates-listing.module').then(m => m.DossierTemplatesListingModule),
},
{
path: `:${DOSSIER_TEMPLATE_ID}`,
children: dossierTemplateIdRoutes,
canActivate: [templateExistsWhenEnteringAdmin()],
},
];
const routes: IqserRoutes = [
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
{
path: 'dossier-templates',
children: dossierTemplatesRoutes,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossierTemplatesGuard],
requiredRoles: ['RED_MANAGER', 'RED_ADMIN'],
permissions: {
allow: [Roles.templates.read],
redirectTo: '/',
},
},
},
{
path: 'users',
component: BaseAdminScreenComponent,
children: [
{
path: '',
component: UserListingScreenComponent,
},
],
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.users.read, 'RED_USER_ADMIN'],
redirectTo: '/',
},
},
},
{
path: 'dossier-permissions',
component: BaseAdminScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, PermissionsGuard],
permissionsObject: 'Dossier',
permissions: {
allow: [Roles.manageAclPermissions, 'RED_ADMIN'],
redirectTo: '/',
},
},
loadChildren: () => import('./screens/permissions/permissions.module').then(m => m.PermissionsModule),
},
{
path: 'license-info',
component: BaseAdminScreenComponent,
canActivate: [IqserAuthGuard, IqserPermissionsGuard, RedRoleGuard],
data: {
permissions: {
allow: [Roles.license.readReport, 'RED_ADMIN'],
redirectTo: '/',
},
},
loadChildren: () => import('./screens/license/license.module').then(m => m.LicenseModule),
},
{
path: 'digital-signature',
component: BaseAdminScreenComponent,
children: [
{
path: '',
component: DigitalSignatureScreenComponent,
},
],
canActivate: [IqserAuthGuard, IqserPermissionsGuard, RedRoleGuard],
data: {
permissions: {
allow: [Roles.digitalSignature.read, 'RED_ADMIN'],
redirectTo: '/',
},
},
},
{
path: 'audit',
component: BaseAdminScreenComponent,
children: [
{
path: '',
component: AuditScreenComponent,
},
],
canActivate: [IqserAuthGuard, IqserPermissionsGuard, RedRoleGuard],
data: {
permissions: {
allow: [Roles.searchAudit, 'RED_ADMIN'],
redirectTo: '/',
},
},
},
{
path: 'general-config',
component: BaseAdminScreenComponent,
children: [
{
path: '',
component: GeneralConfigScreenComponent,
},
],
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.generalConfiguration.read, Roles.smtp.read, 'RED_ADMIN'],
redirectTo: '/',
},
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AdminRoutingModule {}

View File

@ -0,0 +1,130 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '@shared/shared.module';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { DossierTemplateBreadcrumbsComponent } from './shared/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { AddEditCloneDossierTemplateDialogComponent } from './dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component';
import { AddEntityDialogComponent } from './dialogs/add-entity-dialog/add-entity-dialog.component';
import { EditColorDialogComponent } from './dialogs/edit-color-dialog/edit-color-dialog.component';
import { AdminDialogService } from './services/admin-dialog.service';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component';
import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component';
import { UsersStatsComponent } from './components/users-stats/users-stats.component';
import { ResetPasswordComponent } from './dialogs/add-edit-user-dialog/reset-password/reset-password.component';
import { UserDetailsComponent } from './dialogs/add-edit-user-dialog/user-details/user-details.component';
import { AuditService } from './services/audit.service';
import { DigitalSignatureService } from './services/digital-signature.service';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { RulesService } from './services/rules.service';
import { SmtpConfigService } from './services/smtp-config.service';
import { UploadDictionaryDialogComponent } from './dialogs/upload-dictionary-dialog/upload-dictionary-dialog.component';
import { GeneralConfigFormComponent } from './screens/general-config/general-config-form/general-config-form.component';
import { SmtpFormComponent } from './screens/general-config/smtp-form/smtp-form.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
import { A11yModule } from '@angular/cdk/a11y';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { AdminSideNavComponent } from './shared/components/admin-side-nav/admin-side-nav.component';
import { SystemPreferencesFormComponent } from './screens/general-config/system-preferences-form/system-preferences-form.component';
import { ConfigureCertificateDialogComponent } from './dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
import { PkcsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component';
import {
ChevronButtonComponent,
CircleButtonComponent,
DetailsRadioComponent,
EditableInputComponent,
EmptyStateComponent,
HasScrollbarDirective,
HumanizePipe,
IconButtonComponent,
InputWithActionComponent,
IqserAllowDirective,
IqserDenyDirective,
IqserHelpModeModule,
IqserListingModule,
IqserUploadFileModule,
RoundCheckboxComponent,
} from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { AuditInfoDialogComponent } from './dialogs/audit-info-dialog/audit-info-dialog.component';
import { DossierTemplateActionsComponent } from './shared/components/dossier-template-actions/dossier-template-actions.component';
import { IqserUsersModule } from '@iqser/common-ui/lib/users';
import { TenantPipe } from '@iqser/common-ui/lib/tenants';
import { SelectComponent } from '@shared/components/select/select.component';
const dialogs = [
AddEditCloneDossierTemplateDialogComponent,
AddEntityDialogComponent,
EditColorDialogComponent,
SmtpAuthDialogComponent,
AddEditUserDialogComponent,
UploadDictionaryDialogComponent,
ConfigureCertificateDialogComponent,
AuditInfoDialogComponent,
];
const screens = [
AuditScreenComponent,
DefaultColorsScreenComponent,
EntitiesListingScreenComponent,
DigitalSignatureScreenComponent,
UserListingScreenComponent,
GeneralConfigScreenComponent,
];
const components = [
UsersStatsComponent,
ResetPasswordComponent,
UserDetailsComponent,
BaseAdminScreenComponent,
BaseDossierTemplateScreenComponent,
BaseEntityScreenComponent,
GeneralConfigFormComponent,
SmtpFormComponent,
SystemPreferencesFormComponent,
PkcsSignatureConfigurationComponent,
KmsSignatureConfigurationComponent,
...dialogs,
...screens,
];
@NgModule({
declarations: [...components],
providers: [AdminDialogService, AuditService, DigitalSignatureService, RulesService, SmtpConfigService],
imports: [
CommonModule,
SharedModule,
AdminRoutingModule,
A11yModule,
IqserUsersModule,
TranslateModule,
HumanizePipe,
IqserListingModule,
IqserUploadFileModule,
IqserHelpModeModule,
AdminSideNavComponent,
DossierTemplateActionsComponent,
DossierTemplateBreadcrumbsComponent,
IconButtonComponent,
CircleButtonComponent,
ChevronButtonComponent,
EmptyStateComponent,
HasScrollbarDirective,
RoundCheckboxComponent,
InputWithActionComponent,
EditableInputComponent,
DetailsRadioComponent,
IqserAllowDirective,
IqserDenyDirective,
TenantPipe,
SelectComponent,
],
})
export class AdminModule {}

View File

@ -1,302 +0,0 @@
import { inject, provideEnvironmentInitializer } from '@angular/core';
import { PendingChangesGuard } from '@guards/can-deactivate.guard';
import { templateExistsWhenEnteringAdmin } from '@guards/dossier-template-exists.guard';
import { DossierTemplatesGuard } from '@guards/dossier-templates.guard';
import { entityExistsGuard } from '@guards/entity-exists-guard.service';
import { PermissionsGuard } from '@guards/permissions-guard';
import { CompositeRouteGuard, IqserPermissionsGuard, IqserRoutes } from '@iqser/common-ui';
import { IqserAuthGuard } from '@iqser/common-ui/lib/users';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { CopilotService } from '@services/copilot.service';
import { RedRoleGuard } from '@users/red-role.guard';
import { Roles } from '@users/roles';
import { BaseAdminScreenComponent } from './base-admin-screen/base-admin-screen.component';
import { BaseDossierTemplateScreenComponent } from './base-dossier-templates-screen/base-dossier-template-screen.component';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { AuditScreenComponent } from './screens/audit/audit-screen.component';
import { DefaultColorsScreenComponent } from './screens/default-colors/default-colors-screen.component';
import { DigitalSignatureScreenComponent } from './screens/digital-signature/digital-signature-screen.component';
import { EntitiesListingScreenComponent } from './screens/entities-listing/entities-listing-screen.component';
import { GeneralConfigScreenComponent } from './screens/general-config/general-config-screen.component';
import { UserListingScreenComponent } from './screens/user-listing/user-listing-screen.component';
import { AdminDialogService } from './services/admin-dialog.service';
import { AuditService } from './services/audit.service';
import { DigitalSignatureService } from './services/digital-signature.service';
import { RulesService } from './services/rules.service';
import { SmtpConfigService } from './services/smtp-config.service';
const entityRoutes: IqserRoutes = [
{
path: '',
component: BaseDossierTemplateScreenComponent,
children: [{ path: '', component: EntitiesListingScreenComponent }],
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: `:${ENTITY_TYPE}`,
component: BaseEntityScreenComponent,
canActivate: [CompositeRouteGuard, entityExistsGuard()],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
loadChildren: () => import('./screens/entities/entities.routes'),
},
];
const dossierTemplateIdRoutes: IqserRoutes = [
{ path: '', redirectTo: 'info', pathMatch: 'full' },
{
path: 'info',
component: BaseDossierTemplateScreenComponent,
children: [
{
path: '',
canDeactivate: [PendingChangesGuard],
loadComponent: () => import('./screens/info/dossier-template-info-screen/dossier-template-info-screen.component'),
},
],
},
{
path: 'entities',
children: entityRoutes,
},
{
path: '',
component: BaseDossierTemplateScreenComponent,
children: [
{
path: 'entity-rules',
loadComponent: () => import('./screens/rules/rules-screen/rules-screen.component'),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.rules.read],
redirectTo: 'info',
},
type: 'ENTITY',
},
providers: [
RulesService,
provideEnvironmentInitializer(() => {
return inject(CopilotService).connectAsync('/api/llm/llm-websocket');
}),
],
},
{
path: 'component-rules',
loadComponent: () => import('./screens/rules/rules-screen/rules-screen.component'),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.rules.read],
redirectTo: 'info',
},
type: 'COMPONENT',
},
providers: [RulesService],
},
{
path: 'component-mappings',
loadComponent: () => import('./screens/component-mappings/component-mappings-screen.component'),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'components',
loadComponent: () => import('./screens/component-definitions/component-definitions.component'),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'file-attributes',
loadComponent: () => import('./screens/file-attributes-listing/file-attributes-listing-screen.component'),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'watermarks',
loadChildren: () => import('./screens/watermark/watermark.routes'),
},
{
path: 'reports',
loadComponent: () => import('./screens/reports/reports-screen/reports-screen.component'),
canActivate: [IqserAuthGuard, IqserPermissionsGuard],
data: {
permissions: {
allow: [Roles.reportTemplates.read],
redirectTo: '/auth-error',
},
},
},
{
path: 'dossier-attributes',
loadComponent: () => import('./screens/dossier-attributes-listing/dossier-attributes-listing-screen.component'),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'dossier-states',
loadComponent: () =>
import('./screens/dossier-states-listing/dossier-states-listing-screen/dossier-states-listing-screen.component'),
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'default-colors',
component: DefaultColorsScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
},
},
{
path: 'justifications',
loadComponent: () => import('./screens/justifications/justifications-screen/justifications-screen.component'),
canActivate: [IqserPermissionsGuard],
data: {
permissions: {
allow: [Roles.legalBasis.read],
redirectTo: '/auth-error',
},
},
},
],
},
];
const dossierTemplatesRoutes: IqserRoutes = [
{
path: '',
component: BaseAdminScreenComponent,
children: [
{
path: '',
loadComponent: () =>
import(
'./screens/dossier-templates-listing/dossier-templates-listing-screen/dossier-templates-listing-screen.component'
),
},
],
canActivate: [IqserAuthGuard],
},
{
path: `:${DOSSIER_TEMPLATE_ID}`,
children: dossierTemplateIdRoutes,
canActivate: [templateExistsWhenEnteringAdmin()],
},
];
export default [
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
{
path: 'dossier-templates',
children: dossierTemplatesRoutes,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, DossierTemplatesGuard],
requiredRoles: ['RED_MANAGER', 'RED_ADMIN'],
permissions: {
allow: [Roles.templates.read],
redirectTo: '/',
},
},
},
{
path: '',
component: BaseAdminScreenComponent,
providers: [AdminDialogService],
children: [
{
path: 'users',
component: UserListingScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.users.read, 'RED_USER_ADMIN'],
redirectTo: '/',
},
},
},
{
path: 'dossier-permissions',
loadComponent: () => import('./screens/permissions/permissions-screen/permissions-screen.component'),
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard, PermissionsGuard],
permissionsObject: 'Dossier',
permissions: {
allow: [Roles.manageAclPermissions, 'RED_ADMIN'],
redirectTo: '/',
},
},
},
{
path: 'license-info',
loadChildren: () => import('./screens/license/license.routes'),
canActivate: [IqserAuthGuard, IqserPermissionsGuard, RedRoleGuard],
data: {
permissions: {
allow: [Roles.license.readReport, 'RED_ADMIN'],
redirectTo: '/',
},
},
},
{
path: 'digital-signature',
component: DigitalSignatureScreenComponent,
canActivate: [IqserAuthGuard, IqserPermissionsGuard, RedRoleGuard],
data: {
permissions: {
allow: [Roles.digitalSignature.read, 'RED_ADMIN'],
redirectTo: '/',
},
},
providers: [DigitalSignatureService],
},
{
path: 'audit',
component: AuditScreenComponent,
canActivate: [IqserAuthGuard, IqserPermissionsGuard, RedRoleGuard],
data: {
permissions: {
allow: [Roles.searchAudit, 'RED_ADMIN'],
redirectTo: '/',
},
},
providers: [AuditService],
},
{
path: 'general-config',
component: GeneralConfigScreenComponent,
canActivate: [CompositeRouteGuard, IqserPermissionsGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [IqserAuthGuard, RedRoleGuard],
permissions: {
allow: [Roles.generalConfiguration.read, Roles.smtp.read, 'RED_ADMIN'],
redirectTo: '/',
},
},
providers: [SmtpConfigService],
},
],
},
] satisfies IqserRoutes;

View File

@ -1,10 +1,7 @@
import { Component } from '@angular/core';
import { AdminSideNavComponent } from '../shared/components/admin-side-nav/admin-side-nav.component';
import { RouterOutlet } from '@angular/router';
@Component({
templateUrl: './base-admin-screen.component.html',
styleUrls: ['./base-admin-screen.component.scss'],
imports: [AdminSideNavComponent, RouterOutlet],
})
export class BaseAdminScreenComponent {}

View File

@ -1,3 +1,5 @@
<!--TODO: Use this for all dossier template screens -->
<section>
<div class="page-header">
<redaction-dossier-template-breadcrumbs class="flex-1"></redaction-dossier-template-breadcrumbs>

View File

@ -1,22 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DossierTemplateBreadcrumbsComponent } from '../shared/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { DossierTemplateActionsComponent } from '../shared/components/dossier-template-actions/dossier-template-actions.component';
import { CircleButtonComponent } from '@iqser/common-ui';
import { RouterLink, RouterOutlet } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { AdminSideNavComponent } from '../shared/components/admin-side-nav/admin-side-nav.component';
@Component({
templateUrl: './base-dossier-template-screen.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DossierTemplateBreadcrumbsComponent,
DossierTemplateActionsComponent,
CircleButtonComponent,
RouterLink,
TranslateModule,
AdminSideNavComponent,
RouterOutlet,
],
})
export class BaseDossierTemplateScreenComponent {}

View File

@ -1,39 +1,26 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { DOSSIER_TEMPLATE_ID, ENTITY_TYPE } from '@red/domain';
import { Router, RouterLink, RouterOutlet } from '@angular/router';
import { Router } from '@angular/router';
import { firstValueFrom, Observable } from 'rxjs';
import { AdminDialogService } from '../services/admin-dialog.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { CircleButtonComponent, LoadingService } from '@iqser/common-ui';
import { LoadingService } from '@iqser/common-ui';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { map } from 'rxjs/operators';
import { PermissionsService } from '@services/permissions.service';
import { getParam } from '@iqser/common-ui/lib/utils';
import { DossierTemplateBreadcrumbsComponent } from '../shared/components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { AsyncPipe, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { AdminSideNavComponent } from '../shared/components/admin-side-nav/admin-side-nav.component';
import { TenantsService } from '@iqser/common-ui/lib/tenants';
@Component({
templateUrl: './base-entity-screen.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DossierTemplateBreadcrumbsComponent,
CircleButtonComponent,
NgIf,
AsyncPipe,
TranslateModule,
RouterLink,
AdminSideNavComponent,
RouterOutlet,
],
})
export class BaseEntityScreenComponent implements OnInit {
readonly disabledItems$: Observable<string[]>;
readonly canDeleteEntity$: Observable<boolean>;
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
readonly #entityType = getParam(ENTITY_TYPE);
readonly disabledItems$: Observable<string[]>;
readonly canDeleteEntity$: Observable<boolean>;
constructor(
private readonly _router: Router,
@ -42,6 +29,7 @@ export class BaseEntityScreenComponent implements OnInit {
private readonly _dialogService: AdminDialogService,
private readonly _dictionaryService: DictionaryService,
private readonly _permissionsService: PermissionsService,
private readonly _tenantsService: TenantsService,
private readonly _dossierTemplatesService: DossierTemplatesService,
) {
const entity$ = dictionaryMapService.watch$(this.#dossierTemplateId, this.#entityType);
@ -62,7 +50,7 @@ export class BaseEntityScreenComponent implements OnInit {
this._loadingService.start();
const dossierTemplate = this._dossierTemplatesService.find(this.#dossierTemplateId);
await firstValueFrom(this._dictionaryService.deleteDictionaries([this.#entityType], this.#dossierTemplateId));
await this._router.navigate([`/${dossierTemplate.routerLink}/entities`]);
await this._router.navigate([`/${this._tenantsService.activeTenantId}/${dossierTemplate.routerLink}/entities`]);
this._loadingService.stop();
});
}

View File

@ -23,7 +23,7 @@
<div class="mt-44">
<redaction-donut-chart
[config]="chartConfig()"
[config]="chartConfig"
[radius]="63"
[strokeWidth]="15"
[subtitles]="['user-stats.chart.users' | translate]"

View File

@ -1,16 +1,12 @@
import { Component, EventEmitter, input, Input, output, Output } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DonutChartConfig } from '@red/domain';
import { CircleButtonComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
@Component({
selector: 'redaction-users-stats',
templateUrl: './users-stats.component.html',
styleUrls: ['./users-stats.component.scss'],
imports: [CircleButtonComponent, TranslateModule, DonutChartComponent],
})
export class UsersStatsComponent {
readonly chartConfig = input.required<DonutChartConfig[]>();
readonly toggleCollapse = output();
@Output() toggleCollapse = new EventEmitter();
@Input() chartConfig: DonutChartConfig[];
}

View File

@ -1,48 +0,0 @@
<section class="dialog">
<div [innerHTML]="'add-clone-dossier-template.title' | translate: translateParams" class="dialog-header heading-l"></div>
<form [formGroup]="form">
<div class="dialog-content">
<div class="iqser-input-group required w-300">
<label [translate]="'add-edit-clone-dossier-template.form.name'"></label>
<input
[placeholder]="'add-edit-clone-dossier-template.form.name-placeholder' | translate"
formControlName="name"
name="name"
type="text"
/>
</div>
<div class="iqser-input-group w-400">
<label [translate]="'add-edit-clone-dossier-template.form.description'"></label>
<textarea
[placeholder]="'add-edit-clone-dossier-template.form.description-placeholder' | translate"
formControlName="description"
name="description"
rows="4"
type="text"
></textarea>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="save()"
[buttonId]="'saveButton'"
[disabled]="disabled"
[label]="'add-clone-dossier-template.save' | translate: translateParams"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<iqser-icon-button
(action)="save({ nextAction: true })"
[buttonId]="'saveButton'"
[disabled]="disabled"
[label]="'add-clone-dossier-template.save-and-edit' | translate"
[type]="iconButtonTypes.dark"
></iqser-icon-button>
</div>
</form>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,103 +0,0 @@
import { HttpStatusCode } from '@angular/common/http';
import { Component, Inject } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { BaseDialogComponent, CircleButtonComponent, getConfig, IconButtonComponent, SaveOptions } from '@iqser/common-ui';
import { DossierTemplate } from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
export interface CloneTemplateData {
dossierTemplateId?: string;
}
@Component({
templateUrl: './add-clone-dossier-template-dialog.component.html',
styleUrls: ['./add-clone-dossier-template-dialog.component.scss'],
imports: [TranslateModule, ReactiveFormsModule, IconButtonComponent, CircleButtonComponent],
})
export class AddCloneDossierTemplateDialogComponent extends BaseDialogComponent {
readonly dossierTemplate?: DossierTemplate;
readonly isDocumine = getConfig().IS_DOCUMINE;
readonly translateParams: { type: string; dossierTemplateName?: string };
constructor(
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _router: Router,
protected readonly _dialogRef: MatDialogRef<AddCloneDossierTemplateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: CloneTemplateData,
) {
super(_dialogRef);
this.dossierTemplate = this._dossierTemplatesService.find(this.data.dossierTemplateId);
this.translateParams = {
type: this.dossierTemplate ? 'clone' : 'create',
dossierTemplateName: this.dossierTemplate?.name,
};
this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue();
}
override get disabled(): boolean {
// Ignore 'changed' value, doesn't make sense in this context
return !this.valid || this._hasErrors();
}
async save(options?: SaveOptions): Promise<void> {
let dossierTemplate: DossierTemplate;
this._loadingService.start();
const body = {
...this.dossierTemplate,
...this.form.getRawValue(),
};
try {
if (this.dossierTemplate) {
dossierTemplate = await this._dossierTemplatesService.clone(this.dossierTemplate.id, body);
} else {
dossierTemplate = await this._dossierTemplatesService.createOrUpdate(body);
}
if (options?.nextAction) {
await this._router.navigate([dossierTemplate.routerLink]);
}
this._dialogRef.close(true);
} catch (error) {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('add-edit-clone-dossier-template.error.conflict'), { error });
} else {
this._toaster.rawError(error.error.message);
}
}
this._loadingService.stop();
}
#getForm() {
return this._formBuilder.group({
name: [this.dossierTemplate ? this.#getCloneName(this.dossierTemplate) : undefined, Validators.required],
description: [this.dossierTemplate?.description],
});
}
#getCloneName(initialTemplate: DossierTemplate): string {
const templateName = initialTemplate.name.trim();
let nameOfClonedTemplate: string = templateName.split('Copy of ').filter(n => n)[0];
nameOfClonedTemplate = nameOfClonedTemplate.split(/\(\s*\d+\s*\)$/)[0].trim();
const allTemplatesNames = this._dossierTemplatesService.all.map(t => t.name);
let clonesCount = 0;
for (const name of allTemplatesNames) {
const splitName = name.split(nameOfClonedTemplate);
const suffixRegExp = new RegExp(/^\(\s*\d+\s*\)$/);
if (splitName[0] === 'Copy of ' && (splitName[1].trim().match(suffixRegExp) || splitName[1] === '')) {
clonesCount++;
}
}
if (clonesCount >= 1) {
return `Copy of ${nameOfClonedTemplate} (${clonesCount})`;
}
return `Copy of ${nameOfClonedTemplate}`;
}
}

View File

@ -1,10 +1,8 @@
<div class="content-container" iqserHasScrollbar>
<form [formGroup]="form" class="dialog">
<section class="dialog">
<div [innerHTML]="'add-edit-clone-dossier-template.title' | translate : translateParams" class="dialog-header heading-l"></div>
<form [formGroup]="form">
<div class="dialog-content">
<redaction-dossier-template-details [dossierTemplateId]="dossierTemplateId"></redaction-dossier-template-details>
<div class="heading-md mt-24 mb-8" translate="dossier-template-info-screen.title"></div>
<div class="iqser-input-group required w-300">
<label [translate]="'add-edit-clone-dossier-template.form.name'"></label>
<input
@ -15,7 +13,7 @@
/>
</div>
<div class="iqser-input-group w-full">
<div class="iqser-input-group w-400">
<label [translate]="'add-edit-clone-dossier-template.form.description'"></label>
<textarea
[placeholder]="'add-edit-clone-dossier-template.form.description-placeholder' | translate"
@ -26,14 +24,20 @@
></textarea>
</div>
<div class="validity mt-12">
<div class="validity">
<div>
<mat-checkbox (change)="toggleHasValid('from')" [checked]="hasValidFrom()" color="primary">
<mat-checkbox (change)="toggleHasValid('from')" [checked]="hasValidFrom" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-clone-dossier-template.form.valid-from' | translate }}
</mat-checkbox>
<mat-checkbox (change)="toggleHasValid('to')" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-clone-dossier-template.form.valid-to' | translate }}
</mat-checkbox>
</div>
<div>
<div class="iqser-input-group datepicker-wrapper">
@if (hasValidFrom()) {
<ng-container *ngIf="hasValidFrom">
<input
(dateChange)="applyValidityIntervalConstraints()"
[matDatepicker]="fromPicker"
@ -44,16 +48,11 @@
<mat-icon matDatepickerToggleIcon svgIcon="iqser:calendar"></mat-icon>
</mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
}
</ng-container>
</div>
</div>
<div>
<mat-checkbox (change)="toggleHasValid('to')" [checked]="hasValidTo()" color="primary">
{{ 'add-edit-clone-dossier-template.form.valid-to' | translate }}
</mat-checkbox>
<div class="iqser-input-group datepicker-wrapper">
@if (hasValidTo()) {
<ng-container *ngIf="hasValidTo">
<input
(dateChange)="applyValidityIntervalConstraints()"
[matDatepicker]="toPicker"
@ -64,43 +63,45 @@
<mat-icon matDatepickerToggleIcon svgIcon="iqser:calendar"></mat-icon>
</mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
}
</ng-container>
</div>
</div>
</div>
@if (!isDocumine) {
<div class="mt-24">
<div class="heading">
{{ 'add-edit-clone-dossier-template.form.apply-updates-default.heading' | translate }}
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="applyDictionaryUpdatesToAllDossiersByDefault">
{{ 'add-edit-clone-dossier-template.form.apply-updates-default.description' | translate }}
</mat-checkbox>
<div *ngIf="!isDocumine">
<p class="heading download-includes">
{{ 'add-edit-clone-dossier-template.form.apply-updates-default.heading' | translate }}
</p>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="applyDictionaryUpdatesToAllDossiersByDefault">
{{ 'add-edit-clone-dossier-template.form.apply-updates-default.description' | translate }}
</mat-checkbox>
</div>
</div>
<div *ngIf="!isDocumine" class="flex">
<div class="half-flex-basis">
<p class="heading download-includes">{{ 'download-includes' | translate }}</p>
<div class="flex">
<redaction-select
[label]="
'download-type.label'
| translate
: {
length: form.controls['downloadFileTypes'].value.length
}
"
[options]="downloadTypes"
formControlName="downloadFileTypes"
></redaction-select>
</div>
</div>
<div class="mt-24">
<div class="heading mb-14">{{ 'download-includes' | translate }}</div>
<redaction-select
[label]="
'download-type.label'
| translate
: {
length: form.controls['downloadFileTypes'].value.length,
}
"
[options]="downloadTypes"
formControlName="downloadFileTypes"
></redaction-select>
</div>
<div class="mt-24">
<div class="heading">
<div class="half-flex-basis w-full pl-75">
<p class="heading download-includes">
{{ 'add-edit-clone-dossier-template.form.upload-settings.heading' | translate }}
</div>
</p>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="ocrByDefault">
{{ 'add-edit-clone-dossier-template.form.upload-settings.ocr-by-default' | translate }}
@ -112,41 +113,49 @@
</mat-checkbox>
</div>
</div>
</div>
<div class="mt-24 hidden-elements">
<div class="heading">{{ 'add-edit-clone-dossier-template.form.hidden-text.heading' | translate }}</div>
<div *ngIf="!isDocumine">
<p class="heading download-includes">{{ 'add-edit-clone-dossier-template.form.hidden-text.heading' | translate }}</p>
<div class="hidden-elements">
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="keepHiddenText">
{{ 'add-edit-clone-dossier-template.form.hidden-text.title' | translate }}
</mat-checkbox>
<div class="info">{{ 'add-edit-clone-dossier-template.form.hidden-text.description' | translate }}</div>
<div class="info mt-4">{{ 'add-edit-clone-dossier-template.form.hidden-text.description' | translate }}</div>
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="keepImageMetadata">
{{ 'add-edit-clone-dossier-template.form.image-metadata.title' | translate }}
</mat-checkbox>
<div class="info">{{ 'add-edit-clone-dossier-template.form.image-metadata.description' | translate }}</div>
<div class="info mt-4">{{ 'add-edit-clone-dossier-template.form.image-metadata.description' | translate }}</div>
</div>
<div class="iqser-input-group">
<mat-checkbox color="primary" formControlName="keepOverlappingObjects">
{{ 'add-edit-clone-dossier-template.form.overlapping-elements.title' | translate }}
</mat-checkbox>
<div class="info">
<div class="info mt-4">
{{ 'add-edit-clone-dossier-template.form.overlapping-elements.description' | translate }}
</div>
</div>
</div>
}
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="save()"
[buttonId]="'saveButton'"
[disabled]="disabled"
[label]="'add-edit-clone-dossier-template.save' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<iqser-help-button
*ngIf="!isDocumine && !!dossierTemplate"
[helpButtonKey]="'edit_clone_delete_dossier_templates'"
></iqser-help-button>
</div>
</form>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -0,0 +1,58 @@
.validity {
width: 230px;
display: flex;
> div {
display: flex;
flex-direction: column;
margin-top: 16px;
mat-checkbox {
margin-right: 16px;
height: 100%;
align-items: center;
display: flex;
min-height: 42px;
}
.iqser-input-group {
min-height: 42px;
justify-content: center;
}
}
}
redaction-select {
flex: 1;
}
.download-includes {
margin: 16px 0 10px;
font-weight: 500;
}
.hidden-elements {
display: flex;
gap: 40px;
.iqser-input-group {
margin-top: 0;
flex: 1;
mat-checkbox {
font-weight: 500;
}
.info {
margin-left: 24px;
}
}
}
.half-flex-basis {
flex-basis: 50%;
}
.pl-75 {
padding: 0 0 0 75px;
}

View File

@ -0,0 +1,173 @@
import { HttpStatusCode } from '@angular/common/http';
import { Component, Inject } from '@angular/core';
import { AbstractControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { BaseDialogComponent, getConfig } from '@iqser/common-ui';
import { DossierTemplate, IDossierTemplate } from '@red/domain';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { downloadTypesTranslations } from '@translations/download-types-translations';
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import dayjs, { Dayjs } from 'dayjs';
interface EditCloneTemplateData {
dossierTemplateId: string;
clone?: boolean;
}
const downloadTypes = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'].map(type => ({
key: type,
label: downloadTypesTranslations[type],
}));
@Component({
templateUrl: './add-edit-clone-dossier-template-dialog.component.html',
styleUrls: ['./add-edit-clone-dossier-template-dialog.component.scss'],
})
export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogComponent {
readonly isDocumine = getConfig().IS_DOCUMINE;
hasValidFrom: boolean;
hasValidTo: boolean;
readonly downloadTypes = downloadTypes;
readonly dossierTemplate: DossierTemplate;
private _previousValidFrom: Dayjs;
private _previousValidTo: Dayjs;
private _lastValidFrom: Dayjs;
private _lastValidTo: Dayjs;
get disabled(): boolean {
if (!this.data?.clone) {
return super.disabled;
}
return !this.valid;
}
get translateParams() {
return {
type: this.dossierTemplate ? (this.data.clone ? 'clone' : 'edit') : 'create',
name: this.dossierTemplate?.name,
};
}
constructor(
private readonly _dossierTemplatesService: DossierTemplatesService,
protected readonly _dialogRef: MatDialogRef<AddEditCloneDossierTemplateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: EditCloneTemplateData,
) {
super(_dialogRef, !!data && !data.clone);
this.dossierTemplate = this._dossierTemplatesService.find(this.data?.dossierTemplateId);
this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue();
this.hasValidFrom = !!this.dossierTemplate?.validFrom;
this.hasValidTo = !!this.dossierTemplate?.validTo;
this._previousValidFrom = this._lastValidFrom = this.form.get('validFrom').value;
this._previousValidTo = this._lastValidTo = this.form.get('validTo').value;
}
toggleHasValid(extremity: string) {
if (extremity === 'from') {
this.hasValidFrom = !this.hasValidFrom;
this.form.controls['validFrom'].setValue(this.hasValidFrom ? this._lastValidFrom : null);
} else {
this.hasValidTo = !this.hasValidTo;
this.form.controls['validTo'].setValue(this.hasValidTo ? this._lastValidTo : null);
}
this.applyValidityIntervalConstraints();
}
async save() {
this._loadingService.start();
const dossierTemplate = {
dossierTemplateId: this.dossierTemplate?.dossierTemplateId,
...this.form.getRawValue(),
validFrom: this.hasValidFrom ? this.form.get('validFrom').value : null,
validTo: this.hasValidTo ? this.form.get('validTo').value : null,
} as IDossierTemplate;
try {
if (this.data?.clone) {
await this._dossierTemplatesService.clone(this.dossierTemplate.id, dossierTemplate);
} else {
await this._dossierTemplatesService.createOrUpdate(dossierTemplate);
}
this._dialogRef.close(true);
} catch (error) {
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('add-edit-clone-dossier-template.error.conflict'), { error });
} else {
this._toaster.rawError(error.error.message);
}
}
this._loadingService.stop();
}
applyValidityIntervalConstraints(): void {
const formValue = this.form.value;
applyIntervalConstraints(formValue, this._previousValidFrom, this._previousValidTo, this.form, 'validFrom', 'validTo');
this._previousValidFrom = this.form.get('validFrom').value;
this._previousValidTo = this.form.get('validTo').value;
this._lastValidFrom = this._previousValidFrom || this._lastValidFrom;
this._lastValidTo = this._previousValidTo || this._lastValidTo;
}
#getForm() {
return this._formBuilder.group({
name: [this.#getCloneName(), Validators.required],
description: [this.dossierTemplate?.description],
validFrom: [
this.dossierTemplate?.validFrom ? dayjs(this.dossierTemplate?.validFrom) : null,
this.#requiredIfValidator(() => this.hasValidFrom),
],
validTo: [
this.dossierTemplate?.validTo ? dayjs(this.dossierTemplate?.validTo) : null,
this.#requiredIfValidator(() => this.hasValidTo),
],
applyDictionaryUpdatesToAllDossiersByDefault: [this.dossierTemplate?.applyDictionaryUpdatesToAllDossiersByDefault],
ocrByDefault: [this.dossierTemplate?.ocrByDefault],
removeWatermark: [this.dossierTemplate?.removeWatermark],
downloadFileTypes: [this.dossierTemplate?.downloadFileTypes || ['PREVIEW', 'REDACTED']],
keepHiddenText: [this.dossierTemplate?.keepHiddenText],
keepImageMetadata: [this.dossierTemplate?.keepImageMetadata],
keepOverlappingObjects: [this.dossierTemplate?.keepOverlappingObjects],
});
}
#getCloneName(): string {
if (!this.data?.clone) {
return this.dossierTemplate?.name;
}
const templateName = this.dossierTemplate.name.trim();
let nameOfClonedTemplate: string = templateName.split('Copy of ').filter(n => n)[0];
nameOfClonedTemplate = nameOfClonedTemplate.split(/\(\s*\d+\s*\)$/)[0].trim();
const allTemplatesNames = this._dossierTemplatesService.all.map(t => t.name);
let clonesCount = 0;
for (const name of allTemplatesNames) {
const splitName = name.split(nameOfClonedTemplate);
const suffixRegExp = new RegExp(/^\(\s*\d+\s*\)$/);
if (splitName[0] === 'Copy of ' && (splitName[1].trim().match(suffixRegExp) || splitName[1] === '')) {
clonesCount++;
}
}
if (clonesCount >= 1) {
return `Copy of ${nameOfClonedTemplate} (${clonesCount})`;
}
return `Copy of ${nameOfClonedTemplate}`;
}
#requiredIfValidator(predicate) {
return (formControl: AbstractControl) => {
if (!formControl.parent) {
return null;
}
if (predicate()) {
return Validators.required(formControl);
}
return null;
};
}
}

View File

@ -1,23 +1,18 @@
import { Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { BaseDialogComponent, CircleButtonComponent } from '@iqser/common-ui';
import { User } from '@red/domain';
import { ResetPasswordComponent } from './reset-password/reset-password.component';
import { UserDetailsComponent } from './user-details/user-details.component';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
selector: 'redaction-add-edit-user-dialog',
templateUrl: './add-edit-user-dialog.component.html',
imports: [UserDetailsComponent, ResetPasswordComponent, CircleButtonComponent],
})
export class AddEditUserDialogComponent extends BaseDialogComponent {
@ViewChild(UserDetailsComponent) private readonly _userDetailsComponent: UserDetailsComponent;
resettingPassword = false;
@ViewChild(UserDetailsComponent) private readonly _userDetailsComponent: UserDetailsComponent;
constructor(
protected readonly _dialogRef: MatDialogRef<AddEditUserDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly user: User,
) {
constructor(protected readonly _dialogRef: MatDialogRef<AddEditUserDialogComponent>, @Inject(MAT_DIALOG_DATA) readonly user: User) {
super(_dialogRef, !!user);
}

View File

@ -1,16 +1,14 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { UserService } from '@users/user.service';
import { IconButtonComponent, IconButtonTypes, LoadingService } from '@iqser/common-ui';
import { IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { User } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { NamePipe } from '@common-ui/users/name.pipe';
@Component({
selector: 'redaction-reset-password',
templateUrl: './reset-password.component.html',
imports: [TranslateModule, NamePipe, ReactiveFormsModule, IconButtonComponent],
})
export class ResetPasswordComponent {
readonly iconButtonTypes = IconButtonTypes;
@ -22,20 +20,25 @@ export class ResetPasswordComponent {
private readonly _formBuilder: UntypedFormBuilder,
private readonly _userService: UserService,
private readonly _loadingService: LoadingService,
private readonly _toaster: Toaster,
) {}
async save() {
this._loadingService.start();
await firstValueFrom(
this._userService.resetPassword(
{
password: this.form.get('temporaryPassword').value,
temporary: true,
},
this.user.id,
),
);
this.toggleResetPassword.emit();
try {
await firstValueFrom(
this._userService.resetPassword(
{
password: this.form.get('temporaryPassword').value,
temporary: true,
},
this.user.id,
),
);
this.toggleResetPassword.emit();
} catch (error) {
this._toaster.error(_('reset-password-dialog.error.password-policy'));
}
this._loadingService.stop();
}

View File

@ -1,6 +1,6 @@
<div
[translateParams]="{
type: !!user() ? 'edit' : 'create',
type: user ? 'edit' : 'create'
}"
[translate]="'add-edit-user.title'"
class="dialog-header heading-l"
@ -32,22 +32,14 @@
[formControlName]="role"
color="primary"
>
{{ translations[role] | translate: { count: 1 } }}
{{ translations[role] | translate }}
</mat-checkbox>
</div>
</div>
@if (!user()) {
<div class="iqser-input-group">
<label [translate]="'add-edit-user.form.account-setup'"></label>
<mat-checkbox formControlName="sendSetPasswordMail">{{ 'add-edit-user.form.send-email' | translate }}</mat-checkbox>
<span [translate]="'add-edit-user.form.send-email-explanation'" class="hint"></span>
</div>
}
<div
(click)="toggleResetPassword.emit()"
*ngIf="!!user()"
*ngIf="!!user"
[translate]="'add-edit-user.form.reset-password'"
class="mt-24 fit-content link-action"
></div>
@ -56,14 +48,14 @@
<div class="dialog-actions">
<iqser-icon-button
[disabled]="form.invalid || !changed"
[label]="(user() ? 'add-edit-user.actions.save-changes' : 'add-edit-user.actions.save') | translate"
[label]="(user ? 'add-edit-user.actions.save-changes' : 'add-edit-user.actions.save') | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<iqser-icon-button
(action)="delete()"
*ngIf="user() && !disabledDelete(user())"
*ngIf="user && !disabledDelete(user)"
[label]="'add-edit-user.actions.delete' | translate"
[type]="iconButtonTypes.dark"
icon="iqser:trash"

View File

@ -5,7 +5,3 @@
margin-top: 8px;
width: 300px;
}
.hint {
margin-left: 23px;
}

View File

@ -1,31 +1,29 @@
import { Component, input, OnInit, output } from '@angular/core';
import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { AdminDialogService } from '../../../services/admin-dialog.service';
import { BaseFormComponent, IconButtonComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { BaseFormComponent, LoadingService, Toaster } from '@iqser/common-ui';
import { rolesTranslations } from '@translations/roles-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { User } from '@red/domain';
import { UserService } from '@users/user.service';
import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { IProfileUpdateRequest } from '@iqser/common-ui/lib/users';
import { TranslateModule } from '@ngx-translate/core';
import { MatCheckbox } from '@angular/material/checkbox';
import { NgForOf, NgIf } from '@angular/common';
@Component({
selector: 'redaction-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.scss'],
imports: [TranslateModule, ReactiveFormsModule, MatCheckbox, NgForOf, IconButtonComponent, NgIf],
})
export class UserDetailsComponent extends BaseFormComponent implements OnInit {
user = input<User>();
readonly toggleResetPassword = output();
readonly closeDialog = output<boolean>();
readonly cancel = output();
export class UserDetailsComponent extends BaseFormComponent implements OnChanges {
/** e.g. a RED_ADMIN is automatically a RED_USER_ADMIN => can't disable RED_USER_ADMIN as long as RED_ADMIN is checked */
private readonly _ROLE_REQUIREMENTS = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' };
@Input() user: User;
@Output() readonly toggleResetPassword = new EventEmitter();
@Output() readonly closeDialog = new EventEmitter();
@Output() readonly cancel = new EventEmitter();
readonly ROLES = ['RED_USER', 'RED_MANAGER', 'RED_USER_ADMIN', 'RED_ADMIN'];
readonly translations = rolesTranslations;
/** e.g. a RED_ADMIN is automatically a RED_USER_ADMIN => can't disable RED_USER_ADMIN as long as RED_ADMIN is checked */
readonly #ROLE_REQUIREMENTS = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' };
constructor(
private readonly _formBuilder: UntypedFormBuilder,
@ -46,17 +44,13 @@ export class UserDetailsComponent extends BaseFormComponent implements OnInit {
}, []);
}
get sendSetPasswordMail() {
return !this.form.controls.sendSetPasswordMail.value;
}
get #rolesControls() {
private get _rolesControls(): any {
return this.ROLES.reduce(
(prev, role) => ({
...prev,
[role]: [
{
value: this.user() && this.user().has(role),
value: this.user && this.user.has(role),
disabled: this.shouldBeDisabled(role),
},
],
@ -65,20 +59,20 @@ export class UserDetailsComponent extends BaseFormComponent implements OnInit {
);
}
ngOnInit() {
this.form = this.#getForm();
ngOnChanges() {
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}
shouldBeDisabled(role: string): boolean {
const isCurrentAdminUser = this.user() && this.user().isAdmin && this.user().id === this._userService.currentUser.id;
const isCurrentAdminUser = this.user && this.user.isAdmin && this.user.id === this._userService.currentUser.id;
return (
// RED_ADMIN can't remove own RED_ADMIN role
(role === 'RED_ADMIN' && isCurrentAdminUser) ||
// only RED_ADMINs can edit RED_ADMIN roles
(role === 'RED_ADMIN' && !this._userService.currentUser.isAdmin) ||
Object.keys(this.#ROLE_REQUIREMENTS).reduce(
(value, key) => value || (role === this.#ROLE_REQUIREMENTS[key] && this.user()?.roles.includes(key)),
Object.keys(this._ROLE_REQUIREMENTS).reduce(
(value, key) => value || (role === this._ROLE_REQUIREMENTS[key] && this.user?.roles.includes(key)),
false,
)
);
@ -86,38 +80,38 @@ export class UserDetailsComponent extends BaseFormComponent implements OnInit {
async save() {
this._loadingService.start();
const userData: IProfileUpdateRequest = {
...this.form.getRawValue(),
roles: this.activeRoles,
sendSetPasswordMail: this.sendSetPasswordMail,
};
const userData: IProfileUpdateRequest = { ...this.form.getRawValue(), roles: this.activeRoles };
if (!this.user()) {
if (!this.user) {
await firstValueFrom(this._userService.create(userData))
.then(() => {
this.closeDialog.emit(true);
})
.catch(error => {
this._toaster.error(null, { error });
if (error.status === HttpStatusCode.Conflict) {
this._toaster.error(_('add-edit-user.error.email-already-used'));
} else {
this._toaster.error(_('add-edit-user.error.generic'));
}
this._loadingService.stop();
});
} else {
await firstValueFrom(this._userService.updateProfile(userData, this.user().id));
await firstValueFrom(this._userService.updateProfile(userData, this.user.id));
this.closeDialog.emit(true);
}
}
delete() {
this._dialogService.deleteUsers([this.user().id], () => this.closeDialog.emit(true));
this._dialogService.deleteUsers([this.user.id], () => this.closeDialog.emit(true));
}
setRolesRequirements(checked: boolean, role: string): void {
if (Object.keys(this.#ROLE_REQUIREMENTS).includes(role)) {
if (Object.keys(this._ROLE_REQUIREMENTS).includes(role)) {
if (checked) {
this.form.patchValue({ [this.#ROLE_REQUIREMENTS[role]]: true });
this.form.controls[this.#ROLE_REQUIREMENTS[role]].disable();
this.form.patchValue({ [this._ROLE_REQUIREMENTS[role]]: true });
this.form.controls[this._ROLE_REQUIREMENTS[role]].disable();
} else {
this.form.controls[this.#ROLE_REQUIREMENTS[role]].enable();
this.form.controls[this._ROLE_REQUIREMENTS[role]].enable();
}
}
}
@ -128,19 +122,18 @@ export class UserDetailsComponent extends BaseFormComponent implements OnInit {
return user.id === this._userService.currentUser.id || (userAdmin && !currentUserAdmin);
}
#getForm(): UntypedFormGroup {
private _getForm(): UntypedFormGroup {
return this._formBuilder.group({
firstName: [this.user()?.firstName, Validators.required],
lastName: [this.user()?.lastName, Validators.required],
firstName: [this.user?.firstName, Validators.required],
lastName: [this.user?.lastName, Validators.required],
email: [
{
value: this.user()?.email,
disabled: !!this.user()?.email,
value: this.user?.email,
disabled: !!this.user?.email,
},
[Validators.required, Validators.email],
],
...this.#rolesControls,
sendSetPasswordMail: [false],
...this._rolesControls,
});
}
}

View File

@ -16,7 +16,7 @@
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<iqser-help-button *ngIf="!config.IS_DOCUMINE"></iqser-help-button>
<iqser-help-button *ngIf="!config.IS_DOCUMINE" [helpButtonKey]="'create_new_entity'"></iqser-help-button>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>

View File

@ -1,16 +1,7 @@
import { Component, inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {
BaseDialogComponent,
CircleButtonComponent,
getConfig,
HelpButtonComponent,
IconButtonComponent,
IconButtonTypes,
} from '@iqser/common-ui';
import { BaseDialogComponent, getConfig, IconButtonTypes } from '@iqser/common-ui';
import { AddEditEntityComponent } from '@shared/components/add-edit-entity/add-edit-entity.component';
import { TranslateModule } from '@ngx-translate/core';
import { NgIf } from '@angular/common';
interface DialogData {
readonly dossierTemplateId: string;
@ -19,7 +10,6 @@ interface DialogData {
@Component({
templateUrl: './add-entity-dialog.component.html',
styleUrls: ['./add-entity-dialog.component.scss'],
imports: [AddEditEntityComponent, TranslateModule, IconButtonComponent, NgIf, CircleButtonComponent, HelpButtonComponent],
})
export class AddEntityDialogComponent extends BaseDialogComponent {
@ViewChild(AddEditEntityComponent, { static: true }) private readonly _addEditEntityComponent: AddEditEntityComponent;
@ -44,7 +34,11 @@ export class AddEntityDialogComponent extends BaseDialogComponent {
}
async save(): Promise<void> {
await this._addEditEntityComponent.save();
this._dialogRef.close(true);
try {
await this._addEditEntityComponent.save();
this._dialogRef.close(true);
} catch (e) {
console.error(e);
}
}
}

View File

@ -6,11 +6,9 @@
<div class="table-header">Key</div>
<div class="table-header">Value</div>
<ng-container *ngFor="let entry of data.auditEntry.details | keyvalue: originalOrder">
<ng-container *ngFor="let entry of data.auditEntry.details | keyvalue : originalOrder">
<div class="bold">{{ entry.key | humanize }}</div>
<div>
<pre>{{ entry.value | json }}</pre>
</div>
<div>{{ entry.value }}</div>
</ng-container>
</div>
</div>

View File

@ -1,32 +1,30 @@
import { JsonPipe, KeyValue, KeyValuePipe, NgForOf } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { BaseDialogComponent } from '@iqser/common-ui';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { BaseDialogComponent, CircleButtonComponent, HumanizePipe } from '@iqser/common-ui';
import { IAudit } from '@red/domain';
import { TranslateModule } from '@ngx-translate/core';
import { DossierStatesService } from '@services/entity-services/dossier-states.service';
import { KeyValue } from '@angular/common';
interface DialogData {
readonly auditEntry: IAudit;
}
type OrderFn = (a: KeyValue<string, string>, b: KeyValue<string, string>) => number;
@Component({
selector: 'redaction-audit-info-dialog',
templateUrl: './audit-info-dialog.component.html',
styleUrls: ['./audit-info-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TranslateModule, NgForOf, KeyValuePipe, HumanizePipe, CircleButtonComponent, JsonPipe],
})
export class AuditInfoDialogComponent extends BaseDialogComponent {
constructor(
private readonly _dossierStatesService: DossierStatesService,
protected readonly _dialogRef: MatDialogRef<AuditInfoDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
) {
super(_dialogRef, false);
}
readonly originalOrder: OrderFn = () => 0;
readonly originalOrder = (a: KeyValue<string, string>, b: KeyValue<string, string>): number => 0;
async save(): Promise<void> {
return;

View File

@ -1,18 +1,12 @@
import { NgIf } from '@angular/common';
import { HttpStatusCode } from '@angular/common/http';
import { ChangeDetectorRef, Component, ViewChild } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatDialogClose, MatDialogRef } from '@angular/material/dialog';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { DigitalSignatureOption, DigitalSignatureOptions } from '@red/domain';
import { DigitalSignatureService } from '../../services/digital-signature.service';
import { digitalSignatureDialogTranslations } from '../../translations/digital-signature-dialog-translations';
import { KmsSignatureConfigurationComponent } from './form/kms-signature-configuration/kms-signature-configuration.component';
import { BaseDialogComponent, DetailsRadioOption } from '@iqser/common-ui';
import { MatDialogRef } from '@angular/material/dialog';
import { PkcsSignatureConfigurationComponent } from './form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from './form/kms-signature-configuration/kms-signature-configuration.component';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HttpStatusCode } from '@angular/common/http';
import { DigitalSignatureOption, DigitalSignatureOptions } from '@red/domain';
const DEFAULT_DIALOG_WIDTH = '662px';
const KMS_SIGNATURE_DIALOG_WIDTH = '810px';
@ -20,18 +14,6 @@ const KMS_SIGNATURE_DIALOG_WIDTH = '810px';
@Component({
templateUrl: './configure-certificate-dialog.component.html',
styleUrls: ['./configure-certificate-dialog.component.scss'],
imports: [
DetailsRadioComponent,
NgIf,
ReactiveFormsModule,
TranslateModule,
PkcsSignatureConfigurationComponent,
KmsSignatureConfigurationComponent,
IconButtonComponent,
MatDialogClose,
CircleButtonComponent,
],
providers: [DigitalSignatureService],
})
export class ConfigureCertificateDialogComponent extends BaseDialogComponent {
@ViewChild(PkcsSignatureConfigurationComponent) pkcsSignatureConfigurationComponent: PkcsSignatureConfigurationComponent;

View File

@ -1,9 +1,8 @@
import { BaseFormComponent } from '@iqser/common-ui';
import { DigitalSignatureService } from '../../../services/digital-signature.service';
import { DigitalSignatureOption, DigitalSignatureOptions, IDigitalSignatureRequest } from '@red/domain';
import { firstValueFrom, Observable } from 'rxjs';
import { DigitalSignatureService } from '../../../services/digital-signature.service';
// TODO: This should be handled with services for each type of signature
export abstract class BaseSignatureConfigurationComponent extends BaseFormComponent {
file: File | null;

View File

@ -1,27 +1,20 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from '@angular/forms';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { BaseSignatureConfigurationComponent } from '../base-signature-configuration-component';
import { DigitalSignatureOptions, IKmsDigitalSignature, IKmsDigitalSignatureRequest } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { DigitalSignatureService } from '../../../../services/digital-signature.service';
import { UploadFileComponent } from '@iqser/common-ui';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'redaction-kms-signature-configuration',
templateUrl: './kms-signature-configuration.component.html',
styleUrls: ['./kms-signature-configuration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UploadFileComponent, ReactiveFormsModule, NgIf, TranslateModule],
})
export class KmsSignatureConfigurationComponent extends BaseSignatureConfigurationComponent implements OnInit {
@Input() digitalSignature!: IKmsDigitalSignatureRequest;
constructor(
protected readonly _digitalSignatureService: DigitalSignatureService,
private readonly _formBuilder: UntypedFormBuilder,
) {
constructor(protected readonly _digitalSignatureService: DigitalSignatureService, private readonly _formBuilder: UntypedFormBuilder) {
super(_digitalSignatureService, DigitalSignatureOptions.KMS);
}

View File

@ -5,29 +5,28 @@
[readonly]="!!file"
accept=".p12"
></iqser-upload-file>
<form [formGroup]="form">
<div class="flex">
<div class="flex fields-container">
<div class="iqser-input-group required w-300">
<label [translate]="'digital-signature-dialog.forms.pkcs.certificate-name'"></label>
<input formControlName="certificateName" id="certificate-name-input" type="text" />
<input id="certificate-name-input" formControlName="certificateName" type="text" />
</div>
<div *ngIf="!digitalSignature" class="iqser-input-group required w-300">
<label [translate]="'digital-signature-dialog.forms.pkcs.password-key'"></label>
<input formControlName="password" id="certificate-password-input" type="password" />
<input id="certificate-password-input" formControlName="password" type="password" />
</div>
<div class="iqser-input-group w-300">
<label [translate]="'digital-signature-dialog.forms.pkcs.contact-information'"></label>
<input formControlName="contactInfo" id="certificate-contact-info-input" type="text" />
<input id="certificate-contact-info-input" formControlName="contactInfo" type="text" />
</div>
<div class="iqser-input-group w-300">
<label [translate]="'digital-signature-dialog.forms.pkcs.location'"></label>
<input formControlName="location" id="certificate-location-input" type="text" />
<input id="certificate-location-input" formControlName="location" type="text" />
</div>
<div class="iqser-input-group w-450">
<label [translate]="'digital-signature-dialog.forms.pkcs.reason'"></label>
<textarea formControlName="reason" id="certificate-reason-input" rows="4" type="text"></textarea>
<textarea id="certificate-location-input" formControlName="reason" rows="4" type="text"></textarea>
</div>
</div>
</div>

View File

@ -1,28 +1,21 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from '@angular/forms';
import { UntypedFormBuilder, Validators } from '@angular/forms';
import { BaseSignatureConfigurationComponent } from '../base-signature-configuration-component';
import { DigitalSignatureOptions, IPkcsDigitalSignature, IPkcsDigitalSignatureRequest } from '@red/domain';
import { lastIndexOfEnd } from '@utils/functions';
import { firstValueFrom } from 'rxjs';
import { DigitalSignatureService } from '../../../../services/digital-signature.service';
import { BaseSignatureConfigurationComponent } from '../base-signature-configuration-component';
import { UploadFileComponent } from '@iqser/common-ui';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { lastIndexOfEnd } from '../../../../../../utils';
@Component({
selector: 'redaction-pkcs-signature-configuration',
templateUrl: './pkcs-signature-configuration.component.html',
styleUrls: ['./pkcs-signature-configuration.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UploadFileComponent, ReactiveFormsModule, NgIf, TranslateModule],
})
export class PkcsSignatureConfigurationComponent extends BaseSignatureConfigurationComponent implements OnInit {
@Input() digitalSignature!: IPkcsDigitalSignatureRequest;
constructor(
protected readonly _digitalSignatureService: DigitalSignatureService,
private readonly _formBuilder: UntypedFormBuilder,
) {
constructor(protected readonly _digitalSignatureService: DigitalSignatureService, private readonly _formBuilder: UntypedFormBuilder) {
super(_digitalSignatureService, DigitalSignatureOptions.PKCS);
}

View File

@ -21,7 +21,7 @@
>
<mat-icon
*ngIf="!form.get('color').value || form.get('color').value?.length === 0"
svgIcon="iqser:color-picker"
svgIcon="red:color-picker"
></mat-icon>
</div>
</div>

View File

@ -1,16 +1,12 @@
import { Component, Inject } from '@angular/core';
import { DefaultColorType } from '@red/domain';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent, IconButtonTypes } from '@iqser/common-ui';
import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms';
import { BaseDialogComponent, IconButtonTypes } from '@iqser/common-ui';
import { UntypedFormGroup, Validators } from '@angular/forms';
import { defaultColorsTranslations } from '@translations/default-colors-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { firstValueFrom } from 'rxjs';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { ColorPickerModule } from 'ngx-color-picker';
import { MatIcon } from '@angular/material/icon';
import { NgIf } from '@angular/common';
interface IEditColorData {
colorKey: DefaultColorType;
@ -20,7 +16,6 @@ interface IEditColorData {
@Component({
templateUrl: './edit-color-dialog.component.html',
styleUrls: ['./edit-color-dialog.component.scss'],
imports: [ReactiveFormsModule, TranslateModule, ColorPickerModule, MatIcon, NgIf, IconButtonComponent, CircleButtonComponent],
})
export class EditColorDialogComponent extends BaseDialogComponent {
readonly iconButtonTypes = IconButtonTypes;

View File

@ -1,15 +1,13 @@
import { Component, Inject } from '@angular/core';
import { ReactiveFormsModule, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogClose, MatDialogRef } from '@angular/material/dialog';
import { BaseDialogComponent, CircleButtonComponent, IconButtonComponent } from '@iqser/common-ui';
import { ISmtpConfiguration } from '@red/domain';
import { UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { getCurrentUser } from '@users/user.service';
import { TranslateModule } from '@ngx-translate/core';
import { ISmtpConfiguration } from '@red/domain';
import { BaseDialogComponent } from '@iqser/common-ui';
@Component({
selector: 'redaction-smtp-auth-dialog',
templateUrl: './smtp-auth-dialog.component.html',
imports: [ReactiveFormsModule, TranslateModule, IconButtonComponent, CircleButtonComponent, MatDialogClose],
})
export class SmtpAuthDialogComponent extends BaseDialogComponent {
readonly #currentUser = getCurrentUser();
@ -27,7 +25,7 @@ export class SmtpAuthDialogComponent extends BaseDialogComponent {
this._dialogRef.close(this.form.getRawValue());
}
private _getForm() {
private _getForm(): UntypedFormGroup {
return this._formBuilder.group({
user: [this.data?.user || this.#currentUser.email, [Validators.required]],
password: [this.data?.password, Validators.required],

View File

@ -1,18 +1,17 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MatDialogRef } from '@angular/material/dialog';
import { IconButtonComponent, IconButtonTypes } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { IconButtonTypes } from '@iqser/common-ui';
@Component({
templateUrl: './upload-dictionary-dialog.component.html',
styleUrls: ['./upload-dictionary-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [IconButtonComponent, TranslateModule],
})
export class UploadDictionaryDialogComponent {
readonly iconButtonTypes = IconButtonTypes;
constructor(public dialogRef: MatDialogRef<UploadDictionaryDialogComponent>) {}
constructor(private readonly _translateService: TranslateService, public dialogRef: MatDialogRef<UploadDictionaryDialogComponent>) {}
cancel() {
this.dialogRef.close();

View File

@ -16,15 +16,15 @@
<ng-template #headerTemplate>
<div class="table-header-actions">
<iqser-pagination
<redaction-pagination
(pageChanged)="pageChanged($event)"
[settings]="{ currentPage: logs?.page || 0, totalPages: totalPages }"
class="mr-0"
></iqser-pagination>
></redaction-pagination>
<div class="separator">·</div>
<form [attr.help-mode-key]="'audit_filter_audit_entries'" [formGroup]="form">
<form [formGroup]="form" [attr.help-mode-key]="'audit_filter_audit_entries'">
<div class="iqser-input-group w-150 mr-20">
<mat-form-field>
<mat-select (selectionChange)="filterChange()" formControlName="category" id="select-category">
@ -96,7 +96,6 @@
</div>
</ng-template>
<!--TODO: use ngTemplateContextGuard for typings here-->
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as log">
<div class="cell">
@ -117,8 +116,8 @@
<div class="action-buttons">
<iqser-circle-button
(action)="openAuditDetails(log)"
*ngIf="log.hasDetails"
[attr.help-mode-key]="'audit_detailed_audit_info'"
*ngIf="log.hasDetails"
[tooltip]="'audit-screen.action.info' | translate"
icon="red:info"
></iqser-circle-button>

View File

@ -23,3 +23,9 @@ form {
font-size: 16px;
opacity: 0.7;
}
.item-info {
background: var(--iqser-light);
border: 1px solid var(--iqser-grey-1);
padding: 20px;
}

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