From 6e3ec1c243f1deb47b9f710c46a656444c0d8e18 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Tue, 20 Jan 2026 21:48:51 +0000 Subject: [PATCH] Initial commit --- .dockerignore | 5 + .env | 3 + .env.example | 3 + .gitattributes | 1 + .gitignore | 226 +++++ .npmrc | 2 + .vscode/launch.json | 22 + Dockerfile | 32 + README.md | 70 ++ azure-pipelines.yml | 54 + config-overrides.js | 77 ++ nginx/nginx.conf | 50 + package.json | 84 ++ proxy.cmd | 2 + public/favicon.ico | Bin 0 -> 3870 bytes public/index.html | 48 + public/logo192.png | Bin 0 -> 5347 bytes public/logo512.png | Bin 0 -> 9664 bytes public/manifest.json | 25 + public/robots.txt | 3 + public/runtime-env.js | 1 + src/App.tsx | 170 ++++ src/Sass/_ckEditor.scss | 118 +++ src/Sass/_domains.scss | 19 + src/Sass/_esuiteVariables.scss | 32 + src/Sass/_forms.scss | 83 ++ src/Sass/_frame.scss | 22 + src/Sass/_leftMenu.scss | 99 ++ src/Sass/_nav.scss | 16 + src/Sass/autoComplete.scss | 70 ++ src/Sass/general.scss | 7 + src/Sass/global.scss | 33 + src/Sass/horizionalTabs.scss | 19 + src/Sass/login.scss | 137 +++ src/Sass/multiSelect.scss | 6 + src/Sass/old/_colors.scss | 60 ++ src/Sass/old/_container.scss | 27 + src/Sass/old/_fonts.scss | 15 + src/Sass/old/_formfields.scss | 45 + src/Sass/old/_nav.scss | 91 ++ src/Sass/old/_reset.scss | 363 +++++++ src/Sass/old/_sizes.scss | 50 + src/Sass/old/_table.scss | 36 + src/Sass/old/loginredundant.scss | 29 + src/Sass/pill.scss | 38 + src/Sass/vars.scss | 10 + src/components/common/AutoComplete.tsx | 118 +++ src/components/common/Button.tsx | 93 ++ src/components/common/ConfirmButton.tsx | 57 ++ src/components/common/CustomFieldsEditor.tsx | 99 ++ src/components/common/DateView.tsx | 9 + src/components/common/ErrorBlock.tsx | 13 + src/components/common/Form.tsx | 957 ++++++++++++++++++ src/components/common/HorizionalTabs.tsx | 74 ++ src/components/common/Input.tsx | 129 +++ src/components/common/Loading.tsx | 27 + src/components/common/LoadingPanel.tsx | 10 + src/components/common/MultiSelect.tsx | 69 ++ src/components/common/Pagination.tsx | 109 ++ src/components/common/Permission.tsx | 22 + src/components/common/Pill.tsx | 52 + src/components/common/Redirect.tsx | 21 + src/components/common/Select.tsx | 55 + src/components/common/Tab.tsx | 18 + src/components/common/TabHeader.tsx | 36 + src/components/common/Table.tsx | 70 ++ src/components/common/TableBody.tsx | 138 +++ src/components/common/TableFooter.tsx | 40 + src/components/common/TableHeader.tsx | 103 ++ src/components/common/TemplateEditor.tsx | 66 ++ src/components/common/TemplateFiller.tsx | 184 ++++ src/components/common/ToggleSlider.tsx | 26 + src/components/common/ckeditor/TextEditor.jsx | 356 +++++++ .../plugins/abbreviation/abbreviation.js | 14 + .../abbreviation/abbreviationcommand.js | 109 ++ .../abbreviation/abbreviationediting.js | 60 ++ .../plugins/abbreviation/abbreviationui.js | 133 +++ .../plugins/abbreviation/abbreviationview.js | 122 +++ .../ckeditor/plugins/abbreviation/styles.css | 24 + .../ckeditor/plugins/abbreviation/utils.js | 15 + .../common/ckeditor/plugins/field/field.ts | 9 + .../ckeditor/plugins/field/fieldcommand.js | 38 + .../ckeditor/plugins/field/fieldediting.js | 119 +++ .../common/ckeditor/plugins/field/fieldui.js | 67 ++ .../common/ckeditor/plugins/field/styles.css | 12 + src/components/common/columns.tsx | 9 + src/components/common/expando.tsx | 48 + src/components/common/option.tsx | 6 + src/components/common/table.css | 3 + src/components/pickers/CustomFieldPicker.tsx | 94 ++ src/components/pickers/DomainPicker.tsx | 144 +++ src/components/pickers/FormTemplatePicker.tsx | 64 ++ src/components/pickers/GlossaryPicker.tsx | 136 +++ src/components/pickers/SequencePicker.tsx | 58 ++ src/components/pickers/SsoProviderPicker.tsx | 58 ++ src/components/pickers/UserPicker.tsx | 58 ++ src/config.json | 3 + src/img/E-SUITE_logo.svg | 1 + src/img/logo.tsx | 15 + src/img/logo_esuite-white.svg | 1 + src/index.css | 13 + src/index.tsx | 20 + src/logo.svg | 1 + src/modules/audit/audit.tsx | 114 +++ src/modules/audit/components/auditTable.tsx | 119 +++ src/modules/audit/services/auditService.ts | 42 + src/modules/blockedIPs/blockedIPs.tsx | 81 ++ .../blockedIPs/components/blockedIPsTable.tsx | 39 + .../blockedIPs/services/blockedIPsService.ts | 39 + .../errorLogs/components/errorLogsTable.tsx | 27 + src/modules/errorLogs/errorLogs.tsx | 72 ++ .../errorLogs/services/errorLogsService.ts | 34 + .../frame/components/EmailUserAction.tsx | 30 + .../EmailUserActionConfirmEmail.tsx | 141 +++ ...erActionDisableTwoFactorAuthentication.tsx | 63 ++ .../EmailUserActionPasswordReset.tsx | 136 +++ .../frame/components/ForgotPassword.tsx | 69 ++ src/modules/frame/components/LeftMenu.tsx | 92 ++ src/modules/frame/components/LeftMenuItem.tsx | 49 + .../frame/components/LeftMenuSubMenu.tsx | 73 ++ src/modules/frame/components/LoginForm.tsx | 166 +++ src/modules/frame/components/Logout.tsx | 20 + src/modules/frame/components/Mainframe.tsx | 29 + src/modules/frame/components/NotFound.tsx | 7 + src/modules/frame/components/Switch.tsx | 49 + src/modules/frame/components/TopMenu.tsx | 36 + src/modules/frame/components/loginFrame.tsx | 38 + src/modules/frame/models/IEmailUserAction.ts | 12 + src/modules/frame/models/JwtToken.ts | 8 + .../frame/services/authenticationService.ts | 137 +++ src/modules/homepage/Env.tsx | 13 + src/modules/homepage/HomePage.tsx | 20 + .../components/CustomFieldsTable.tsx | 36 + .../customfields/customFieldDetails.tsx | 359 +++++++ .../manager/customfields/customFields.tsx | 99 ++ .../services/customFieldsService.ts | 94 ++ src/modules/manager/domains/Domains.tsx | 97 ++ .../manager/domains/DomainsDetails.tsx | 60 ++ .../domains/components/AddUserToRole.tsx | 82 ++ .../components/EmailTemplateEditor.tsx | 130 +++ .../manager/domains/components/GeneralTab.tsx | 148 +++ .../domains/components/MailTemplatesTab.tsx | 83 ++ .../domains/components/RoleAccessEditor.tsx | 166 +++ .../domains/components/RolesDetails.tsx | 129 +++ .../domains/components/RolesEditor.tsx | 141 +++ .../manager/domains/components/RolesTable.tsx | 35 + .../domains/components/RollAccessTable.tsx | 150 +++ .../domains/components/SecurityRolesTab.tsx | 73 ++ .../domains/components/UserRoleEditor.tsx | 116 +++ .../domains/components/UserRolesTable.tsx | 34 + .../domains/components/domainsTable.tsx | 35 + .../domains/serrvices/domainsService.ts | 100 ++ .../domains/serrvices/mailTemplatesService.ts | 69 ++ .../manager/domains/serrvices/rolesService.ts | 188 ++++ src/modules/manager/forms/Forms.tsx | 97 ++ src/modules/manager/forms/FormsDetails.tsx | 141 +++ .../manager/forms/components/formsTable.tsx | 36 + .../manager/forms/services/formsService.ts | 118 +++ .../manager/glossary/GlossariesDetails.tsx | 177 ++++ src/modules/manager/glossary/Glossary.tsx | 168 +++ .../glossary/components/GlossariesTable.tsx | 127 +++ .../glossary/services/glossaryService.ts | 106 ++ .../manager/organisations/Organisations.tsx | 97 ++ .../organisations/OrganisationsDetails.tsx | 140 +++ .../components/OrganisationsTable.tsx | 51 + .../services/organisationsService.ts | 70 ++ .../manager/sequence/SequenceDetails.tsx | 152 +++ .../sequence/components/squenceTable.tsx | 35 + src/modules/manager/sequence/sequence.tsx | 99 ++ .../sequence/services/sequenceService.ts | 81 ++ src/modules/manager/sites/SiteDetails.tsx | 143 +++ src/modules/manager/sites/Sites.tsx | 108 ++ .../manager/sites/components/SitesTable.tsx | 65 ++ .../manager/sites/services/sitessService.ts | 73 ++ .../manager/specifications/Specifications.tsx | 107 ++ .../specifications/SpecificationsDetails.tsx | 198 ++++ .../components/SpecificationsTable.tsx | 45 + .../services/specificationService.ts | 87 ++ .../manager/ssoManager/SsoProviderDetails.tsx | 161 +++ .../ssoManager/components/ssoManagerTable.tsx | 45 + .../ssoManager/services/ssoManagerService.ts | 84 ++ src/modules/manager/ssoManager/ssoManager.tsx | 97 ++ src/modules/manager/users/UserDetails.tsx | 40 + .../manager/users/components/GeneralTab.tsx | 144 +++ .../manager/users/components/usersTable.tsx | 90 ++ .../manager/users/services/usersService.ts | 94 ++ src/modules/manager/users/users.tsx | 103 ++ src/modules/profile/Profile.tsx | 204 ++++ src/modules/profile/models/ProfileDetails.ts | 11 + .../models/TwoFactorAuthenticationSettings.ts | 4 + .../profile/services/profileService.ts | 47 + src/react-app-env.d.ts | 1 + src/reportWebVitals.ts | 15 + src/services/Paginated.tsx | 7 + src/services/httpService.ts | 107 ++ src/setupTests.ts | 5 + src/types/globals.ts | 11 + src/utils/GeneralIdRef.tsx | 36 + src/utils/MapToJson.tsx | 11 + src/utils/deepfind.tsx | 17 + src/utils/theme.tsx | 21 + src/utils/withRouter.tsx | 28 + tsconfig.json | 27 + 203 files changed, 14818 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .vscode/launch.json create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 azure-pipelines.yml create mode 100644 config-overrides.js create mode 100644 nginx/nginx.conf create mode 100644 package.json create mode 100644 proxy.cmd create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 public/logo192.png create mode 100644 public/logo512.png create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100644 public/runtime-env.js create mode 100644 src/App.tsx create mode 100644 src/Sass/_ckEditor.scss create mode 100644 src/Sass/_domains.scss create mode 100644 src/Sass/_esuiteVariables.scss create mode 100644 src/Sass/_forms.scss create mode 100644 src/Sass/_frame.scss create mode 100644 src/Sass/_leftMenu.scss create mode 100644 src/Sass/_nav.scss create mode 100644 src/Sass/autoComplete.scss create mode 100644 src/Sass/general.scss create mode 100644 src/Sass/global.scss create mode 100644 src/Sass/horizionalTabs.scss create mode 100644 src/Sass/login.scss create mode 100644 src/Sass/multiSelect.scss create mode 100644 src/Sass/old/_colors.scss create mode 100644 src/Sass/old/_container.scss create mode 100644 src/Sass/old/_fonts.scss create mode 100644 src/Sass/old/_formfields.scss create mode 100644 src/Sass/old/_nav.scss create mode 100644 src/Sass/old/_reset.scss create mode 100644 src/Sass/old/_sizes.scss create mode 100644 src/Sass/old/_table.scss create mode 100644 src/Sass/old/loginredundant.scss create mode 100644 src/Sass/pill.scss create mode 100644 src/Sass/vars.scss create mode 100644 src/components/common/AutoComplete.tsx create mode 100644 src/components/common/Button.tsx create mode 100644 src/components/common/ConfirmButton.tsx create mode 100644 src/components/common/CustomFieldsEditor.tsx create mode 100644 src/components/common/DateView.tsx create mode 100644 src/components/common/ErrorBlock.tsx create mode 100644 src/components/common/Form.tsx create mode 100644 src/components/common/HorizionalTabs.tsx create mode 100644 src/components/common/Input.tsx create mode 100644 src/components/common/Loading.tsx create mode 100644 src/components/common/LoadingPanel.tsx create mode 100644 src/components/common/MultiSelect.tsx create mode 100644 src/components/common/Pagination.tsx create mode 100644 src/components/common/Permission.tsx create mode 100644 src/components/common/Pill.tsx create mode 100644 src/components/common/Redirect.tsx create mode 100644 src/components/common/Select.tsx create mode 100644 src/components/common/Tab.tsx create mode 100644 src/components/common/TabHeader.tsx create mode 100644 src/components/common/Table.tsx create mode 100644 src/components/common/TableBody.tsx create mode 100644 src/components/common/TableFooter.tsx create mode 100644 src/components/common/TableHeader.tsx create mode 100644 src/components/common/TemplateEditor.tsx create mode 100644 src/components/common/TemplateFiller.tsx create mode 100644 src/components/common/ToggleSlider.tsx create mode 100644 src/components/common/ckeditor/TextEditor.jsx create mode 100644 src/components/common/ckeditor/plugins/abbreviation/abbreviation.js create mode 100644 src/components/common/ckeditor/plugins/abbreviation/abbreviationcommand.js create mode 100644 src/components/common/ckeditor/plugins/abbreviation/abbreviationediting.js create mode 100644 src/components/common/ckeditor/plugins/abbreviation/abbreviationui.js create mode 100644 src/components/common/ckeditor/plugins/abbreviation/abbreviationview.js create mode 100644 src/components/common/ckeditor/plugins/abbreviation/styles.css create mode 100644 src/components/common/ckeditor/plugins/abbreviation/utils.js create mode 100644 src/components/common/ckeditor/plugins/field/field.ts create mode 100644 src/components/common/ckeditor/plugins/field/fieldcommand.js create mode 100644 src/components/common/ckeditor/plugins/field/fieldediting.js create mode 100644 src/components/common/ckeditor/plugins/field/fieldui.js create mode 100644 src/components/common/ckeditor/plugins/field/styles.css create mode 100644 src/components/common/columns.tsx create mode 100644 src/components/common/expando.tsx create mode 100644 src/components/common/option.tsx create mode 100644 src/components/common/table.css create mode 100644 src/components/pickers/CustomFieldPicker.tsx create mode 100644 src/components/pickers/DomainPicker.tsx create mode 100644 src/components/pickers/FormTemplatePicker.tsx create mode 100644 src/components/pickers/GlossaryPicker.tsx create mode 100644 src/components/pickers/SequencePicker.tsx create mode 100644 src/components/pickers/SsoProviderPicker.tsx create mode 100644 src/components/pickers/UserPicker.tsx create mode 100644 src/config.json create mode 100644 src/img/E-SUITE_logo.svg create mode 100644 src/img/logo.tsx create mode 100644 src/img/logo_esuite-white.svg create mode 100644 src/index.css create mode 100644 src/index.tsx create mode 100644 src/logo.svg create mode 100644 src/modules/audit/audit.tsx create mode 100644 src/modules/audit/components/auditTable.tsx create mode 100644 src/modules/audit/services/auditService.ts create mode 100644 src/modules/blockedIPs/blockedIPs.tsx create mode 100644 src/modules/blockedIPs/components/blockedIPsTable.tsx create mode 100644 src/modules/blockedIPs/services/blockedIPsService.ts create mode 100644 src/modules/errorLogs/components/errorLogsTable.tsx create mode 100644 src/modules/errorLogs/errorLogs.tsx create mode 100644 src/modules/errorLogs/services/errorLogsService.ts create mode 100644 src/modules/frame/components/EmailUserAction.tsx create mode 100644 src/modules/frame/components/EmailUserActionConfirmEmail.tsx create mode 100644 src/modules/frame/components/EmailUserActionDisableTwoFactorAuthentication.tsx create mode 100644 src/modules/frame/components/EmailUserActionPasswordReset.tsx create mode 100644 src/modules/frame/components/ForgotPassword.tsx create mode 100644 src/modules/frame/components/LeftMenu.tsx create mode 100644 src/modules/frame/components/LeftMenuItem.tsx create mode 100644 src/modules/frame/components/LeftMenuSubMenu.tsx create mode 100644 src/modules/frame/components/LoginForm.tsx create mode 100644 src/modules/frame/components/Logout.tsx create mode 100644 src/modules/frame/components/Mainframe.tsx create mode 100644 src/modules/frame/components/NotFound.tsx create mode 100644 src/modules/frame/components/Switch.tsx create mode 100644 src/modules/frame/components/TopMenu.tsx create mode 100644 src/modules/frame/components/loginFrame.tsx create mode 100644 src/modules/frame/models/IEmailUserAction.ts create mode 100644 src/modules/frame/models/JwtToken.ts create mode 100644 src/modules/frame/services/authenticationService.ts create mode 100644 src/modules/homepage/Env.tsx create mode 100644 src/modules/homepage/HomePage.tsx create mode 100644 src/modules/manager/customfields/components/CustomFieldsTable.tsx create mode 100644 src/modules/manager/customfields/customFieldDetails.tsx create mode 100644 src/modules/manager/customfields/customFields.tsx create mode 100644 src/modules/manager/customfields/services/customFieldsService.ts create mode 100644 src/modules/manager/domains/Domains.tsx create mode 100644 src/modules/manager/domains/DomainsDetails.tsx create mode 100644 src/modules/manager/domains/components/AddUserToRole.tsx create mode 100644 src/modules/manager/domains/components/EmailTemplateEditor.tsx create mode 100644 src/modules/manager/domains/components/GeneralTab.tsx create mode 100644 src/modules/manager/domains/components/MailTemplatesTab.tsx create mode 100644 src/modules/manager/domains/components/RoleAccessEditor.tsx create mode 100644 src/modules/manager/domains/components/RolesDetails.tsx create mode 100644 src/modules/manager/domains/components/RolesEditor.tsx create mode 100644 src/modules/manager/domains/components/RolesTable.tsx create mode 100644 src/modules/manager/domains/components/RollAccessTable.tsx create mode 100644 src/modules/manager/domains/components/SecurityRolesTab.tsx create mode 100644 src/modules/manager/domains/components/UserRoleEditor.tsx create mode 100644 src/modules/manager/domains/components/UserRolesTable.tsx create mode 100644 src/modules/manager/domains/components/domainsTable.tsx create mode 100644 src/modules/manager/domains/serrvices/domainsService.ts create mode 100644 src/modules/manager/domains/serrvices/mailTemplatesService.ts create mode 100644 src/modules/manager/domains/serrvices/rolesService.ts create mode 100644 src/modules/manager/forms/Forms.tsx create mode 100644 src/modules/manager/forms/FormsDetails.tsx create mode 100644 src/modules/manager/forms/components/formsTable.tsx create mode 100644 src/modules/manager/forms/services/formsService.ts create mode 100644 src/modules/manager/glossary/GlossariesDetails.tsx create mode 100644 src/modules/manager/glossary/Glossary.tsx create mode 100644 src/modules/manager/glossary/components/GlossariesTable.tsx create mode 100644 src/modules/manager/glossary/services/glossaryService.ts create mode 100644 src/modules/manager/organisations/Organisations.tsx create mode 100644 src/modules/manager/organisations/OrganisationsDetails.tsx create mode 100644 src/modules/manager/organisations/components/OrganisationsTable.tsx create mode 100644 src/modules/manager/organisations/services/organisationsService.ts create mode 100644 src/modules/manager/sequence/SequenceDetails.tsx create mode 100644 src/modules/manager/sequence/components/squenceTable.tsx create mode 100644 src/modules/manager/sequence/sequence.tsx create mode 100644 src/modules/manager/sequence/services/sequenceService.ts create mode 100644 src/modules/manager/sites/SiteDetails.tsx create mode 100644 src/modules/manager/sites/Sites.tsx create mode 100644 src/modules/manager/sites/components/SitesTable.tsx create mode 100644 src/modules/manager/sites/services/sitessService.ts create mode 100644 src/modules/manager/specifications/Specifications.tsx create mode 100644 src/modules/manager/specifications/SpecificationsDetails.tsx create mode 100644 src/modules/manager/specifications/components/SpecificationsTable.tsx create mode 100644 src/modules/manager/specifications/services/specificationService.ts create mode 100644 src/modules/manager/ssoManager/SsoProviderDetails.tsx create mode 100644 src/modules/manager/ssoManager/components/ssoManagerTable.tsx create mode 100644 src/modules/manager/ssoManager/services/ssoManagerService.ts create mode 100644 src/modules/manager/ssoManager/ssoManager.tsx create mode 100644 src/modules/manager/users/UserDetails.tsx create mode 100644 src/modules/manager/users/components/GeneralTab.tsx create mode 100644 src/modules/manager/users/components/usersTable.tsx create mode 100644 src/modules/manager/users/services/usersService.ts create mode 100644 src/modules/manager/users/users.tsx create mode 100644 src/modules/profile/Profile.tsx create mode 100644 src/modules/profile/models/ProfileDetails.ts create mode 100644 src/modules/profile/models/TwoFactorAuthenticationSettings.ts create mode 100644 src/modules/profile/services/profileService.ts create mode 100644 src/react-app-env.d.ts create mode 100644 src/reportWebVitals.ts create mode 100644 src/services/Paginated.tsx create mode 100644 src/services/httpService.ts create mode 100644 src/setupTests.ts create mode 100644 src/types/globals.ts create mode 100644 src/utils/GeneralIdRef.tsx create mode 100644 src/utils/MapToJson.tsx create mode 100644 src/utils/deepfind.tsx create mode 100644 src/utils/theme.tsx create mode 100644 src/utils/withRouter.tsx create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df0f3aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.vscode +node_modules +build +Dockerfile \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..51dfa74 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +NODE_ENV=development +API_URL=http://localhost:3001/api/ +EXTERNAL_LOGIN=true \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..51dfa74 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +NODE_ENV=development +API_URL=http://localhost:3001/api/ +EXTERNAL_LOGIN=true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d7c444c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* -crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acf8edd --- /dev/null +++ b/.gitignore @@ -0,0 +1,226 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ + +# New VS2015 folders +.vs/ + +# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets +!packages/*/build/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# NuGet Packages Directory +packages/ + +# Compare files +*.orig + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.pfx +*.publishsettings +*.swp + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + +# TeX files +Documentation/*.aux +Documentation/*.out +Documentation/*.pdf +Documentation/*.idx +Documentation/*.toc +Documentation/*.gz + +# image resizer cache +imagecache/ +*.DotSettings + +# TypeScript mapping files +*.js.map + +# Exclude all Typescript-generated JS files +src/Sunrise.Web.Customer/App/*.js +src/Sunrise.Web.Customer/App/**/*.js +src/Sunrise.Web.Customer/Views/**/*.js +src/Sunrise.Web.Customer/Areas/**/*.js +src/Sunrise.Web.Support/Views/**/*.js + +*.GhostDoc.user.dic +*.GhostDoc.xml + +/TestResult.xml +*.VisualState.xml + +artifacts/ +/src/Sunrise.Web.Customer/Scripts/typings + +# Db project +src/Sunrise.Database/*.jfm +src/Sunrise.Database/Sunrise.Database.jfm +/src/Sunrise.Database/*.jfm +/src/Sunrise.Database/Sunrise.Database.jfm +/src/Sunrise.Web.Customer/node_modules + +# node modules +node_modules +.eslintrc + +#NCrunch folders +_NCrunch*/ +src/Sunrise.Web.FrontEnd/.eslintrc.js +src/Sunrise.Web.FrontEnd/.prettierrc +src/Sunrise.Web.FrontEnd/.vscode/settings.json +src/Sunrise.Web.Customer/Content/bundles/vendor.js +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# css build files +/public/styles.css +/public/styles.css.map + +# misc +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +/package-lock.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..dacfd92 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@fortawesome:registry=https://npm.fontawesome.com/ +//npm.fontawesome.com/:_authToken=FDC47A4D-82AB-4EDC-887C-4853D3D0AEEA \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9392a88 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Chrome", + "request": "launch", + "type": "chrome", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..342155e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +#Stage 1 - the build process +FROM node:latest as build-deps +WORKDIR /app +#COPY package.json . +COPY . . +RUN npm install +RUN npm run build +RUN npm run build-css + +#Stage 2 - the production environment +FROM nginx:latest + +ENV EXTERNAL_LOGIN=true + +RUN apt-get update && apt-get upgrade -y && \ + apt-get install -y nodejs \ + npm # note this one +WORKDIR /usr/share/nginx/html +COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build-deps /app/build /usr/share/nginx/html +copy --from=build-deps /app/public/styles.css /usr/share/nginx/html + +# copy .env.example as .env to the container +COPY .env.example .env + +RUN npm install -g cross-env +RUN npm install -g runtime-env-cra + +EXPOSE 80 +EXPOSE 443 + +CMD ["/bin/sh", "-c", "runtime-env-cra && nginx -g \"daemon off;\""] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..58beeac --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can't go back!** + +If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. + +You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `npm run build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..81921f2 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,54 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +name: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)$(postfix)$(branchName) # NOTE: rev resets when the default retention period expires + +trigger: + branches: + include: + - '*' + +pool: + vmImage: ubuntu-latest # ubuntu-latest - set to windows-latest or another Windows vmImage for Windows builds + +variables: + containerRegistry: esuite.azurecr.io + + imageName: 'e-Suite.webui' + + ${{ if eq(variables['Build.SourceBranchName'], 'master') }}: + branchName: '' + ${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}: + branchName: $[ replace(replace(variables['Build.SourceBranch'], 'refs/heads/', ''), '/', '-' ) ] + ${{ elseif startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}: + branchName: $[ replace(replace(variables['System.PullRequest.TargetBranch'], 'refs/heads/', ''), '/', '-' ) ] + + ${{ if eq(variables['branchName'], '') }}: + postfix: '' + ${{ else }}: + postfix: '-' + +steps: +- task: Docker@2 + displayName: Build e-suite Web UI Image + inputs: + repository: $(imageName) + command: build + Dockerfile: Dockerfile + containerRegistry: | + $(containerRegistry) + tags: | + $(Build.BuildNumber) + +- task: Docker@2 + displayName: Push e-suite Web UI Image + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + inputs: + containerRegistry: | + $(containerRegistry) + repository: $(imageName) + command: push + tags: | + $(Build.BuildNumber) \ No newline at end of file diff --git a/config-overrides.js b/config-overrides.js new file mode 100644 index 0000000..485793a --- /dev/null +++ b/config-overrides.js @@ -0,0 +1,77 @@ +//const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); + +module.exports = function override(config, env) { + if (!config.plugins) { + config.plugins = []; + } + + for ( const rule of config.module.rules ) + { + if (rule.oneOf !== undefined) { + //loader: require.resolve('file-loader'), + rule.oneOf[2].use[1].options = { + // Exclude `js` files to keep the "css" loader working as it injects + // its runtime that would otherwise be processed through the "file" loader. + // Also exclude `html` and `json` extensions so they get processed + // by webpack's internal loaders. + exclude: [ + /\.(js|mjs|jsx|ts|tsx)$/, + /\.html$/, + /\.json$/, + /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/, + /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/ + ], + name: 'static/media/[name].[hash:8].[ext]', + } + + //test: cssRegex, + rule.oneOf[5].exclude = [ + /\.module\.css$/, + /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, + ]; + + //test: cssModuleRegex, + rule.oneOf[6].exclude = [ + /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, + ]; + + //Added items + // rule.oneOf.unshift( { + // test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/, + // use: [ + // { + // loader: 'style-loader', + // options: { + // injectType: 'singletonStyleTag', + // attributes: { + // 'data-cke': true + // } + // } + // }, + // 'css-loader', + // { + // loader: 'postcss-loader', + // options: { + // postcssOptions: styles.getPostCssConfig( { + // themeImporter: { + // themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) + // }, + // minify: true + // } ) + // } + // } + // ] + // } + // ); + + // rule.oneOf.unshift( { + // test: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/, + // use: [ 'raw-loader' ] + // } + // ); + + } + } + + return config; +} \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..be17fa6 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,50 @@ +server { + + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + + #proxy_pass http://192.168.151.107:3030/; + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } +} + + +#upstream backend { +# + ## nginx as a load balancer with proxy + #ip_hash; + #server 192.168.0.1:31300 weight=5; + #server 192.168.0.1:31301 weight=1; + #server 192.168.0.1:31302 max_fails=3 fail_timeout=30s weight=1; +#} + + +#proxy_cache_path /etc/nginx/cache keys_zone=my_cache:10m max_size=100m inactive=60m use_temp_path=off; +# nginx as a reverse proxy +# this will route our frontend requests to the backend on port 4008 +#server { + #listen 9090; +# + #location / { + #proxy_cache my_cache; + #proxy_cache_valid 200 30s; + #proxy_cache_methods GET HEAD POST; + #proxy_cache_min_uses 1; + #proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; + #proxy_pass http://backend; + #proxy_redirect off; + #proxy_set_header Host $host; + #proxy_set_header X-Real-IP $remote_addr; + #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + #proxy_set_header X-Forwarded-Host $server_name; + #} +#} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f0f1e6 --- /dev/null +++ b/package.json @@ -0,0 +1,84 @@ +{ + "name": "e-suite.webui", + "version": "0.1.0", + "private": true, + "dependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@ckeditor/ckeditor5-react": "^6.1.0", + "@fortawesome/fontawesome-pro": "^6.5.0", + "@fortawesome/fontawesome-svg-core": "^6.2.1", + "@fortawesome/free-solid-svg-icons": "^6.2.1", + "@fortawesome/pro-duotone-svg-icons": "^6.5.0", + "@fortawesome/pro-light-svg-icons": "^6.5.0", + "@fortawesome/pro-regular-svg-icons": "^6.5.0", + "@fortawesome/pro-solid-svg-icons": "^6.5.0", + "@fortawesome/pro-thin-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "@types/jest": "^29.5.1", + "@types/js-cookie": "^3.0.6", + "@types/lodash.debounce": "^4.0.7", + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "axios": "^1.4.0", + "bootstrap": "^5.3.0-alpha3", + "buffer": "^6.0.3", + "ckeditor5": "^43.1.0", + "ckeditor5-premium-features": "^43.1.0", + "concurrently": "^8.2.2", + "cross-env": "^7.0.3", + "date-fns": "^2.30.0", + "html-react-parser": "^3.0.16", + "joi": "^17.9.1", + "js-cookie": "^3.0.5", + "jwt-decode": "^3.1.2", + "lodash.debounce": "^4.0.8", + "nodemon": "^3.1.4", + "react": "^18.2.0", + "react-bootstrap": "^2.7.4", + "react-dom": "^18.2.0", + "react-helmet-async": "^1.3.0", + "react-router-dom": "^6.10.0", + "react-scripts": "^5.0.1", + "react-toastify": "^9.1.2", + "react-toggle": "^4.1.3", + "runtime-env-cra": "^0.2.4", + "sass": "^1.62.0", + "typescript": "^4.7.4", + "web-vitals": "^3.3.1" + }, + "scripts": { + "build-css": "sass src/Sass/global.scss public/styles.css", + "watch-css": "nodemon -e scss -x \"npm run build-css\" ", + "start-react": "cross-env NODE_ENV=development runtime-env-cra --config-name=./public/runtime-env.js && react-app-rewired start", + "start": "concurrently \"npm run start-react\" \"npm run watch-css\" ", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/node": "^20.2.3", + "@types/react-toggle": "^4.0.3", + "react-app-rewired": "^2.2.1" + } +} diff --git a/proxy.cmd b/proxy.cmd new file mode 100644 index 0000000..dd5d22f --- /dev/null +++ b/proxy.cmd @@ -0,0 +1,2 @@ +cd ../e-suite.Proxy/ +docker compose up \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..5c0049e --- /dev/null +++ b/public/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + e-suite + + + +
+ + + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 zcmZWtbyO6NvR-oO24RV%BvuJ&=?+<7=`LvyB&A_#M7mSDYw1v6DJkiYl9XjT!%$dLEBTQ8R9|wd3008in6lFF3GV-6mLi?MoP_y~}QUnaDCHI#t z7w^m$@6DI)|C8_jrT?q=f8D?0AM?L)Z}xAo^e^W>t$*Y0KlT5=@bBjT9kxb%-KNdk zeOS1tKO#ChhG7%{ApNBzE2ZVNcxbrin#E1TiAw#BlUhXllzhN$qWez5l;h+t^q#Eav8PhR2|T}y5kkflaK`ba-eoE+Z2q@o6P$)=&` z+(8}+-McnNO>e#$Rr{32ngsZIAX>GH??tqgwUuUz6kjns|LjsB37zUEWd|(&O!)DY zQLrq%Y>)Y8G`yYbYCx&aVHi@-vZ3|ebG!f$sTQqMgi0hWRJ^Wc+Ibv!udh_r%2|U) zPi|E^PK?UE!>_4`f`1k4hqqj_$+d!EB_#IYt;f9)fBOumGNyglU(ofY`yHq4Y?B%- zp&G!MRY<~ajTgIHErMe(Z8JG*;D-PJhd@RX@QatggM7+G(Lz8eZ;73)72Hfx5KDOE zkT(m}i2;@X2AT5fW?qVp?@WgN$aT+f_6eo?IsLh;jscNRp|8H}Z9p_UBO^SJXpZew zEK8fz|0Th%(Wr|KZBGTM4yxkA5CFdAj8=QSrT$fKW#tweUFqr0TZ9D~a5lF{)%-tTGMK^2tz(y2v$i%V8XAxIywrZCp=)83p(zIk6@S5AWl|Oa2hF`~~^W zI;KeOSkw1O#TiQ8;U7OPXjZM|KrnN}9arP)m0v$c|L)lF`j_rpG(zW1Qjv$=^|p*f z>)Na{D&>n`jOWMwB^TM}slgTEcjxTlUby89j1)|6ydRfWERn3|7Zd2&e7?!K&5G$x z`5U3uFtn4~SZq|LjFVrz$3iln-+ucY4q$BC{CSm7Xe5c1J<=%Oagztj{ifpaZk_bQ z9Sb-LaQMKp-qJA*bP6DzgE3`}*i1o3GKmo2pn@dj0;He}F=BgINo};6gQF8!n0ULZ zL>kC0nPSFzlcB7p41doao2F7%6IUTi_+!L`MM4o*#Y#0v~WiO8uSeAUNp=vA2KaR&=jNR2iVwG>7t%sG2x_~yXzY)7K& zk3p+O0AFZ1eu^T3s};B%6TpJ6h-Y%B^*zT&SN7C=N;g|#dGIVMSOru3iv^SvO>h4M=t-N1GSLLDqVTcgurco6)3&XpU!FP6Hlrmj}f$ zp95;b)>M~`kxuZF3r~a!rMf4|&1=uMG$;h^g=Kl;H&Np-(pFT9FF@++MMEx3RBsK?AU0fPk-#mdR)Wdkj)`>ZMl#^<80kM87VvsI3r_c@_vX=fdQ`_9-d(xiI z4K;1y1TiPj_RPh*SpDI7U~^QQ?%0&!$Sh#?x_@;ag)P}ZkAik{_WPB4rHyW#%>|Gs zdbhyt=qQPA7`?h2_8T;-E6HI#im9K>au*(j4;kzwMSLgo6u*}-K`$_Gzgu&XE)udQ zmQ72^eZd|vzI)~!20JV-v-T|<4@7ruqrj|o4=JJPlybwMg;M$Ud7>h6g()CT@wXm` zbq=A(t;RJ^{Xxi*Ff~!|3!-l_PS{AyNAU~t{h;(N(PXMEf^R(B+ZVX3 z8y0;0A8hJYp@g+c*`>eTA|3Tgv9U8#BDTO9@a@gVMDxr(fVaEqL1tl?md{v^j8aUv zm&%PX4^|rX|?E4^CkplWWNv*OKM>DxPa z!RJ)U^0-WJMi)Ksc!^ixOtw^egoAZZ2Cg;X7(5xZG7yL_;UJ#yp*ZD-;I^Z9qkP`} zwCTs0*%rIVF1sgLervtnUo&brwz?6?PXRuOCS*JI-WL6GKy7-~yi0giTEMmDs_-UX zo=+nFrW_EfTg>oY72_4Z0*uG>MnXP=c0VpT&*|rvv1iStW;*^={rP1y?Hv+6R6bxFMkxpWkJ>m7Ba{>zc_q zEefC3jsXdyS5??Mz7IET$Kft|EMNJIv7Ny8ZOcKnzf`K5Cd)&`-fTY#W&jnV0l2vt z?Gqhic}l}mCv1yUEy$%DP}4AN;36$=7aNI^*AzV(eYGeJ(Px-j<^gSDp5dBAv2#?; zcMXv#aj>%;MiG^q^$0MSg-(uTl!xm49dH!{X0){Ew7ThWV~Gtj7h%ZD zVN-R-^7Cf0VH!8O)uUHPL2mO2tmE*cecwQv_5CzWeh)ykX8r5Hi`ehYo)d{Jnh&3p z9ndXT$OW51#H5cFKa76c<%nNkP~FU93b5h-|Cb}ScHs@4Q#|}byWg;KDMJ#|l zE=MKD*F@HDBcX@~QJH%56eh~jfPO-uKm}~t7VkHxHT;)4sd+?Wc4* z>CyR*{w@4(gnYRdFq=^(#-ytb^5ESD?x<0Skhb%Pt?npNW1m+Nv`tr9+qN<3H1f<% zZvNEqyK5FgPsQ`QIu9P0x_}wJR~^CotL|n zk?dn;tLRw9jJTur4uWoX6iMm914f0AJfB@C74a;_qRrAP4E7l890P&{v<}>_&GLrW z)klculcg`?zJO~4;BBAa=POU%aN|pmZJn2{hA!d!*lwO%YSIzv8bTJ}=nhC^n}g(ld^rn#kq9Z3)z`k9lvV>y#!F4e{5c$tnr9M{V)0m(Z< z#88vX6-AW7T2UUwW`g<;8I$Jb!R%z@rCcGT)-2k7&x9kZZT66}Ztid~6t0jKb&9mm zpa}LCb`bz`{MzpZR#E*QuBiZXI#<`5qxx=&LMr-UUf~@dRk}YI2hbMsAMWOmDzYtm zjof16D=mc`^B$+_bCG$$@R0t;e?~UkF?7<(vkb70*EQB1rfUWXh$j)R2)+dNAH5%R zEBs^?N;UMdy}V};59Gu#0$q53$}|+q7CIGg_w_WlvE}AdqoS<7DY1LWS9?TrfmcvT zaypmplwn=P4;a8-%l^e?f`OpGb}%(_mFsL&GywhyN(-VROj`4~V~9bGv%UhcA|YW% zs{;nh@aDX11y^HOFXB$a7#Sr3cEtNd4eLm@Y#fc&j)TGvbbMwze zXtekX_wJqxe4NhuW$r}cNy|L{V=t#$%SuWEW)YZTH|!iT79k#?632OFse{+BT_gau zJwQcbH{b}dzKO?^dV&3nTILYlGw{27UJ72ZN){BILd_HV_s$WfI2DC<9LIHFmtyw? zQ;?MuK7g%Ym+4e^W#5}WDLpko%jPOC=aN)3!=8)s#Rnercak&b3ESRX3z{xfKBF8L z5%CGkFmGO@x?_mPGlpEej!3!AMddChabyf~nJNZxx!D&{@xEb!TDyvqSj%Y5@A{}9 zRzoBn0?x}=krh{ok3Nn%e)#~uh;6jpezhA)ySb^b#E>73e*frBFu6IZ^D7Ii&rsiU z%jzygxT-n*joJpY4o&8UXr2s%j^Q{?e-voloX`4DQyEK+DmrZh8A$)iWL#NO9+Y@!sO2f@rI!@jN@>HOA< z?q2l{^%mY*PNx2FoX+A7X3N}(RV$B`g&N=e0uvAvEN1W^{*W?zT1i#fxuw10%~))J zjx#gxoVlXREWZf4hRkgdHx5V_S*;p-y%JtGgQ4}lnA~MBz-AFdxUxU1RIT$`sal|X zPB6sEVRjGbXIP0U+?rT|y5+ev&OMX*5C$n2SBPZr`jqzrmpVrNciR0e*Wm?fK6DY& zl(XQZ60yWXV-|Ps!A{EF;=_z(YAF=T(-MkJXUoX zI{UMQDAV2}Ya?EisdEW;@pE6dt;j0fg5oT2dxCi{wqWJ<)|SR6fxX~5CzblPGr8cb zUBVJ2CQd~3L?7yfTpLNbt)He1D>*KXI^GK%<`bq^cUq$Q@uJifG>p3LU(!H=C)aEL zenk7pVg}0{dKU}&l)Y2Y2eFMdS(JS0}oZUuVaf2+K*YFNGHB`^YGcIpnBlMhO7d4@vV zv(@N}(k#REdul8~fP+^F@ky*wt@~&|(&&meNO>rKDEnB{ykAZ}k>e@lad7to>Ao$B zz<1(L=#J*u4_LB=8w+*{KFK^u00NAmeNN7pr+Pf+N*Zl^dO{LM-hMHyP6N!~`24jd zXYP|Ze;dRXKdF2iJG$U{k=S86l@pytLx}$JFFs8e)*Vi?aVBtGJ3JZUj!~c{(rw5>vuRF$`^p!P8w1B=O!skwkO5yd4_XuG^QVF z`-r5K7(IPSiKQ2|U9+`@Js!g6sfJwAHVd|s?|mnC*q zp|B|z)(8+mxXyxQ{8Pg3F4|tdpgZZSoU4P&9I8)nHo1@)9_9u&NcT^FI)6|hsAZFk zZ+arl&@*>RXBf-OZxhZerOr&dN5LW9@gV=oGFbK*J+m#R-|e6(Loz(;g@T^*oO)0R zN`N=X46b{7yk5FZGr#5&n1!-@j@g02g|X>MOpF3#IjZ_4wg{dX+G9eqS+Es9@6nC7 zD9$NuVJI}6ZlwtUm5cCAiYv0(Yi{%eH+}t)!E^>^KxB5^L~a`4%1~5q6h>d;paC9c zTj0wTCKrhWf+F#5>EgX`sl%POl?oyCq0(w0xoL?L%)|Q7d|Hl92rUYAU#lc**I&^6p=4lNQPa0 znQ|A~i0ip@`B=FW-Q;zh?-wF;Wl5!+q3GXDu-x&}$gUO)NoO7^$BeEIrd~1Dh{Tr` z8s<(Bn@gZ(mkIGnmYh_ehXnq78QL$pNDi)|QcT*|GtS%nz1uKE+E{7jdEBp%h0}%r zD2|KmYGiPa4;md-t_m5YDz#c*oV_FqXd85d@eub?9N61QuYcb3CnVWpM(D-^|CmkL z(F}L&N7qhL2PCq)fRh}XO@U`Yn<?TNGR4L(mF7#4u29{i~@k;pLsgl({YW5`Mo+p=zZn3L*4{JU;++dG9 X@eDJUQo;Ye2mwlRs?y0|+_a0zY+Zo%Dkae}+MySoIppb75o?vUW_?)>@g{U2`ERQIXV zeY$JrWnMZ$QC<=ii4X|@0H8`si75jB(ElJb00HAB%>SlLR{!zO|C9P3zxw_U8?1d8uRZ=({Ga4shyN}3 zAK}WA(ds|``G4jA)9}Bt2Hy0+f3rV1E6b|@?hpGA=PI&r8)ah|)I2s(P5Ic*Ndhn^ z*T&j@gbCTv7+8rpYbR^Ty}1AY)YH;p!m948r#%7x^Z@_-w{pDl|1S4`EM3n_PaXvK z1JF)E3qy$qTj5Xs{jU9k=y%SQ0>8E$;x?p9ayU0bZZeo{5Z@&FKX>}s!0+^>C^D#z z>xsCPvxD3Z=dP}TTOSJhNTPyVt14VCQ9MQFN`rn!c&_p?&4<5_PGm4a;WS&1(!qKE z_H$;dDdiPQ!F_gsN`2>`X}$I=B;={R8%L~`>RyKcS$72ai$!2>d(YkciA^J0@X%G4 z4cu!%Ps~2JuJ8ex`&;Fa0NQOq_nDZ&X;^A=oc1&f#3P1(!5il>6?uK4QpEG8z0Rhu zvBJ+A9RV?z%v?!$=(vcH?*;vRs*+PPbOQ3cdPr5=tOcLqmfx@#hOqX0iN)wTTO21jH<>jpmwRIAGw7`a|sl?9y9zRBh>(_%| zF?h|P7}~RKj?HR+q|4U`CjRmV-$mLW>MScKnNXiv{vD3&2@*u)-6P@h0A`eeZ7}71 zK(w%@R<4lLt`O7fs1E)$5iGb~fPfJ?WxhY7c3Q>T-w#wT&zW522pH-B%r5v#5y^CF zcC30Se|`D2mY$hAlIULL%-PNXgbbpRHgn<&X3N9W!@BUk@9g*P5mz-YnZBb*-$zMM z7Qq}ic0mR8n{^L|=+diODdV}Q!gwr?y+2m=3HWwMq4z)DqYVg0J~^}-%7rMR@S1;9 z7GFj6K}i32X;3*$SmzB&HW{PJ55kT+EI#SsZf}bD7nW^Haf}_gXciYKX{QBxIPSx2Ma? zHQqgzZq!_{&zg{yxqv3xq8YV+`S}F6A>Gtl39_m;K4dA{pP$BW0oIXJ>jEQ!2V3A2 zdpoTxG&V=(?^q?ZTj2ZUpDUdMb)T?E$}CI>r@}PFPWD9@*%V6;4Ag>D#h>!s)=$0R zRXvdkZ%|c}ubej`jl?cS$onl9Tw52rBKT)kgyw~Xy%z62Lr%V6Y=f?2)J|bZJ5(Wx zmji`O;_B+*X@qe-#~`HFP<{8$w@z4@&`q^Q-Zk8JG3>WalhnW1cvnoVw>*R@c&|o8 zZ%w!{Z+MHeZ*OE4v*otkZqz11*s!#s^Gq>+o`8Z5 z^i-qzJLJh9!W-;SmFkR8HEZJWiXk$40i6)7 zZpr=k2lp}SasbM*Nbn3j$sn0;rUI;%EDbi7T1ZI4qL6PNNM2Y%6{LMIKW+FY_yF3) zSKQ2QSujzNMSL2r&bYs`|i2Dnn z=>}c0>a}>|uT!IiMOA~pVT~R@bGlm}Edf}Kq0?*Af6#mW9f9!}RjW7om0c9Qlp;yK z)=XQs(|6GCadQbWIhYF=rf{Y)sj%^Id-ARO0=O^Ad;Ph+ z0?$eE1xhH?{T$QI>0JP75`r)U_$#%K1^BQ8z#uciKf(C701&RyLQWBUp*Q7eyn76} z6JHpC9}R$J#(R0cDCkXoFSp;j6{x{b&0yE@P7{;pCEpKjS(+1RQy38`=&Yxo%F=3y zCPeefABp34U-s?WmU#JJw23dcC{sPPFc2#J$ZgEN%zod}J~8dLm*fx9f6SpO zn^Ww3bt9-r0XaT2a@Wpw;C23XM}7_14#%QpubrIw5aZtP+CqIFmsG4`Cm6rfxl9n5 z7=r2C-+lM2AB9X0T_`?EW&Byv&K?HS4QLoylJ|OAF z`8atBNTzJ&AQ!>sOo$?^0xj~D(;kS$`9zbEGd>f6r`NC3X`tX)sWgWUUOQ7w=$TO&*j;=u%25ay-%>3@81tGe^_z*C7pb9y*Ed^H3t$BIKH2o+olp#$q;)_ zfpjCb_^VFg5fU~K)nf*d*r@BCC>UZ!0&b?AGk_jTPXaSnCuW110wjHPPe^9R^;jo3 zwvzTl)C`Zl5}O2}3lec=hZ*$JnkW#7enKKc)(pM${_$9Hc=Sr_A9Biwe*Y=T?~1CK z6eZ9uPICjy-sMGbZl$yQmpB&`ouS8v{58__t0$JP%i3R&%QR3ianbZqDs<2#5FdN@n5bCn^ZtH992~5k(eA|8|@G9u`wdn7bnpg|@{m z^d6Y`*$Zf2Xr&|g%sai#5}Syvv(>Jnx&EM7-|Jr7!M~zdAyjt*xl;OLhvW-a%H1m0 z*x5*nb=R5u><7lyVpNAR?q@1U59 zO+)QWwL8t zyip?u_nI+K$uh{y)~}qj?(w0&=SE^8`_WMM zTybjG=999h38Yes7}-4*LJ7H)UE8{mE(6;8voE+TYY%33A>S6`G_95^5QHNTo_;Ao ztIQIZ_}49%{8|=O;isBZ?=7kfdF8_@azfoTd+hEJKWE!)$)N%HIe2cplaK`ry#=pV z0q{9w-`i0h@!R8K3GC{ivt{70IWG`EP|(1g7i_Q<>aEAT{5(yD z=!O?kq61VegV+st@XCw475j6vS)_z@efuqQgHQR1T4;|-#OLZNQJPV4k$AX1Uk8Lm z{N*b*ia=I+MB}kWpupJ~>!C@xEN#Wa7V+7{m4j8c?)ChV=D?o~sjT?0C_AQ7B-vxqX30s0I_`2$in86#`mAsT-w?j{&AL@B3$;P z31G4(lV|b}uSDCIrjk+M1R!X7s4Aabn<)zpgT}#gE|mIvV38^ODy@<&yflpCwS#fRf9ZX3lPV_?8@C5)A;T zqmouFLFk;qIs4rA=hh=GL~sCFsXHsqO6_y~*AFt939UYVBSx1s(=Kb&5;j7cSowdE;7()CC2|-i9Zz+_BIw8#ll~-tyH?F3{%`QCsYa*b#s*9iCc`1P1oC26?`g<9))EJ3%xz+O!B3 zZ7$j~To)C@PquR>a1+Dh>-a%IvH_Y7^ys|4o?E%3`I&ADXfC8++hAdZfzIT#%C+Jz z1lU~K_vAm0m8Qk}K$F>|>RPK%<1SI0(G+8q~H zAsjezyP+u!Se4q3GW)`h`NPSRlMoBjCzNPesWJwVTY!o@G8=(6I%4XHGaSiS3MEBK zhgGFv6Jc>L$4jVE!I?TQuwvz_%CyO!bLh94nqK11C2W$*aa2ueGopG8DnBICVUORP zgytv#)49fVXDaR$SukloYC3u7#5H)}1K21=?DKj^U)8G;MS)&Op)g^zR2($<>C*zW z;X7`hLxiIO#J`ANdyAOJle4V%ppa*(+0i3w;8i*BA_;u8gOO6)MY`ueq7stBMJTB; z-a0R>hT*}>z|Gg}@^zDL1MrH+2hsR8 zHc}*9IvuQC^Ju)^#Y{fOr(96rQNPNhxc;mH@W*m206>Lo<*SaaH?~8zg&f&%YiOEG zGiz?*CP>Bci}!WiS=zj#K5I}>DtpregpP_tfZtPa(N<%vo^#WCQ5BTv0vr%Z{)0q+ z)RbfHktUm|lg&U3YM%lMUM(fu}i#kjX9h>GYctkx9Mt_8{@s%!K_EI zScgwy6%_fR?CGJQtmgNAj^h9B#zmaMDWgH55pGuY1Gv7D z;8Psm(vEPiwn#MgJYu4Ty9D|h!?Rj0ddE|&L3S{IP%H4^N!m`60ZwZw^;eg4sk6K{ ziA^`Sbl_4~f&Oo%n;8Ye(tiAdlZKI!Z=|j$5hS|D$bDJ}p{gh$KN&JZYLUjv4h{NY zBJ>X9z!xfDGY z+oh_Z&_e#Q(-}>ssZfm=j$D&4W4FNy&-kAO1~#3Im;F)Nwe{(*75(p=P^VI?X0GFakfh+X-px4a%Uw@fSbmp9hM1_~R>?Z8+ ziy|e9>8V*`OP}4x5JjdWp}7eX;lVxp5qS}0YZek;SNmm7tEeSF*-dI)6U-A%m6YvCgM(}_=k#a6o^%-K4{`B1+}O4x zztDT%hVb;v#?j`lTvlFQ3aV#zkX=7;YFLS$uIzb0E3lozs5`Xy zi~vF+%{z9uLjKvKPhP%x5f~7-Gj+%5N`%^=yk*Qn{`> z;xj&ROY6g`iy2a@{O)V(jk&8#hHACVDXey5a+KDod_Z&}kHM}xt7}Md@pil{2x7E~ zL$k^d2@Ec2XskjrN+IILw;#7((abu;OJii&v3?60x>d_Ma(onIPtcVnX@ELF0aL?T zSmWiL3(dOFkt!x=1O!_0n(cAzZW+3nHJ{2S>tgSK?~cFha^y(l@-Mr2W$%MN{#af8J;V*>hdq!gx=d0h$T7l}>91Wh07)9CTX zh2_ZdQCyFOQ)l(}gft0UZG`Sh2`x-w`5vC2UD}lZs*5 zG76$akzn}Xi))L3oGJ75#pcN=cX3!=57$Ha=hQ2^lwdyU#a}4JJOz6ddR%zae%#4& za)bFj)z=YQela(F#Y|Q#dp}PJghITwXouVaMq$BM?K%cXn9^Y@g43$=O)F&ZlOUom zJiad#dea;-eywBA@e&D6Pdso1?2^(pXiN91?jvcaUyYoKUmvl5G9e$W!okWe*@a<^ z8cQQ6cNSf+UPDx%?_G4aIiybZHHagF{;IcD(dPO!#=u zWfqLcPc^+7Uu#l(Bpxft{*4lv#*u7X9AOzDO z1D9?^jIo}?%iz(_dwLa{ex#T}76ZfN_Z-hwpus9y+4xaUu9cX}&P{XrZVWE{1^0yw zO;YhLEW!pJcbCt3L8~a7>jsaN{V3>tz6_7`&pi%GxZ=V3?3K^U+*ryLSb)8^IblJ0 zSRLNDvIxt)S}g30?s_3NX>F?NKIGrG_zB9@Z>uSW3k2es_H2kU;Rnn%j5qP)!XHKE zPB2mHP~tLCg4K_vH$xv`HbRsJwbZMUV(t=ez;Ec(vyHH)FbfLg`c61I$W_uBB>i^r z&{_P;369-&>23R%qNIULe=1~T$(DA`ev*EWZ6j(B$(te}x1WvmIll21zvygkS%vwG zzkR6Z#RKA2!z!C%M!O>!=Gr0(J0FP=-MN=5t-Ir)of50y10W}j`GtRCsXBakrKtG& zazmITDJMA0C51&BnLY)SY9r)NVTMs);1<=oosS9g31l{4ztjD3#+2H7u_|66b|_*O z;Qk6nalpqdHOjx|K&vUS_6ITgGll;TdaN*ta=M_YtyC)I9Tmr~VaPrH2qb6sd~=AcIxV+%z{E&0@y=DPArw zdV7z(G1hBx7hd{>(cr43^WF%4Y@PXZ?wPpj{OQ#tvc$pABJbvPGvdR`cAtHn)cSEV zrpu}1tJwQ3y!mSmH*uz*x0o|CS<^w%&KJzsj~DU0cLQUxk5B!hWE>aBkjJle8z~;s z-!A=($+}Jq_BTK5^B!`R>!MulZN)F=iXXeUd0w5lUsE5VP*H*oCy(;?S$p*TVvTxwAeWFB$jHyb0593)$zqalVlDX=GcCN1gU0 zlgU)I$LcXZ8Oyc2TZYTPu@-;7<4YYB-``Qa;IDcvydIA$%kHhJKV^m*-zxcvU4viy&Kr5GVM{IT>WRywKQ9;>SEiQD*NqplK-KK4YR`p0@JW)n_{TU3bt0 zim%;(m1=#v2}zTps=?fU5w^(*y)xT%1vtQH&}50ZF!9YxW=&7*W($2kgKyz1mUgfs zfV<*XVVIFnohW=|j+@Kfo!#liQR^x>2yQdrG;2o8WZR+XzU_nG=Ed2rK?ntA;K5B{ z>M8+*A4!Jm^Bg}aW?R?6;@QG@uQ8&oJ{hFixcfEnJ4QH?A4>P=q29oDGW;L;= z9-a0;g%c`C+Ai!UmK$NC*4#;Jp<1=TioL=t^YM)<<%u#hnnfSS`nq63QKGO1L8RzX z@MFDqs1z ztYmxDl@LU)5acvHk)~Z`RW7=aJ_nGD!mOSYD>5Odjn@TK#LY{jf?+piB5AM-CAoT_ z?S-*q7}wyLJzK>N%eMPuFgN)Q_otKP;aqy=D5f!7<=n(lNkYRXVpkB{TAYLYg{|(jtRqYmg$xH zjmq?B(RE4 zQx^~Pt}gxC2~l=K$$-sYy_r$CO(d=+b3H1MB*y_5g6WLaWTXn+TKQ|hNY^>Mp6k*$ zwkovomhu776vQATqT4blf~g;TY(MWCrf^^yfWJvSAB$p5l;jm@o#=!lqw+Lqfq>X= z$6~kxfm7`3q4zUEB;u4qa#BdJxO!;xGm)wwuisj{0y2x{R(IGMrsIzDY9LW>m!Y`= z04sx3IjnYvL<4JqxQ8f7qYd0s2Ig%`ytYPEMKI)s(LD}D@EY>x`VFtqvnADNBdeao zC96X+MxnwKmjpg{U&gP3HE}1=s!lv&D{6(g_lzyF3A`7Jn*&d_kL<;dAFx!UZ>hB8 z5A*%LsAn;VLp>3${0>M?PSQ)9s3}|h2e?TG4_F{}{Cs>#3Q*t$(CUc}M)I}8cPF6% z=+h(Kh^8)}gj(0}#e7O^FQ6`~fd1#8#!}LMuo3A0bN`o}PYsm!Y}sdOz$+Tegc=qT z8x`PH$7lvnhJp{kHWb22l;@7B7|4yL4UOOVM0MP_>P%S1Lnid)+k9{+3D+JFa#Pyf zhVc#&df87APl4W9X)F3pGS>@etfl=_E5tBcVoOfrD4hmVeTY-cj((pkn%n@EgN{0f zwb_^Rk0I#iZuHK!l*lN`ceJn(sI{$Fq6nN& zE<-=0_2WN}m+*ivmIOxB@#~Q-cZ>l136w{#TIJe478`KE7@=a{>SzPHsKLzYAyBQO zAtuuF$-JSDy_S@6GW0MOE~R)b;+0f%_NMrW(+V#c_d&U8Z9+ec4=HmOHw?gdjF(Lu zzra83M_BoO-1b3;9`%&DHfuUY)6YDV21P$C!Rc?mv&{lx#f8oc6?0?x zK08{WP65?#>(vPfA-c=MCY|%*1_<3D4NX zeVTi-JGl2uP_2@0F{G({pxQOXt_d{g_CV6b?jNpfUG9;8yle-^4KHRvZs-_2siata zt+d_T@U$&t*xaD22(fH(W1r$Mo?3dc%Tncm=C6{V9y{v&VT#^1L04vDrLM9qBoZ4@ z6DBN#m57hX7$C(=#$Y5$bJmwA$T8jKD8+6A!-IJwA{WOfs%s}yxUw^?MRZjF$n_KN z6`_bGXcmE#5e4Ym)aQJ)xg3Pg0@k`iGuHe?f(5LtuzSq=nS^5z>vqU0EuZ&75V%Z{ zYyhRLN^)$c6Ds{f7*FBpE;n5iglx5PkHfWrj3`x^j^t z7ntuV`g!9Xg#^3!x)l*}IW=(Tz3>Y5l4uGaB&lz{GDjm2D5S$CExLT`I1#n^lBH7Y zDgpMag@`iETKAI=p<5E#LTkwzVR@=yY|uBVI1HG|8h+d;G-qfuj}-ZR6fN>EfCCW z9~wRQoAPEa#aO?3h?x{YvV*d+NtPkf&4V0k4|L=uj!U{L+oLa(z#&iuhJr3-PjO3R z5s?=nn_5^*^Rawr>>Nr@K(jwkB#JK-=+HqwfdO<+P5byeim)wvqGlP-P|~Nse8=XF zz`?RYB|D6SwS}C+YQv+;}k6$-%D(@+t14BL@vM z2q%q?f6D-A5s$_WY3{^G0F131bbh|g!}#BKw=HQ7mx;Dzg4Z*bTLQSfo{ed{4}NZW zfrRm^Ca$rlE{Ue~uYv>R9{3smwATcdM_6+yWIO z*ZRH~uXE@#p$XTbCt5j7j2=86e{9>HIB6xDzV+vAo&B?KUiMP|ttOElepnl%|DPqL b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/public/runtime-env.js b/public/runtime-env.js new file mode 100644 index 0000000..81c1e73 --- /dev/null +++ b/public/runtime-env.js @@ -0,0 +1 @@ +window.__RUNTIME_CONFIG__ = {"NODE_ENV":"development","API_URL":"http://localhost:3001/api/","EXTERNAL_LOGIN":"true"}; \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..aa85189 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,170 @@ +import React, { useEffect } from "react"; +import { Routes, Route, Navigate, useNavigate } from "react-router-dom"; +import { Helmet, HelmetProvider, HtmlProps } from "react-helmet-async"; +import { ToastContainer } from "react-toastify"; +import config from "./config.json"; +import authentication from "./modules/frame/services/authenticationService"; +import ForgotPassword from "./modules/frame/components/ForgotPassword"; +import NotFound from "./modules/frame/components/NotFound"; +import Logout from "./modules/frame/components/Logout"; +import LoginForm from "./modules/frame/components/LoginForm"; +import Redirect from "./components/common/Redirect"; +import Mainframe from "./modules/frame/components/Mainframe"; +import EmailUserAction from "./modules/frame/components/EmailUserAction"; + +import HomePage from "./modules/homepage/HomePage"; +import Profile from "./modules/profile/Profile"; +import EnvPage from "./modules/homepage/Env"; +import Sequence from "./modules/manager/sequence/sequence"; +import HOCSequenceDetails from "./modules/manager/sequence/SequenceDetails"; +import CustomFields from "./modules/manager/customfields/customFields"; +import HOCCustomFieldDetails from "./modules/manager/customfields/customFieldDetails"; +import Forms from "./modules/manager/forms/Forms"; +import HOCFormsDetails from "./modules/manager/forms/FormsDetails"; +import HOCGlossaries from "./modules/manager/glossary/Glossary"; +import HOCGlossariesDetails from "./modules/manager/glossary/GlossariesDetails"; +import HOCAudit from "./modules/audit/audit"; +import Domains from "./modules/manager/domains/Domains"; +import DomainsDetails from "./modules/manager/domains/DomainsDetails"; +import Users from "./modules/manager/users/users"; +import UserDetails from "./modules/manager/users/UserDetails"; +import RolesDetails from "./modules/manager/domains/components/RolesDetails"; +import AddUserToRole from "./modules/manager/domains/components/AddUserToRole"; +import LoginFrame from "./modules/frame/components/loginFrame"; +import theme from "./utils/theme"; +import Organisations from "./modules/manager/organisations/Organisations"; +import HOCOrganisationsDetails from "./modules/manager/organisations/OrganisationsDetails"; +import Sites from "./modules/manager/sites/Sites"; +import SiteDetails from "./modules/manager/sites/SiteDetails"; +import Specifications from "./modules/manager/specifications/Specifications"; +import SpecificationsDetails from "./modules/manager/specifications/SpecificationsDetails"; +import BlockedIPs from "./modules/blockedIPs/blockedIPs"; +import ErrorLogs from "./modules/errorLogs/errorLogs"; +import SsoManager from "./modules/manager/ssoManager/ssoManager"; +import SsoProviderDetails from "./modules/manager/ssoManager/SsoProviderDetails"; + +function GetSecureRoutes() { + const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? }/> + : }/>; + + return ( + <> + }/> + } /> + } /> + } /> + + }/> + }/> + }/> + + }/> + }/> + }/> + } /> + + }/> + }/> + }/> + + } /> + }/> + }/> + }/> + }/> + }/> + + }/> + }/> + }/> + + }/> + }/> + }/> + + }/> + }/> + }/> + + }/> + }/> + }/> + }/> + }/> + }/> + + }/> + }/> + }/> + + }/> + }/> + }/> + + + {profileRoute} + }/> + }/> + + ); +} + +function App() { + let navigate = useNavigate(); + + useEffect(() => { + const timer = setInterval(async () => { + try { + if (authentication.hasToken()) { + await authentication.refreshToken(); + + if (authentication.tokenExpired()) { + navigate("/login"); + authentication.logout() + } + } + } + catch (e: any) { + console.log(e); + } + }, 10 * 1000); + return () => clearInterval(timer); + }); + + const isSignedIn = authentication.getCurrentUser() != null; + + const secureRoutes = isSignedIn ? GetSecureRoutes() : } />; + + var htmlAttributes : HtmlProps = { + 'data-bs-theme' : theme.getPreferredTheme() + } + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + window.location.reload(); + }) + + const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? }/> + : } />; + + return ( + + + {config.applicationName} + +
+ + } /> + {loginRoute} + } /> + } /> + } /> + {secureRoutes} + } /> + + +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/Sass/_ckEditor.scss b/src/Sass/_ckEditor.scss new file mode 100644 index 0000000..e21081d --- /dev/null +++ b/src/Sass/_ckEditor.scss @@ -0,0 +1,118 @@ +@import './_esuiteVariables.scss'; + +.form-editor { + min-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2) + $spacePadding)); + max-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2) + $spacePadding)); +} + +.mailTemplate-editor { + min-width: calc(100vw - ($leftMenuWidth + $gridGap + $mailtemplateNameListWidth + ($frameWorkAreaPadding * 2) + $spacePadding)); + max-width: calc(100vw - ($leftMenuWidth + $gridGap + $mailtemplateNameListWidth + ($frameWorkAreaPadding * 2) + $spacePadding)); +} + + +@media print { + body { + margin: 0 !important; + } +} + +.ck-main-container { + // --ckeditor5-preview-height: 700px; + font-family: 'Lato'; + width: fit-content; + margin-left: auto; + margin-right: auto; + + .ck { + .ck-dropdown__panel { + .ck-list{ + max-height: 30vh; + overflow-y: auto; + } + } + } +} + +.ck-content { + font-family: 'Lato'; + line-height: 1.6; + word-break: break-word; + + .table table { + overflow: auto; + } +} + +.editor-container__editor-wrapper { + display: flex; + width: fit-content; +} + +.editor-container_document-editor { + border: 1px solid var(--ck-color-base-border); +} + +.editor-container_document-editor .editor-container__toolbar { + display: flex; + position: relative; + box-shadow: 0 2px 3px hsla(0, 0%, 0%, 0.078); +} + +.editor-container_document-editor .editor-container__toolbar > .ck.ck-toolbar { + flex-grow: 1; + width: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-top: 0; + border-left: 0; + border-right: 0; +} + +.editor-container_document-editor .editor-container__menu-bar > .ck.ck-menu-bar { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-top: 0; + border-left: 0; + border-right: 0; +} + +// .editor-container_document-editor .editor-container__editor-wrapper { +// max-height: var(--ckeditor5-preview-height); +// min-height: var(--ckeditor5-preview-height); +// overflow-y: scroll; +// background: var(--ck-color-base-foreground); +// } + +// .editor-container_document-editor .editor-container__editor { +// margin-top: 28px; +// margin-bottom: 28px; +// height: 100%; +// } + +.editor-container_document-editor .editor-container__editor .ck.ck-editor__editable { + box-sizing: border-box; + //min-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2))); + //max-width: calc(100vw - ($leftMenuWidth + ($frameWorkAreaPadding * 2))); + + min-width: 100%; + max-width: 100%; + + // min-width: calc(210mm + 2px); + // max-width: calc(210mm + 2px); + height: fit-content; + // padding: 20mm 12mm; + border: 1px hsl(0, 0%, 82.7%) solid; + //background: hsl(0, 0%, 100%); + //color: black; + box-shadow: 0 2px 3px hsla(0, 0%, 0%, 0.078); + flex: 1 1 auto; + // margin-left: 72px; + // margin-right: 72px; +} + + +.ck-toolbar { + background: hsl(0, 0%, 100%); + color: black; +} \ No newline at end of file diff --git a/src/Sass/_domains.scss b/src/Sass/_domains.scss new file mode 100644 index 0000000..236d2da --- /dev/null +++ b/src/Sass/_domains.scss @@ -0,0 +1,19 @@ +@import './_esuiteVariables.scss'; + +.two-column-grid-1-1 { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: $gridGap; +} + +.two-column-grid-1-3 { + display: grid; + grid-template-columns: 1fr 3fr; + grid-gap: $gridGap; +} + +.mail-types { + padding-left: 0rem; + list-style: none; + width: $mailtemplateNameListWidth; +} \ No newline at end of file diff --git a/src/Sass/_esuiteVariables.scss b/src/Sass/_esuiteVariables.scss new file mode 100644 index 0000000..7ab64ec --- /dev/null +++ b/src/Sass/_esuiteVariables.scss @@ -0,0 +1,32 @@ +// Overrides for Bootstrap variables light mode +$blue: #6262fb !default; +$indigo: #6610f2 !default; +$purple: #6f42c1 !default; +$pink: #de6c74 !default; +$red: #b00020 !default; +$orange: #fd7e14 !default; +$yellow: #ee9700 !default; +$green: #004733 !default; +$teal: #00bb9c !default; +$cyan: #0dcaf0 !default; + +$mode--light-bg: #f8fafb; +$mode--dark-bg: #262626; + +$gridGap: 20px; + +$spacePadding: 17px; + +//Frame +$frameWorkAreaPadding: 16px; + +//MailTemplate Editor +$mailtemplateNameListWidth: 245px; + +//Left Menu +$leftMenuWidth: 70px; +$leftMenu-background: #2c2c2e; +$leftMenu-color: #ddd; +$leftMenu-HoverColour: $blue; +$leftMenu-selected-color: $blue; +$leftMenu-submenu-open: $blue; diff --git a/src/Sass/_forms.scss b/src/Sass/_forms.scss new file mode 100644 index 0000000..aa85749 --- /dev/null +++ b/src/Sass/_forms.scss @@ -0,0 +1,83 @@ +@import "../Sass/old/_colors.scss"; +@import "../Sass/esuiteVariables"; + +.form-group { + padding: 10px 0; +} + +.label { + +} + +input { + background: transparent; +} + +table { + white-space: pre-wrap; +} + +.form-control { + +} + +.form-check { + +} + +.form-check-input { + +} + +.form-check-label { + +} + +.flex { + display: flex; + height: 38px; +} + +.passwordIcon { + vertical-align: middle; + position: relative; + top: 3px; +} + +.fullHeight { + height: 100%; + padding-left: 5px; + padding-right: 5px; +} + +.e-printWidget { + width: 100%; + margin-top: 20px; + min-height: 100px; + border: 1px solid $gray-600; + border-radius: 5px; + + .thumbnail { + width: 80px; + min-height: 80px; + float: left; + background-color: $gray-600; + margin: 10px; + } + + .label { + padding-top: 35px; + width: 200px; + height: 100px; + } + + .e-print { + cursor: pointer; + width: 100px; + } +} + +.allignedCheckBox .checkbox { + float: left; + margin-top: 10px; +} \ No newline at end of file diff --git a/src/Sass/_frame.scss b/src/Sass/_frame.scss new file mode 100644 index 0000000..a2b6bbf --- /dev/null +++ b/src/Sass/_frame.scss @@ -0,0 +1,22 @@ +@import './_esuiteVariables.scss'; + +.frame { + width: 100vw; + height: 100vh; + overflow: hidden; +} + +.frame-row { + display: flex; +} + +.frame-leftMenu { + height: auto; +} + +.frame-workArea { + height: calc(100vh - 76px); + width: 100%; + overflow: auto; + padding: $frameWorkAreaPadding; +} \ No newline at end of file diff --git a/src/Sass/_leftMenu.scss b/src/Sass/_leftMenu.scss new file mode 100644 index 0000000..311f04d --- /dev/null +++ b/src/Sass/_leftMenu.scss @@ -0,0 +1,99 @@ +@import "../Sass//global.scss"; +@import "../Sass/nav.scss"; + +.leftMenuSelected { + color: $leftMenu-selected-color !important; +} + +.leftMenuSubMenuOpen { + color: $leftMenu-submenu-open !important; +} + +.leftMenu { + height: 100%; + width: $leftMenuWidth; + background-color: $leftMenu-background; + color: $leftMenu-color; + padding-top: 5px; +} + +.LeftMenuItem{ + width: 100%; + height: 72px; + padding-top: 10px; + padding-bottom: 10px; + + a { + color: $leftMenu-color; + text-decoration-line: none; + + &:hover { + color: $leftMenu-HoverColour; + } + } +} + +.leftMenuItemIcon { + margin: 0; + position: absolute; + left:50%; + transform: translatex(-50%); + + height: 50%; + display: inline-block; + vertical-align: top; + position: relative; + width: 50%; + text-align: center; + line-height: 100%; +} + +.leftMenuItemLabel{ + width: 100%; + text-align: center; +} + +.leftMenuSubMenu { + cursor: pointer; + + &:hover { + color: $leftMenu-HoverColour; + } +} + +.subbar { + background-color: $leftMenu-background; + height: 100%; + left: $leftMenuWidth; + position: fixed; + top: $headerHeight; + width: 235px; + z-index: 1000; + border-left: 1px inset #999; + + .header { + color: $leftMenu-color; + font-size: 21px; + transform: translate(18px, 10px); + margin-bottom: 1rem; + } + + a { + color: $leftMenu-color; + display: block; + font-family: "Ubuntu-Light", sans-serif; + height: 40px; + padding-top: 9px; + margin: 0px 18px; + text-decoration-line: none; + + &:hover { + color: $leftMenu-HoverColour; + text-decoration: none; + } + + &:not(:last-child) { + border-bottom: 1px solid #bbb; + } + } +} \ No newline at end of file diff --git a/src/Sass/_nav.scss b/src/Sass/_nav.scss new file mode 100644 index 0000000..7c44e14 --- /dev/null +++ b/src/Sass/_nav.scss @@ -0,0 +1,16 @@ +$headerHeight: 75px; + +.navbar-brand { + width: 50px; + height: auto; +} + +.navbar-right { + margin: 0 0 0 auto; +} + +.navbar { + height: $headerHeight; + border-bottom: 1px solid black; +} + diff --git a/src/Sass/autoComplete.scss b/src/Sass/autoComplete.scss new file mode 100644 index 0000000..1c6261a --- /dev/null +++ b/src/Sass/autoComplete.scss @@ -0,0 +1,70 @@ +.autocomplete { + .autocomplete-option { + width: 100%; + height: 30px; + } + + .autocomplete-text-input { + border: none; + width: 100%; + + &:focus { + outline: none; + } + + &:focus-visible { + border: none; + } + + &:focus-within { + border: none; + } + + &:active { + border: none; + } + } + + .autocomplete-options { + border: 1px; + border-style: solid; + border-color: var(--bs-body-tx); + border-radius: 5px; + background-color: var(--bs-body-bg); + position: absolute; + max-height: 400px; + z-index: 80; + padding: 5px; + max-height: 350px !important; + overflow: auto; + + li { + margin: 0; + padding: 0px 5px 0px 0px; + list-style: none; + margin-left: 5px; + } + } + + button { + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + } + + .autocomplete-option:focus { + background-color: #f3f3f4; + } + + .autocomplete-option:hover { + background-color: #f3f3f3; + } + + .text-left { + text-align: left; + } +} \ No newline at end of file diff --git a/src/Sass/general.scss b/src/Sass/general.scss new file mode 100644 index 0000000..d3cd968 --- /dev/null +++ b/src/Sass/general.scss @@ -0,0 +1,7 @@ +.loading { + cursor: wait; +} + +.pagination { + padding:0; +} diff --git a/src/Sass/global.scss b/src/Sass/global.scss new file mode 100644 index 0000000..f167216 --- /dev/null +++ b/src/Sass/global.scss @@ -0,0 +1,33 @@ +@import "../../node_modules/bootstrap/scss/functions"; + +//default variable overrides +@import './_esuiteVariables.scss'; + +@import "../../node_modules/bootstrap/scss/variables"; +@import "../../node_modules/bootstrap/scss/variables-dark"; + +@import "../../node_modules/bootstrap/scss/maps"; +@import "../../node_modules/bootstrap/scss/mixins"; +@import "../../node_modules/bootstrap/scss/root"; + +// main bootstrap import +//@import "~bootstrap/scss/bootstrap.scss"; + +@import "../../node_modules/react-toastify/dist/ReactToastify"; +@import "../../node_modules/bootstrap/dist/css/bootstrap"; +@import "../../node_modules/react-toggle/style"; +@import "../../node_modules/@ckeditor/ckeditor5-theme-lark/theme/theme"; + +@import "./_domains.scss"; +@import "./general.scss"; +@import "./login.scss"; +@import "./ckEditor.scss"; +@import "./autoComplete.scss"; +@import "./pill.scss"; +@import "./multiSelect.scss"; +@import "./horizionalTabs"; + +//Changes needed to make MS Edge behave the same as other browsers +input::-ms-reveal { + display: none; +} diff --git a/src/Sass/horizionalTabs.scss b/src/Sass/horizionalTabs.scss new file mode 100644 index 0000000..192bb04 --- /dev/null +++ b/src/Sass/horizionalTabs.scss @@ -0,0 +1,19 @@ +.horizionalTabs { + .tab-list { + border-bottom: 1px solid #ccc; + padding-left: 0; + } + + .tab-list-item { + display: inline-block; + list-style: none; + margin-bottom: -1px; + padding: 0.5rem 0.75rem; + } + + .tab-list-active { + background-color: white; + border: solid #ccc; + border-width: 1px 1px 0 1px; + } + } \ No newline at end of file diff --git a/src/Sass/login.scss b/src/Sass/login.scss new file mode 100644 index 0000000..536c8b9 --- /dev/null +++ b/src/Sass/login.scss @@ -0,0 +1,137 @@ +.loginFormContainer { + position: relative; + left: 50px; + top: 150px; + width: 400px; + padding: 20px; + + .logo { + text-align: center; + } + + input[type=text], input[type=password] { + background-color: transparent; + border: 0px solid; + border-bottom: 1px solid; + border-radius: 0; + } + + input[type=text]:focus, input[type=password]:focus { + outline-style: none; + -webkit-box-shadow: none; + } + + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + transition: background-color 5000s ease-in-out 0s; + } + + .fullHeight { + width: 10%; + border-bottom: 1px solid; + } + + .disabledIcon { + pointer-events: none; + } + + .alert { + margin-top: 10px; + width: 100%; + } + + ul { + list-style-type: none; + padding-top: 10px; + margin: 0px; + } + + ul li:before { + content: '\2713'; //its a tick mark + padding: 5px; + } + + ul li{ + color: grey; + } + + .checked{ + color: green; + } + + .unchecked { + color: grey; + } +} + +.label{ + padding-bottom: 10px; + padding-top: 10px; +} + +.forgottenLink { + margin-top: 10px; +} + +.next{ + margin-top: 10px; +} +.alert-danger { + margin-top: 10px; +} + +.emailSent{ + margin-top: 10px; + width: 100%; +} + + +.changePasswordBlock { + .logo { + text-align: center; + } + + input[type=text], input[type=password] { + background-color: transparent; + border: 0px solid; + border-bottom: 1px solid; + border-radius: 0; + } + + input[type=text]:focus, input[type=password]:focus { + outline-style: none; + -webkit-box-shadow: none; + } + + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + transition: background-color 5000s ease-in-out 0s; + } + + ul { + list-style-type: none; + padding-top: 10px; + margin: 0px; + } + + ul li:before { + content: '\2713'; //its a tick mark + padding: 5px; + } + + ul li{ + color: grey; + } + + .checked{ + color: green; + } + + .unchecked { + color: grey; + } +} diff --git a/src/Sass/multiSelect.scss b/src/Sass/multiSelect.scss new file mode 100644 index 0000000..d920f76 --- /dev/null +++ b/src/Sass/multiSelect.scss @@ -0,0 +1,6 @@ +.multiSelect { + .multiSelectContainer { + display: inline-flex; + width: 100%; + } +} \ No newline at end of file diff --git a/src/Sass/old/_colors.scss b/src/Sass/old/_colors.scss new file mode 100644 index 0000000..65ddf93 --- /dev/null +++ b/src/Sass/old/_colors.scss @@ -0,0 +1,60 @@ +$white: #fff !default; +$gray-100: #f8f9fa !default; +$gray-200: #e9ecef !default; +$gray-300: #dee2e6 !default; +$gray-400: #ced4da !default; +$gray-500: #adb5bd !default; +$gray-600: #6c757d !default; +$gray-700: #495057 !default; +$gray-800: #343a40 !default; +$gray-900: #212529 !default; +$black: #000 !default; + +$grays: ( + "100": $gray-100, + "200": $gray-200, + "300": $gray-300, + "400": $gray-400, + "500": $gray-500, + "600": $gray-600, + "700": $gray-700, + "800": $gray-800, + "900": $gray-900 +) !default; + +$blue: #6262fb !default; +$indigo: #6610f2 !default; +$purple: #6f42c1 !default; +$pink: #de6c74 !default; +$red: #b00020 !default; +$orange: #fd7e14 !default; +$yellow: #ee9700 !default; +$green: #004733 !default; +$teal: #00bb9c !default; +$cyan: #0dcaf0 !default; + +$colors: ( + "blue": $blue, + "indigo": $indigo, + "purple": $purple, + "pink": $pink, + "red": $red, + "orange": $orange, + "yellow": $yellow, + "green": $green, + "teal": $teal, + "cyan": $cyan, + "black": $black, + "white": $white, + "gray": $gray-600, + "gray-dark": $gray-800 +) !default; + +$primary: #6262fb !default; +$secondary: $gray-600 !default; +$success: $green !default; +$info: $cyan !default; +$warning: $yellow !default; +$danger: $red !default; +$light: $gray-100 !default; +$dark: $gray-900 !default; \ No newline at end of file diff --git a/src/Sass/old/_container.scss b/src/Sass/old/_container.scss new file mode 100644 index 0000000..05293d6 --- /dev/null +++ b/src/Sass/old/_container.scss @@ -0,0 +1,27 @@ + + +@mixin e-sContainer { + width: calc($full-width - ($header-height + ($lge*2))); + margin-left: $header-height; + padding: $lge; +} + +@mixin e-sElementContainer { + width: $full-width; + margin: $center; +} + +@mixin e-sContentContainer { + width: $full-width; + padding: 0 $sm; + display: flex; + flex-wrap: wrap; +} + +.e-sContainer { + @include e-sContainer; +} + +.e-sContentContainer { + @include e-sContentContainer; +} diff --git a/src/Sass/old/_fonts.scss b/src/Sass/old/_fonts.scss new file mode 100644 index 0000000..0928811 --- /dev/null +++ b/src/Sass/old/_fonts.scss @@ -0,0 +1,15 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); +$roboto: "roboto"; +$fontawesome: "fontAwesome"; + +$light: 300; +$normal: 400; +$medium: 500; +$bold: 700; + +$body-size: 1rem; +$title-size: 1.6rem; +$headline-size: 2rem; +$display-size: 2.4rem; + +$navsymbol-size: 2.2rem; \ No newline at end of file diff --git a/src/Sass/old/_formfields.scss b/src/Sass/old/_formfields.scss new file mode 100644 index 0000000..7e0309b --- /dev/null +++ b/src/Sass/old/_formfields.scss @@ -0,0 +1,45 @@ + +form { + margin-top: 24px; +} + +.form-control { + height: 50px; + width: calc($full-width / 1.5); + padding-left: $sm; + border-radius: $sm; + font-size: $body-size; + margin-top: $med; + margin-bottom: $med; +} + +.form-group { + margin: $center; + width: $full-width; + display: flex; + flex-direction: row; + justify-content: center; + label { + display: flex; + flex-direction: row; + width: 20%; + vertical-align: middle; + align-items: center; + justify-content: left; + .label { + display: flex; + align-items: center; + } + input{ + margin-left: $med; + width: $full-width; + } + } +} + +.fieldName { + text-align: left; +} + + + diff --git a/src/Sass/old/_nav.scss b/src/Sass/old/_nav.scss new file mode 100644 index 0000000..b3fc1f1 --- /dev/null +++ b/src/Sass/old/_nav.scss @@ -0,0 +1,91 @@ +.navbar { + width: $tabbar-height + 12px; + height: $full-height; + position: fixed; + display: flex; + flex-direction: column; +} + +.navbar-expand-lg .navbar-nav { + display: flex; + flex-direction: column; + align-items: flex-start; + align-items: center; + a { + margin-top: $med; + } +} + +.dropdown-menu { + position: relative; + left: 70px; + border-radius: 0px; + .show { + display: flex; + position: relative; + left: 170px; + } +} + +.dropdown-menu[data-bs-popper] { + left: 70px; +} + +.navbar-brand { + width: 55px; + height: auto; + margin: 0 auto; +} + +.nav-link { + text-decoration: none; + font-size: 22px; +} + +.dropdown-menu .show a{ + font-size: $body-size; +} + +.nav:after { + clear: both; +} + +.navbarIcon { + display: flex; + justify-content: center; + width: $tabbar-height + 12px; + img { + margin-top: $med; + width: $tabbar-height; + } +} + +.navigationButton { + display: flex; + flex-direction: column; + height: 83px; + align-items: center; + justify-content: center; +} + +.navigationButton button { + width: 80%; + height: 54%; + align-items: center; + background: none; + border: none; + border-radius: 10; + cursor: pointer; +} + +.navIcon { + font-size: $navsymbol-size; +} + +.navbar-collapse { + flex-grow: 0; +} + + + + diff --git a/src/Sass/old/_reset.scss b/src/Sass/old/_reset.scss new file mode 100644 index 0000000..90d361c --- /dev/null +++ b/src/Sass/old/_reset.scss @@ -0,0 +1,363 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + } + + /* Sections + ========================================================================== */ + + /** + * Remove the margin in all browsers. + */ + + html, body { + margin: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + /** + * Render the `main` element consistently in IE. + */ + + main { + display: block; + } + + /** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + /* Grouping content + ========================================================================== */ + + /** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + + hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /* Text-level semantics + ========================================================================== */ + + /** + * Remove the gray background on active links in IE 10. + */ + + a { + background-color: transparent; + } + + /** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + + abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ + } + + /** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /** + * Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* Embedded content + ========================================================================== */ + + /** + * Remove the border on images inside links in IE 10. + */ + + img { + border-style: none; + } + + /* Forms + ========================================================================== */ + + /** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ + } + + /** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + + button, + input { /* 1 */ + overflow: visible; + } + + /** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + + button, + select { /* 1 */ + text-transform: none; + } + + /** + * Correct the inability to style clickable types in iOS and Safari. + */ + + button, + [type="button"], + [type="reset"], + [type="submit"] { + -webkit-appearance: button; + } + + /** + * Remove the inner border and padding in Firefox. + */ + + button::-moz-focus-inner, + [type="button"]::-moz-focus-inner, + [type="reset"]::-moz-focus-inner, + [type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; + } + + /** + * Restore the focus styles unset by the previous rule. + */ + + button:-moz-focusring, + [type="button"]:-moz-focusring, + [type="reset"]:-moz-focusring, + [type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; + } + + /** + * Correct the padding in Firefox. + */ + + fieldset { + padding: 0.35em 0.75em 0.625em; + } + + /** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + + legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ + } + + /** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + + progress { + vertical-align: baseline; + } + + /** + * Remove the default vertical scrollbar in IE 10+. + */ + + textarea { + overflow: auto; + } + + /** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + + [type="checkbox"], + [type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + } + + /** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + + [type="number"]::-webkit-inner-spin-button, + [type="number"]::-webkit-outer-spin-button { + height: auto; + } + + /** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + + [type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ + } + + /** + * Remove the inner padding in Chrome and Safari on macOS. + */ + + [type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + + /** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + + ::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ + } + + /* Interactive + ========================================================================== */ + + /* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + + details { + display: block; + } + + /* + * Add the correct display in all browsers. + */ + + summary { + display: list-item; + } + + /* Misc + ========================================================================== */ + + /** + * Add the correct display in IE 10+. + */ + + template { + display: none; + } + + /** + * Add the correct display in IE 10. + */ + + [hidden] { + display: none; + } + + p { + display: inline; + margin-block-start: 0; + margin-block-end: 0; + margin-inline-start: 0px; + margin-inline-end: 0px; + } + + input:focus-visible { + outline: none; + } \ No newline at end of file diff --git a/src/Sass/old/_sizes.scss b/src/Sass/old/_sizes.scss new file mode 100644 index 0000000..3ca9c1f --- /dev/null +++ b/src/Sass/old/_sizes.scss @@ -0,0 +1,50 @@ + +$full-width: 50%; +$full-height: 100%; + +$force-full-height: 100vh; +$force-full-width: 100vw; + +$header-height: 70px; + +$left: left; +$right: right; + +$small-radius: 3px; +$medium-radius: 5px; +$large-radius: 7px; +$chip-radius: 24px; + +$chip-height: 26px; +$chip-padding: 0 6px; + +$button-height: 30px; +$button-padding: 0 25px; +$button-spacing: 20px; + +$tabbar-height: 58px; +$tabitem-height: 54px; +$tabitem-width: auto; +$tabitem-padding: 18px; + +//table bullshit +$table-width: 90%; +$th-height: 55px; +$tr-height: 50px; + + +//margins and padding +$sm: 8px; +$med: 16px; +$lge: 24px; +$thick: 32px; +$thick-x2: 64px; + +//borders +$thin-border: 1px solid; +$med-border: 3px solid; +$thick-border: 5px solid; +$thick-x2-border: 10px solid; + +//position - I know this isn't a size or anythig, but I'm a fucking renegade.... +$center: 0 auto; \ No newline at end of file diff --git a/src/Sass/old/_table.scss b/src/Sass/old/_table.scss new file mode 100644 index 0000000..51403d3 --- /dev/null +++ b/src/Sass/old/_table.scss @@ -0,0 +1,36 @@ +@import './global.scss'; + +@mixin table { + background: $white; + border-radius: $sm; +} + +.tableBackground { + @include table; + padding: $lge; +} + +.tableInfo { + display: inline-flex; + flex-direction: column; + justify-content: space-between; +} + +table { + @include table; + display: table; + width: $full-width; + justify-content: left; + border: $thin-border + $--es-mono-100; +} + +thead { + background-color: $background; + height: $th-height; + width: 100%; + th { + display: table-row; + justify-content: start; + } +} + diff --git a/src/Sass/old/loginredundant.scss b/src/Sass/old/loginredundant.scss new file mode 100644 index 0000000..0cf809e --- /dev/null +++ b/src/Sass/old/loginredundant.scss @@ -0,0 +1,29 @@ +@import "./global"; + +.loginContainer { + width: calc($full-width / 3.5); + height: $force-full-height; +} + +.loginFormContainer { + width: $table-width; + height: calc($force-full-height / 2); + margin: $center; + top: 50%; + transform: translate(0, -50%); + position: relative; + img { + width: calc($full-width / 2); + display: block; + margin: $center; + } +} + +.forgottenLink { +margin-top: $lge; +} + +.clickables { + margin-top: $med; + margin-left: $lge; +} \ No newline at end of file diff --git a/src/Sass/pill.scss b/src/Sass/pill.scss new file mode 100644 index 0000000..13ed54c --- /dev/null +++ b/src/Sass/pill.scss @@ -0,0 +1,38 @@ +@import './esuiteVariables'; + +.pill { + background-color: #51bdc2; + padding: 3px; + color: white; + display: inline-block; + margin-right: 5px; + margin-top: 5px; + padding-left: 5px; + padding-right: 0px; + border-radius: 5px; + animation-duration: 1s; + animation-fill-mode: both; + + button { + background-color: transparent; + color: inherit; + border: 0; + background: none; + -webkit-appearance: none; + cursor: pointer; + } + + @keyframes flash { + 0%, 50%, 100% { + opacity: 1; + } + + 25%, 75% { + opacity: 0; + } + } + + .flash { + animation-name: flash; + } +} \ No newline at end of file diff --git a/src/Sass/vars.scss b/src/Sass/vars.scss new file mode 100644 index 0000000..2c33c2f --- /dev/null +++ b/src/Sass/vars.scss @@ -0,0 +1,10 @@ +//Frame +$frameWorkAreaPadding: 16px; + +//Left Menu +$leftMenuWidth: 70px; +$leftMenu-background: #2c2c2e; +$leftMenu-color: #ddd; +$leftMenu-HoverColour: $blue; +$leftMenu-selected-color: $blue; +$leftMenu-submenu-open: $blue; diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx new file mode 100644 index 0000000..d6998fc --- /dev/null +++ b/src/components/common/AutoComplete.tsx @@ -0,0 +1,118 @@ +import * as React from "react"; +import Option from "./option"; + +interface AutocompleteProps { + options?: Option[]; + selectedOptions?: Option[]; + placeholder?: string; + onSelect: (item: Option) => void; +} + +interface AutocompleteState { + filteredOptions: Option[]; +} + +export default class Autocomplete extends React.PureComponent { + private inputRef; + constructor(props: AutocompleteProps) { + super(props); + this.state = { filteredOptions: [] } + this.inputRef = React.createRef(); + } + + + private filterOptions(filterTerm: string) { + if (filterTerm !== "") { + let filtered = this.props.options?.filter(x => x.name.toLowerCase().includes(filterTerm.toLowerCase())) ?? []; + filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id)); + this.setState({ + filteredOptions: filtered + }); + } + else { + this.setState({ + filteredOptions: [] + }); + } + } + + private showOptions() { + let filtered = this.props.options?.filter(x => x.name) ?? []; + filtered = filtered.filter(x => !this.props.selectedOptions?.some(y => x._id === y._id)); + this.setState({ + filteredOptions: filtered + }); + } + + private hideAutocomplete = (event: any) => { + if (event.target.classList.contains('autocomplete-text-input')) + return; + + this.setState({ + filteredOptions: [] + }); + } + + private handleBlur = (event: React.FocusEvent) => { + setTimeout(() => { + if ( + this.inputRef.current && + this.inputRef.current.contains(document.activeElement) + ) { + return; + } + + this.setState({ filteredOptions: [] }); + }, 0); + }; + + componentDidMount() { + document.addEventListener('click', this.hideAutocomplete); + } + + componentWillUnmount() { + document.removeEventListener('click', this.hideAutocomplete); + } + + render() { + const { placeholder, onSelect } = this.props; + const { filteredOptions } = this.state; + + return ( +
+ { this.filterOptions(e.target.value) }} + onFocus={(e) => { this.showOptions() }} + placeholder={placeholder} + /> + {filteredOptions.length > 0 && ( +
    + {filteredOptions.map((x, i) => +
  • + +
  • + )} +
+ )} +
+ ) + } +} diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx new file mode 100644 index 0000000..e44077d --- /dev/null +++ b/src/components/common/Button.tsx @@ -0,0 +1,93 @@ +import react, { SyntheticEvent } from 'react'; +import { Link } from 'react-router-dom'; + +export enum ButtonType{ + none, + primary, + secondary, + success, + danger, + warning, + info, + light, + dark, + link +} + +export interface ButtonProps{ + testid?: string; + className?: string; + id?:string; + name?:string; + keyValue?: T; + children: React.ReactNode; + buttonType : ButtonType; + disabled?: boolean; + to?: string; + onClick?: ( keyValue : T | undefined ) => void; +} + +class Button extends react.Component> { + Click = (e : SyntheticEvent) => { + const {keyValue, onClick} = this.props; + + if (onClick) + { + e.preventDefault(); + onClick(keyValue); + } + } + + render() { + const { id, className, children, buttonType, disabled, name, to, testid } = this.props; + + let classNames = ""; + + switch (buttonType) + { + case ButtonType.primary: + classNames = "btn btn-primary"; + break; + case ButtonType.secondary: + classNames = "btn btn-secondary"; + break; + case ButtonType.success: + classNames = "btn btn-success"; + break; + case ButtonType.danger: + classNames = "btn btn-danger"; + break; + case ButtonType.warning: + classNames = "btn btn-warning"; + break; + case ButtonType.info: + classNames = "btn btn-info"; + break; + case ButtonType.light: + classNames = "btn btn-light"; + break; + case ButtonType.dark: + classNames = "btn btn-dark"; + break; + case ButtonType.link: + classNames = "btn btn-link"; + break; + case ButtonType.none: + classNames = "btn btn-default" + break; + } + + if (className !== undefined) + { + classNames += " " + className; + } + + if (to !== undefined){ + return {children} + } + + return ; + } +} + +export default Button; \ No newline at end of file diff --git a/src/components/common/ConfirmButton.tsx b/src/components/common/ConfirmButton.tsx new file mode 100644 index 0000000..d5c52ef --- /dev/null +++ b/src/components/common/ConfirmButton.tsx @@ -0,0 +1,57 @@ +import react from 'react'; +import Button, { ButtonType } from './Button'; + +export interface ConfirmButtonProps{ + delayMS? : number; + buttonType : ButtonType; + keyValue: T; + children: React.ReactNode; + confirmMessage?: React.ReactNode; + onClick?: ( keyValue? : T ) => void; +} + +export interface ConfirmButtonState{ + firstClick : boolean +} + +class ConfirmButton extends react.Component, ConfirmButtonState > { + state : ConfirmButtonState = { + firstClick : false + } + + FirstClick = () => { + const firstClick = true; + this.setState({firstClick}); + + let { delayMS } = this.props; + if (delayMS === undefined) + delayMS = 5000; + + setTimeout(() => { + console.log(`updating state`) + const firstClick = false; + this.setState({firstClick}); + }, delayMS); + } + + SecondClick = () => { + const {keyValue, onClick} = this.props; + + if (onClick) + onClick(keyValue); + } + + render() { + const { buttonType, children, confirmMessage } = this.props; + const { firstClick } = this.state; + + return ( + <> + {!firstClick && } + {firstClick && } + + ); + } +} + +export default ConfirmButton; \ No newline at end of file diff --git a/src/components/common/CustomFieldsEditor.tsx b/src/components/common/CustomFieldsEditor.tsx new file mode 100644 index 0000000..dcdaa17 --- /dev/null +++ b/src/components/common/CustomFieldsEditor.tsx @@ -0,0 +1,99 @@ +import { faAdd } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React from "react"; +import { CustomField } from "../../modules/manager/customfields/services/customFieldsService"; +import { Paginated } from "../../services/Paginated"; +import { GeneralIdRef } from "../../utils/GeneralIdRef"; +import CustomFieldPicker from "../pickers/CustomFieldPicker"; +import Column from "./columns"; +import Table from "./Table"; +import Button, { ButtonType } from "./Button"; + +export type CustomFieldEditorAdd = ( newField : CustomField) => void; +export type CustomFieldEditorDelete = ( keyValue : any ) => void; + +interface CustomFieldsEditorProps { + name: string; + label: string; + error?: string; + value: CustomField[]; + exclude : CustomField[]; + onAdd? : CustomFieldEditorAdd; + onDelete?: CustomFieldEditorDelete +} + +interface CustomFieldsEditorState { + id : GeneralIdRef | undefined, + displayName : string | undefined +} + +class CustomFieldsEditor extends React.Component { + columns : Column[] = [ + { key: "name", label: "Name", order: "asc" }, + ]; + + state : CustomFieldsEditorState = { + id : undefined, + displayName: undefined + } + + handleAdd = () => { + const { onAdd } = this.props; + + if (onAdd) + { + const {id, displayName} = this.state; + + if (id) + { + const newField: CustomField = { + id: id.id ?? BigInt(-1), + name: String(displayName), + fieldType: "", + defaultValue: "", + minEntries: 1, + guid : id.guid + }; + + onAdd(newField); + + this.setState( { + id : undefined, + displayName: undefined + }); + } + } + } + + handleChange = (name: string, value: GeneralIdRef, displayValue : string) => { + this.setState({ + id : value, + displayName : displayValue + }); + }; + + render() { + const { value, exclude, label, onDelete } = this.props; + + const paginated : Paginated = { + count: 0, + page: 1, + pageSize: 10, + totalPages: 0, + data: value + } + + return
+ {label} + +
+ + +
+ ; + } +} + +export default CustomFieldsEditor; diff --git a/src/components/common/DateView.tsx b/src/components/common/DateView.tsx new file mode 100644 index 0000000..1a5bc62 --- /dev/null +++ b/src/components/common/DateView.tsx @@ -0,0 +1,9 @@ +interface DateViewProps{ + value : Date; +} + +export function DateView( props: DateViewProps) : JSX.Element { + const options : Intl.DateTimeFormatOptions = { year: '2-digit', month: 'short', day: 'numeric', hour12:true, hour:"2-digit", minute:"2-digit" }; + + return <>{props.value.toLocaleString(undefined,options)} +} \ No newline at end of file diff --git a/src/components/common/ErrorBlock.tsx b/src/components/common/ErrorBlock.tsx new file mode 100644 index 0000000..1dcbff4 --- /dev/null +++ b/src/components/common/ErrorBlock.tsx @@ -0,0 +1,13 @@ +interface ErrorProps { + error?: string +} + +function ErrorBlock(props : ErrorProps) +{ + const { error } = props; + + return ( <>{error &&
{error}
} ); +} + +export default ErrorBlock; + diff --git a/src/components/common/Form.tsx b/src/components/common/Form.tsx new file mode 100644 index 0000000..de63740 --- /dev/null +++ b/src/components/common/Form.tsx @@ -0,0 +1,957 @@ +import React from "react"; +import Joi from "joi"; +import Input, { InputType } from "./Input"; +import ToggleSlider from "./ToggleSlider"; +import Select from "./Select"; +import Option from "./option"; +import { GeneralIdRef } from "../../utils/GeneralIdRef"; +import SequencePicker from "../pickers/SequencePicker"; +import GlossaryPicker from "../pickers/GlossaryPicker"; +import CustomFieldsEditor, { CustomFieldEditorAdd, CustomFieldEditorDelete } from "./CustomFieldsEditor"; +import { CustomField, numberParams, textParams } from "../../modules/manager/customfields/services/customFieldsService"; +import FormTemplatePicker from "../pickers/FormTemplatePicker"; +import { CustomFieldValue, CustomFieldValues, Glossary } from "../../modules/manager/glossary/services/glossaryService"; +import TemplateEditor from "./TemplateEditor"; +import DomainPicker from "../pickers/DomainPicker"; +import UserPicker from "../pickers/UserPicker"; +import Button, { ButtonType } from "./Button"; +import Expando from "./expando"; +import ErrorBlock from "./ErrorBlock"; +import SsoProviderPicker from "../pickers/SsoProviderPicker"; + +export interface FormError { + [key: string]: string; +} + +export interface FormData { + [key: string]: string | number | boolean | CustomFieldValue[] | GeneralIdRef | CustomField[] | bigint | Glossary | null | undefined; +} + +export interface businessValidationError { + path: string; + message: string; +} + +export interface businessValidationResult { + details: businessValidationError[]; +} + +export interface propertyValue { + name: string; + value: string | boolean | number; +} + +export interface joiSchema { + [key: string]: object; +} + +export interface FormState { + loaded: boolean; + data: FormData; + customFields?: CustomField[]; + errors: FormError; + redirect?: string; +} + +export interface Match

{ + params: P; + isExact: boolean; + path: string; + url: string; +} + +export interface State { + from: Location; +} + +export interface LocationProps { + hash: string; + pathname: string; + search: string; + state: State; +} + +export interface FormProps

{ + location: LocationProps; + match: Match

; + staticContext?: any; +} + +class Form, FS extends FormState> extends React.Component { + schema: joiSchema = {}; + + validate = (data: FormData) => { + let options: Joi.ValidationOptions = { + context: {}, + abortEarly: false, + }; + + const { customFields } = this.state; + + let schema = this.schema; + if (customFields !== undefined) { + for (const customfield of customFields) { + const name = "customfield_" + customfield.id; + switch (customfield.fieldType) { + case "Number": + if (customfield.parameters !== undefined) { + const parameters: numberParams = JSON.parse(customfield.parameters!); + + options.context![name + "_minEntries"] = customfield.minEntries; + if (parameters.minValue) options.context![name + "_minValue"] = Number(parameters.minValue); + if (parameters.maxValue) options.context![name + "_maxValue"] = Number(parameters.maxValue); + + let minCheck = options.context![name + "_minValue"] + ? Joi.number() + .empty("") + .min(options.context![name + "_minValue"]) + : Joi.number().empty(""); + let maxCheck = options.context![name + "_maxValue"] + ? Joi.number() + .empty("") + .max(options.context![name + "_maxValue"]) + : Joi.number().empty(""); + + schema[name] = Joi.array() + .min(1) + .items( + Joi.object({ + displayValue: Joi.string().allow(""), + value: Joi.when("$" + name + "_minEntries", { + is: 0, + then: Joi.number().empty(""), + otherwise: Joi.number().required(), + }) + .when("$" + name + "_minValue", { + is: Joi.number(), + then: minCheck, + }) + .when("$" + name + "_maxValue", { + is: Joi.number(), + then: maxCheck, + }) + .label(customfield.name), + }) + ); + } else { + schema[name] = Joi.optional().label(customfield.name); + } + break; + default: + schema[name] = Joi.optional().label(customfield.name); + } + } + } + + const joiSchema = Joi.object(schema); + const { error } = joiSchema.validate(data, options); + let errors: FormError = {}; + if (error) { + if (error.details === undefined) { + errors[error.name] = error.message; + } else { + for (let item of error.details) { + errors[item.path[0]] = item.message; + } + } + } + + return errors; + }; + + GetCustomFieldValues = (customField: CustomField) => { + const name = "customfield_" + customField.id; + + const { data } = this.state; + + const codedValue = data[name]; + + let values: CustomFieldValue[] = []; + + switch (customField.fieldType) { + case "FormTemplate": + if (codedValue !== undefined) { + const formTemplateValue = { value: JSON.stringify(codedValue as GeneralIdRef) }; + values.push(formTemplateValue); + } + break; + case "Sequence": + if (codedValue !== undefined) { + values = codedValue as CustomFieldValue[]; + } + break; + case "Glossary": + if (codedValue !== undefined) { + values = codedValue as CustomFieldValue[]; + } + break; + case "Domain": + if (codedValue !== undefined) { + values = codedValue as CustomFieldValue[]; + } + break; + case "Text": + const textParameters: textParams = JSON.parse(customField.parameters!); + if (textParameters.multiLine) { + const textValue = { + value: codedValue === undefined ? customField.defaultValue : codedValue, + displayValue: codedValue === undefined ? customField.defaultValue : codedValue, + } as CustomFieldValue; + values.push(textValue); + } else { + if (codedValue === undefined) { + const numberValue = { + value: customField.defaultValue, + displayValue: customField.defaultValue, + } as CustomFieldValue; + values.push(numberValue); + } else { + values = codedValue as CustomFieldValue[]; + } + } + break; + case "Number": + if (codedValue === undefined) { + const numberValue = { + value: customField.defaultValue, + displayValue: customField.defaultValue, + } as CustomFieldValue; + values.push(numberValue); + } else { + values = codedValue as CustomFieldValue[]; + } + + break; + default: + const textValue = { value: codedValue === undefined ? customField.defaultValue : String((codedValue as CustomFieldValue[])[0].displayValue) }; + values.push(textValue); + break; + } + + return values; + }; + + CustomFieldValues = () => { + const { customFields } = this.state; + + let result: CustomFieldValues[] = []; + + if (customFields === undefined) { + return result; + } + + for (const customfield of customFields) { + const values = this.GetCustomFieldValues(customfield); + + const id: GeneralIdRef = { + id: customfield.id, + guid: customfield.guid, + }; + + const newItem: CustomFieldValues = { + id, + values, + }; + + result.push(newItem); + } + + return result; + }; + + setCustomFieldValues(data: object, customFieldValues: CustomFieldValues[], customFields: CustomField[]) { + if (customFieldValues !== undefined) { + for (const x of customFieldValues) { + const customfieldName = "customfield_" + x.id.id; + + switch (this.getCustomFieldType(x, customFields).toLowerCase()) { + case "glossary": + case "domain": + case "number": + case "text": + (data as any)[customfieldName] = x.values.map((x) => { + return { + displayValue: x.displayValue, + value: x.value, + }; + }); + break; + case "formtemplate": + case "multilinetext": + (data as any)[customfieldName] = x.values[0].value; + break; + default: + (data as any)[customfieldName] = x.values; + break; + } + } + } + } + + getCustomFieldType = (field: CustomFieldValues, childCustomFieldDefinition: CustomField[]): string => { + const fieldDefinition = childCustomFieldDefinition.filter((x) => x.id === field.id.id)[0]; + + if (fieldDefinition.parameters) { + const textParameters: textParams = JSON.parse(fieldDefinition.parameters!); + if (textParameters.multiLine) return "multilinetext"; + } + + return fieldDefinition.fieldType; + }; + + handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + const submitEvent = e.nativeEvent as SubmitEvent; + + const submitter = submitEvent.submitter as any; + + const errors = this.validate(this.state.data); + this.setState({ errors: errors }); + + const disabled = Object.keys(errors).length > 0; + + if (disabled) return; + + this.doSubmit(submitter.name); + }; + + doSubmit = async (buttonName: string) => {}; + + handleGeneralError = (ex: any) => { + const errors: FormError = { ...this.state.errors }; + + if (ex.response) { + errors._general = ex.response.data.detail; + } else { + errors._general = ex.message; + } + + this.setState({ errors }); + }; + + handleChange = (e: React.ChangeEvent) => { + const input = e.currentTarget; + + const data: FormData = { ...this.state.data }; + + if ((input as any).type === InputType.checkbox) { + data[input.name] = !data[input.name]; + } else data[input.name] = input.value; + + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleTextAreaChange = (e: React.ChangeEvent) => { + const input = e.currentTarget; + + const data: FormData = { ...this.state.data }; + + data[input.name] = input.value; + + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleCustomFieldChange = (e: React.ChangeEvent) => { + const input = e.currentTarget; + + const data: FormData = { ...this.state.data }; + + switch ((input as any).type) { + case InputType.checkbox: + data[input.name] = !data[input.name]; + break; + default: + const customFieldValue: CustomFieldValue = { + displayValue: input.value, + value: input.value, + }; + + data[input.name] = [customFieldValue]; + break; + } + + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleTemplateEditorChange = (name: string, value: string) => { + const data: FormData = { ...this.state.data }; + + data[name] = value; + + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleSelectChange = (e: React.ChangeEvent) => { + const input = e.currentTarget; + + const data: FormData = { ...this.state.data }; + data[input.name] = input.value; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handlePickerChange = (name: string, value: GeneralIdRef) => { + const data: FormData = { ...this.state.data }; + data[name] = value; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleDomainPickerChange = (name: string, values: CustomFieldValue[]) => { + const data: FormData = { ...this.state.data }; + data[name] = values; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleGlossaryPickerChange = (name: string, values: CustomFieldValue[]) => { + const data: FormData = { ...this.state.data }; + data[name] = values; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleTemplateFormPickerChange = (name: string, value: GeneralIdRef) => { + const data: FormData = { ...this.state.data }; + data[name] = value; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleUserPickerChange = (name: string, value: GeneralIdRef) => { + const data: FormData = { ...this.state.data }; + data[name] = value; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleSsoProviderPickerChange = (name: string, value: GeneralIdRef) => { + const data: FormData = { ...this.state.data }; + data[name] = value; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + handleToggleChange = (e: React.ChangeEvent) => { + const input = e.currentTarget; + const { name, checked } = input; + + const data: FormData = { ...this.state.data }; + data[name] = checked; + const errors = this.validate(data); + + this.setState({ data, errors }); + }; + + renderButton( + label: string, + name?: string, + onClick?: (keyValue: any) => {}, + testid?: string, + enabled: boolean = true, + buttonType: ButtonType = ButtonType.primary, + overrideErrorChecking: boolean = false + ) { + const { errors } = this.state; + + let disabled = !enabled || Object.keys(errors).filter((x) => !x.startsWith("_")).length > 0; + + if (overrideErrorChecking) disabled = !enabled; + + return ( + + ); + } + + renderError(name: string) { + const { errors } = this.state; + + return ; + } + + renderInput( + name: string, + label: string, + type: InputType = InputType.text, + readOnly = false, + defaultValue: string = "", + placeHolder: string = "", + maxLength: number = 0, + visible: boolean = true, + autoComplete: string | undefined = undefined + ) { + const { data, errors } = this.state; + + let value = data[name]; + + let cleanValue: string | undefined; + if (value === undefined) { + cleanValue = defaultValue; + } else if (typeof value === "string") { + cleanValue = value as string; + } else if (typeof value === "number") { + cleanValue = String(value); + } else if (typeof value === "boolean") { + cleanValue = String(value); + } else if (value as CustomFieldValue) { + cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value.toString(); + } + + if (readOnly) { + return ( + + ); + } else { + return ( + + ); + } + } + + renderInputWithChangeEvent( + name: string, + label: string, + type: InputType = InputType.text, + readOnly = false, + handleChangeEvent: any, + defaultValue: string = "", + placeHolder: string = "", + maxLength: number = 0 + ) { + const { data, errors } = this.state; + + let value = data[name]; + + let cleanValue: string | undefined; + if (value === undefined) { + cleanValue = defaultValue; + } else if (typeof value === "string") { + cleanValue = value as string; + } else if (typeof value === "number") { + cleanValue = String(value); + } else if (typeof value === "boolean") { + cleanValue = String(value); + } else if (value as CustomFieldValue) { + cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value.toString(); + } + + if (readOnly) { + return ( + + ); + } else { + return ( + + ); + } + } + + renderInputNumber(name: string, label: string, readOnly = false, defaultValue: string = "", min?: number, max?: number, step: number = 1) { + const { data, errors } = this.state; + + let value = data[name]; + + let cleanValue: string | undefined; + if (value === undefined) { + cleanValue = defaultValue; + } else if (typeof value === "string") { + cleanValue = value as string; + } else if (typeof value === "number") { + cleanValue = String(value); + } else if (typeof value === "boolean") { + cleanValue = String(value); + } else if (value as CustomFieldValue) { + cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value.toString(); + } + + if (readOnly) { + return ; + } else { + return ( + + ); + } + } + + renderInputTextarea(includeLabel: boolean, name: string, label: string, readOnly = false, defaultValue: string = "") { + const { data, errors } = this.state; + + let value = data[name]; + + let cleanValue: string | undefined; + if (value === undefined) { + cleanValue = defaultValue; + } else if (typeof value === "string") { + cleanValue = value as string; + } + + if (readOnly) { + return ( + + ); + } else { + return ( + + ); + } + } + + renderCustomFieldInput(includeLabel: boolean, name: string, label: string, type: InputType = InputType.text, readOnly = false, defaultValue: string = "") { + const { data, errors } = this.state; + + let value = data[name]; + + let cleanValue: string | undefined; + if (value === undefined) { + cleanValue = defaultValue; + } else if (typeof value === "string") { + cleanValue = value as string; + } else if (typeof value === "number") { + cleanValue = String(value); + } else if (typeof value === "boolean") { + cleanValue = String(value); + } else if (value as CustomFieldValue) { + const customFieldValue = value as CustomFieldValue[]; + cleanValue = customFieldValue[0].displayValue ?? customFieldValue[0].value?.toString(); + } + + if (readOnly) { + return ; + } else { + return ( + + ); + } + } + + renderCustomFieldNumber( + includeLabel: boolean, + name: string, + label: string, + readOnly = false, + defaultValue: string = "", + min?: number, + max?: number, + step: number = 1 + ) { + const { data, errors } = this.state; + + let values: CustomFieldValue[] = data[name] as CustomFieldValue[]; + + let value: CustomFieldValue | undefined = undefined; + + if (values) { + if (values.length > 0) { + value = values[0]; + } + } + + let cleanValue: string | undefined; + if (value === undefined) { + cleanValue = defaultValue; + } else if (typeof value === "string") { + cleanValue = value as string; + } else if (typeof value === "number") { + cleanValue = String(value); + } else if (value as CustomFieldValue) { + cleanValue = (value as CustomFieldValue).displayValue ?? (value as CustomFieldValue).value?.toString(); + } + + if (readOnly) { + return ; + } else { + return ( + + ); + } + } + + renderTemplateEditor(className: string, name: string, label: string, allowCustomFields: boolean) { + const { data } = this.state; + + let value = data[name] as string; + + return ( +

+ + +
+ ); + } + + renderSelect(name: string, label: string, options: Option[]) { + const { data, errors } = this.state; + + return } + {type !== InputType.textarea && + 0 ? maxLength: undefined} + autoComplete={autoComplete} + />} + {type === InputType.password &&
{ + const newIcon = showPasswordIcon === faEye ? faEyeSlash : faEye; + setShowPasswordIcon(newIcon) + } + } /> +
} + + + + ); +} + +export default Input; diff --git a/src/components/common/Loading.tsx b/src/components/common/Loading.tsx new file mode 100644 index 0000000..f54f2c5 --- /dev/null +++ b/src/components/common/Loading.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import LoadingPanel from "./LoadingPanel"; + +interface Loading2Props { + loaded : boolean + children: React.ReactNode; +} + +interface Loading2State { +} + +class Loading extends React.Component { + state = { loaded : false } + + render() { + const { loaded, children } = this.props; + + if (!loaded) { + return () + } + else { + return (<>{children}); + } + } +} + +export default Loading; \ No newline at end of file diff --git a/src/components/common/LoadingPanel.tsx b/src/components/common/LoadingPanel.tsx new file mode 100644 index 0000000..aa81610 --- /dev/null +++ b/src/components/common/LoadingPanel.tsx @@ -0,0 +1,10 @@ +import { FunctionComponent } from "react"; + +interface LoadingProps { +} + +const LoadingPanel: FunctionComponent = () => { + return (
Loading
); +} + +export default LoadingPanel; \ No newline at end of file diff --git a/src/components/common/MultiSelect.tsx b/src/components/common/MultiSelect.tsx new file mode 100644 index 0000000..d4d9e05 --- /dev/null +++ b/src/components/common/MultiSelect.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import Option from "./option"; +import Autocomplete from "./AutoComplete"; +import Pill from "./Pill"; + +interface MultiSelectProps { + includeLabel? : boolean, + name : string, + label : string, + error? : string, + + // value : unknown + options? : Option[], + selectedOptions : Option[], + // includeBlankFirstEntry? : boolean, + // onChange?: (e: React.ChangeEvent) => void; + + onAdd: (item: Option) => void; + onDelete: (item: Option) => void; +} + +interface MultiSelectState { + +} + +class MultiSelect extends React.Component { + + handleDelete = ( id: string) => { + const { options, onDelete } = this.props; + + const foundItem : Option | undefined = options?.filter( x => x._id === id)[0]; + + if (foundItem) + onDelete(foundItem); + } + + render() { + const { includeLabel, name, label, error, options, selectedOptions, onAdd + // value, includeBlankFirstEntry, onChange, ...rest + } = this.props; + + let selectedBlock = <>; + + if (selectedOptions.length > 0) + { + selectedBlock = <> + { + selectedOptions.map( x => + + ) + } + + } + + return ( +
+ {(includeLabel===undefined || includeLabel===true) && } + +
+ + {selectedBlock} +
+ {error &&
{error}
} +
+ ); + } +} + +export default MultiSelect; \ No newline at end of file diff --git a/src/components/common/Pagination.tsx b/src/components/common/Pagination.tsx new file mode 100644 index 0000000..fd0ddc4 --- /dev/null +++ b/src/components/common/Pagination.tsx @@ -0,0 +1,109 @@ +import React from "react"; +import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Paginated } from "../../services/Paginated"; +import Button, { ButtonType } from "./Button"; +import { InputType } from "./Input"; + +interface PaginationProps { + data : Paginated + onChangePage : (page: number, pageSize: number) => void; + onUnselect?: () => void; +} + +interface PaginationState { + +} + +class Pagination extends React.Component, PaginationState> { + state = { } + + changePage( page : number, pageSize : number ){ + const {onChangePage, onUnselect} = this.props; + + onChangePage(page, pageSize); + if (onUnselect) { + onUnselect(); + } + } + + clickFirst = () => { + const { pageSize } = this.props.data; + + this.changePage(1, pageSize ); + } + + clickPrevious = () => { + const { page, pageSize } = this.props.data; + + this.changePage(page - 1, pageSize); + } + + clickNext = () => { + const { page, pageSize } = this.props.data; + + this.changePage(page + 1, pageSize); + } + + clickLast = () => { + const { totalPages, pageSize } = this.props.data; + + this.changePage(totalPages, pageSize); + } + + PageSizeChange = (e: React.ChangeEvent) => { + const input = e.currentTarget; + const { page } = this.props.data; + + let newPageSize : number = +input.value; + + this.changePage(page, newPageSize); + } + + handlePageSelect = ( e : React.ChangeEvent) => + { + const { pageSize, totalPages } = this.props.data; + const input = e.currentTarget; + + const newPage = Number(input.value); + + if (1 <= newPage && newPage <= totalPages) { + this.changePage(newPage, pageSize); + } + } + + render() { + + const { data } = this.props; + + if (data === null) + return <> + + const pageSizeOptions = [{ _id: "10", name: "10" }, + { _id: "25", name: "25" }, + { _id: "50", name: "50" }, + { _id: "100", name: "100" }]; + + return (
+ + + + + + + of {data.totalPages} + + + {data.count} Items + +
); + } +} + +export default Pagination; \ No newline at end of file diff --git a/src/components/common/Permission.tsx b/src/components/common/Permission.tsx new file mode 100644 index 0000000..3c0d66f --- /dev/null +++ b/src/components/common/Permission.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import authenticationService from "../../modules/frame/services/authenticationService"; + + +interface PermissionProps{ + privilegeKey : string; + children: React.ReactNode; +} + +class Permission extends React.Component { + render() { + const { privilegeKey, children } = this.props; + const hasAccess = authenticationService.hasAccess( privilegeKey ); + + if (hasAccess === false) + return ( <> ); + else + return ( <>{children} ); + } +} + +export default Permission; \ No newline at end of file diff --git a/src/components/common/Pill.tsx b/src/components/common/Pill.tsx new file mode 100644 index 0000000..bcb9db3 --- /dev/null +++ b/src/components/common/Pill.tsx @@ -0,0 +1,52 @@ +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as React from "react"; + +interface PillProps { + pillKey: any; + displayText: string; + enabled?: boolean; + onClick: (item: any) => void; + className?: string; + readOnly?: boolean; + flash?: boolean; +} + +export default function Pill({ + pillKey, + displayText, + enabled = true, + className, + onClick, + readOnly, + flash = false +}: PillProps) { + const handleOnClick = () => { + onClick(pillKey); + }; + + let classNames = "pill"; + + if (className) + classNames += " " + className; + + if (flash) + classNames += " flash"; + + return ( +
+ {displayText} + {!readOnly && ( + + )} +
+ ); +} diff --git a/src/components/common/Redirect.tsx b/src/components/common/Redirect.tsx new file mode 100644 index 0000000..bd884d8 --- /dev/null +++ b/src/components/common/Redirect.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +interface RedirectProps { + to : string +} + +interface RedirectState { + +} + +class Redirect extends React.Component { + render() { + const {to} = this.props; + + window.location.replace(to); + + return null; + } +} + +export default Redirect; \ No newline at end of file diff --git a/src/components/common/Select.tsx b/src/components/common/Select.tsx new file mode 100644 index 0000000..5439698 --- /dev/null +++ b/src/components/common/Select.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import Option from "./option"; + +export interface SelectProps { + includeLabel? : boolean, + name : string, + label : string, + error? : string, + value : unknown + options? : Option[], + includeBlankFirstEntry? : boolean, + onChange?: (e: React.ChangeEvent) => void; +} + +function GenerateValue( value : unknown ) +{ + let actualValue = value; + + if (value === true) + return "true"; + if (value === false) + return "false"; + + return actualValue as string | number | readonly string[] | undefined; +} + +class Select extends React.Component { + render() { + const { includeLabel, name, label, error, value, options, includeBlankFirstEntry, onChange, ...rest } = this.props; + + const actualValue = GenerateValue( value); + + return ( +
+ {(includeLabel===undefined || includeLabel===true) && } + {!options && } + {options && + + } + {error &&
{error}
} +
+ ); + } +}; + +export default Select; \ No newline at end of file diff --git a/src/components/common/Tab.tsx b/src/components/common/Tab.tsx new file mode 100644 index 0000000..34a3449 --- /dev/null +++ b/src/components/common/Tab.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +interface TabProps{ + label: string; + children : JSX.Element | JSX.Element[]; +} + +class Tab extends React.Component { + render() { + return ( +
+ +
+ ); + } +} + +export default Tab; \ No newline at end of file diff --git a/src/components/common/TabHeader.tsx b/src/components/common/TabHeader.tsx new file mode 100644 index 0000000..d91d44f --- /dev/null +++ b/src/components/common/TabHeader.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +interface TabHeaderProps{ + isActive : boolean, + label : string + onClick : any; +} + +class TabHeader extends React.Component { + + onClick = () => { + const { label, onClick } = this.props; + onClick(label); + }; + + render() { + const { + onClick, + props: { isActive, label }, + } = this; + + let className = "tab-list-item"; + + if (isActive === true) { + className += " tab-list-active"; + } + + return ( +
  • + {label} +
  • + ); + } +} + +export default TabHeader; \ No newline at end of file diff --git a/src/components/common/Table.tsx b/src/components/common/Table.tsx new file mode 100644 index 0000000..e9f49cb --- /dev/null +++ b/src/components/common/Table.tsx @@ -0,0 +1,70 @@ +import { Component } from 'react'; +import { Paginated } from '../../services/Paginated'; +import Column from './columns'; +import TableBody, { AuditParams } from './TableBody'; +import TableFooter from './TableFooter'; +import TableHeader from './TableHeader'; +import debounce from 'lodash.debounce'; + +export interface PublishedTableProps { + data: Paginated, + sortColumn? : Column, + selectedRow? : T; + onChangePage? : (page: number, pageSize : number) => {}; + onSort? : (sortColumn : Column) => void; + onSearch?: ( name: string, value: string) => void; + canEdit? : ( item : T ) => boolean; + canDelete?: ( item : T ) => boolean; + onDelete?: ( item? : T ) => void; + onSelectRow?: (item: T) => void; + onUnselectRow?: () => void; +} + +export interface TableProps extends PublishedTableProps { + keyName : string; + columns : Column[]; + editPath? : string; + onAuditParams?: ( item : T ) => AuditParams; + secondaryAudit? : boolean; +} + +interface TableState{ + debouncedOnSearch?: any +} + +class Table extends Component, TableState> { + state : TableState = { + } + + componentDidMount(): void { + const {onSearch } = this.props; + const debounceDelay = 200; + const debouncedOnSearch = onSearch === undefined ? undefined : debounce(onSearch, debounceDelay); + + this.setState( { + debouncedOnSearch, + }) + } + + render() { + const { data, keyName, selectedRow, columns, sortColumn, editPath, canEdit, canDelete, onSort, onChangePage, onDelete, onAuditParams, onSelectRow, secondaryAudit, onUnselectRow } = this.props; + const { debouncedOnSearch } = this.state; + + const showEdit = (editPath != null) && (editPath !== ""); + const showDelete = onDelete != null; + const showAudit = onAuditParams != null; + const showSecondaryAudit = showAudit && secondaryAudit === true; + + return ( + <> +
    + + + +
    + + ); + } +} + +export default Table; \ No newline at end of file diff --git a/src/components/common/TableBody.tsx b/src/components/common/TableBody.tsx new file mode 100644 index 0000000..21fc6b4 --- /dev/null +++ b/src/components/common/TableBody.tsx @@ -0,0 +1,138 @@ +import React, { Component } from "react"; +import deepFind from "../../utils/deepfind"; +import Column from "./columns"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBook, faBookJournalWhills, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { Link } from "react-router-dom"; +import ConfirmButton from "./ConfirmButton"; +import { Buffer } from 'buffer'; +import Button, { ButtonType } from "./Button"; +import { DateView } from "./DateView"; + +export interface AuditParams{ + entityName : string, + primaryKey : string +} + +export interface TableBodyProps{ + data : T[] | undefined; + keyName : string; + columns : Column[]; + editPath? : string; + selectedRow? : T; + canEdit? : ( item : T ) => boolean; + canDelete? : ( item : T ) => boolean; + onDelete?: ( item? : T ) => void; + onAuditParams?: ( item : T ) => AuditParams; + onSelectRow? : ( item : T ) => void; + showSecondaryAudit : boolean; +} + +class TableBody extends Component> { + resolvePath = ( path : string, args : string[]) => { + let modifiedPath = path; + + let index : number = 0; + while ( index < args.length ) + { + modifiedPath = modifiedPath.replace("{"+index+"}", args[index]) + index++; + } + + return modifiedPath; + } + + renderCell = (item : T, column : Column) => { + const {keyName} = this.props; + if (column.content) return column.content(item); + + const foundItem = deepFind(item, column.path || column.key) + + let columnContent : JSX.Element; + + if (foundItem instanceof Date) { + columnContent = + } + else if (typeof foundItem === "object"){ + columnContent = <>; + } + else { + columnContent = <> + {foundItem} + ; + } + + const linkPath = column.link; + + if (linkPath !== undefined){ + const resolvedlinkPath = this.resolvePath( linkPath, [ (item as any)[keyName] ] ); + columnContent = {columnContent}; + } + + return <> + {columnContent} + ; + }; + + clickRow = ( value : T ) => + { + const { onSelectRow } = this.props; + + if (onSelectRow !== undefined) + onSelectRow( value ); + } + + createKey = (item : T, column : Column) => { + const { keyName } = this.props; + + return (item as any)[keyName] + '_' + (column.path || column.key); + }; + + handleAuditParams = ( item : T, primaryOnly : boolean ) => { + const { onAuditParams } = this.props; + if (onAuditParams !== undefined) { + var auditParams = onAuditParams(item); + let json = JSON.stringify(auditParams); + var params = Buffer.from(json).toString('base64') ; + + var queryString = ""; + if (primaryOnly===false) + queryString += "?primaryOnly=" + primaryOnly; + + return "/audit/" + params + queryString; + } + + return ""; + } + + render() { + const { data, keyName, selectedRow, columns, editPath, canEdit, canDelete, onDelete, onAuditParams, showSecondaryAudit } = this.props; + const showDelete:boolean = onDelete != null; + const showEdit:boolean = (editPath != null) && (editPath !== ""); + const showAudit:boolean = onAuditParams !== undefined; + + return ( + + {data?.map((item) => { + let classNames = ""; + if (selectedRow === item) + { + classNames+="table-primary"; + } + + return ( + {columns.map((column) => ( + this.clickRow(item)}>{this.renderCell(item, column)} + ))} + {showEdit && {(canEdit === undefined || canEdit(item)) && }} + {showDelete && {(canDelete === undefined || canDelete(item)) && }} + {showAudit && } + {showAudit && showSecondaryAudit && } + ) + })} + + ); + } +} + +export default TableBody; diff --git a/src/components/common/TableFooter.tsx b/src/components/common/TableFooter.tsx new file mode 100644 index 0000000..70391d0 --- /dev/null +++ b/src/components/common/TableFooter.tsx @@ -0,0 +1,40 @@ +import React, { Component } from "react"; +import { Paginated } from "../../services/Paginated"; +import Column from "./columns"; +import Pagination from "./Pagination"; + +export interface TableFooterProps{ + data : Paginated; + columns : Column[]; + showEdit : boolean; + showDelete : boolean; + showAudit : boolean; + showSecondaryAudit : boolean; + onChangePage?: (page: number, pageSize: number) => void; + onUnselectRow?: () => void; +} + +class TableFooter extends Component> { + render() { + const { data, columns, showEdit, showDelete, showAudit, showSecondaryAudit, onChangePage, onUnselectRow} = this.props; + + let staticColumnCount = 0; + if (showEdit) staticColumnCount++; + if (showDelete) staticColumnCount++; + if (showAudit) staticColumnCount++; + if (showAudit && showSecondaryAudit) staticColumnCount++; + + let pagination = onChangePage === undefined ? undefined : ; + + if (pagination) + return + + {pagination} + + + + return <> + } +} + +export default TableFooter; diff --git a/src/components/common/TableHeader.tsx b/src/components/common/TableHeader.tsx new file mode 100644 index 0000000..6d4401b --- /dev/null +++ b/src/components/common/TableHeader.tsx @@ -0,0 +1,103 @@ +import { faSortAsc, faSortDesc } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ChangeEvent, Component } from "react"; +import Column from "./columns"; +import Input, { InputType } from "./Input"; + +export interface TableHeaderProps{ + sortColumn? : Column; + columns : Column[]; + showDelete? : boolean; + showEdit? : boolean; + showAudit? : boolean; + showSecondaryAudit? : boolean; + onSort? : (sortColumn : Column) => void; + onSearch?: ( name: string, value: string) => void; +} + +class TableHeader extends Component> { + columnsMatch = ( left? : Column, right? : Column) => + { + if (left?.key !== right?.key) return false; + return true; + } + + raiseSort = (column : Column) => { + let sortColumn = this.props.sortColumn; + + if (sortColumn) { + if (this.columnsMatch(column, sortColumn)){ + sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; + } + else { + sortColumn = column; + sortColumn.order = "asc"; + } + + const { onSort } = this.props; + + if (onSort) + onSort(sortColumn); + } + }; + + renderSortIcon = (column : Column) => { + const { sortColumn } = this.props; + + if (!sortColumn) return null; + + if (!this.columnsMatch(column, sortColumn)) return null; + + if (sortColumn?.order === "asc") return + + return + }; + + changeSearch = (e: ChangeEvent) => + { + const {onSearch} = this.props; + + if (onSearch) + onSearch(e?.target?.name, e?.target?.value); + }; + + render() { + const { columns, showEdit, showDelete, showAudit, showSecondaryAudit, onSearch } = this.props; + + let searchRow = <>; + if (onSearch) + searchRow = + {columns.map((column) => + + { + (column.searchable === undefined || column.searchable === true ) && + + } + + )} + {showEdit && } + {showDelete && } + {showAudit && } + {showAudit && showSecondaryAudit && } + ; + + return ( + + + {columns.map((column) => + this.raiseSort(column)}> + {column.label} {this.renderSortIcon(column)} + + )} + {showEdit && } + {showDelete && } + {showAudit && } + {showAudit && showSecondaryAudit && } + + {searchRow} + + ); + } +} + +export default TableHeader; diff --git a/src/components/common/TemplateEditor.tsx b/src/components/common/TemplateEditor.tsx new file mode 100644 index 0000000..22f7d27 --- /dev/null +++ b/src/components/common/TemplateEditor.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import TextEditor from "./ckeditor/TextEditor"; +import customFieldsService from '../../modules/manager/customfields/services/customFieldsService'; + +interface customfieldType { + type: string; + name: string; + guid: string | undefined; + id: string | bigint; +} + +interface TemplateEditorProps { + className : string + name : string; + data : string; + showFields : boolean; + onChange : ( name : string, value : string ) => void; +} + +interface TemplateEditorState { + customfields: Array; + ready : boolean; +} + +class TemplateEditor extends React.Component { + state = { + customfields : [], + ready: false + } + + async componentWillMount() { + const pageData = await customFieldsService.getFields(0, 10, "name", true); + + const customfields = pageData.data.map( + (x) => { + return { + type: "CustomField", + name: x.name, + guid: x.guid, + id: x.id + } + } + ); + + const { className, name, data, onChange } = this.props; + + this.setState( { + ready : true, + customfields, + }); + + + } + + render() { + const { className, name, data, onChange } = this.props; + const { ready, customfields } = this.state; + + if (!ready) + return <> + + return ; + } +} + +export default TemplateEditor; \ No newline at end of file diff --git a/src/components/common/TemplateFiller.tsx b/src/components/common/TemplateFiller.tsx new file mode 100644 index 0000000..3d95797 --- /dev/null +++ b/src/components/common/TemplateFiller.tsx @@ -0,0 +1,184 @@ +import React from "react"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; +import formsService, { CreateFormInstance, EditFormInstance } from "../../modules/manager/forms/services/formsService"; +import parse, { HTMLReactParserOptions, domToReact } from 'html-react-parser'; +import { CustomField } from "../../modules/manager/customfields/services/customFieldsService"; +import Form, { FormState } from "./Form"; +import { toast } from "react-toastify"; +import Loading from "./Loading"; + +interface TemplateFillerProps { + templateId? : GeneralIdRef; + formInstanceId?: GeneralIdRef; + onValidationChanged? : () => {}; +} + +interface TemplateFillerState extends FormState { + customFields? : CustomField[], + template : { + name?: string; + templateId? : GeneralIdRef, + version? : bigint, + definition?: string; + } +} + +class TemplateFiller extends Form { + state : TemplateFillerState = { + loaded: false, + customFields : undefined, + template : { + name: undefined, + templateId: undefined, + version: undefined, + definition: undefined, + }, + data : {}, + errors: {} + } + + schema = { + }; + + componentDidMount(): void { + this.loadTemplate(); + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + if ((prevProps.formInstanceId !== this.props.formInstanceId) || (prevProps.templateId !== this.props.templateId)) + this.loadTemplate(); + + if (this.props.onValidationChanged) + { + const prevErrorCount = Object.keys(prevState.errors).length > 0 + const errorCount = Object.keys(this.state.errors).length > 0 + + if (prevErrorCount !== errorCount) { + this.props.onValidationChanged(); + } + } + + } + + loadTemplate = async () => { + const { templateId, formInstanceId } = this.props; + let loadedData : any; + + if (templateId !== undefined){ + loadedData = await formsService.getForm(templateId?.id, templateId?.guid); + //Get the form definiton for the template provided by templateId and load. + + loadedData.templateId = undefined; + loadedData.customFieldValues = undefined; + loadedData.updatedVersion = undefined; + } else if (formInstanceId !== undefined){ + loadedData = await formsService.getFormInstance(formInstanceId?.id, formInstanceId?.guid); + console.log("formInstanceId", loadedData); + } else { + loadedData = { + name: undefined, + id : undefined, + guid: undefined, + version: undefined, + definition: undefined, + customFieldDefinitions: undefined, + templateId : undefined, + customFieldValues : undefined, + updatedVersion : undefined + } + } + + const { template, data } = this.state; + + template.name = loadedData.name; + template.templateId = MakeGeneralIdRef(loadedData.id, loadedData.guid); + template.version = loadedData.version; + template.definition = loadedData.definition; + const customFields = loadedData.customFieldDefinitions; + + this.setCustomFieldValues(data, loadedData.customFieldValues, customFields ); + + this.setState({ loaded: true, template, customFields, data }); + } + + parseDefinition = ( definition: string, customFieldDefinitions: CustomField[]) => { + const options: HTMLReactParserOptions = { + replace: (domNode) => { + const domNodeAsAny : any = domNode; + if (domNodeAsAny.name === "span"){ + if (domNodeAsAny.attribs.fieldtype === "CustomField") + { + const customField = customFieldDefinitions.filter( x => x.guid === domNodeAsAny.attribs.guid)[0]; + return this.renderCustomField(customField, false); + } + } + else if (domNodeAsAny.name === "p"){ + return
    {domToReact(domNodeAsAny.children, options)}
    + } + } + } + + return parse(definition, options); + } + + hasValidationErrors = () : boolean => { + const { errors } = this.state; + + const result = Object.keys(errors).length > 0; + return result; + } + + async Save() { + const { errors } = this.state; + const { templateId, version } = this.state.template; + const { formInstanceId } = this.props; + + if ( Object.keys(errors).length > 0 ) + { + toast.error("There are errors on the form"); + throw new Error( "There are errors on the form"); + } + + const customFieldValues = this.CustomFieldValues(); + if (formInstanceId !== undefined){ + if (templateId === undefined) + throw Error("TemplateId cannot be null"); + + if (version === undefined) + throw Error("Version cannot be null"); + + const editFormInstance : EditFormInstance = { formInstanceId, templateId, version, customFieldValues }; + await formsService.putFormInstance(editFormInstance); + } + else { + if (templateId !== undefined && version !== undefined) + { + //const customFieldValues = this.CustomFieldValues(); + const formInstance : CreateFormInstance = { templateId, version, customFieldValues }; + return await formsService.postFormInstance(formInstance); + } + else + throw new Error("template unknown"); + } + } + + render() { + const { loaded, template, customFields } = this.state; + + let parsedDefinition : any; + if (template.definition) + parsedDefinition = this.parseDefinition(template.definition, customFields!); + else + parsedDefinition = <>; + + return ( + +
    + {parsedDefinition} +
    +
    + ); + } +} + +export default TemplateFiller; \ No newline at end of file diff --git a/src/components/common/ToggleSlider.tsx b/src/components/common/ToggleSlider.tsx new file mode 100644 index 0000000..f7e60e6 --- /dev/null +++ b/src/components/common/ToggleSlider.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import Toggle, { ToggleProps } from "react-toggle"; + +export interface ToggleSliderProps extends ToggleProps { + name: string; + label: string; + error: string; + readOnly?: boolean; + defaultChecked: boolean; +} + +class ToggleSlider extends React.Component { + render() { + const { name, label, error, readOnly, defaultChecked, ...rest } = this.props; + + return ( +
    + + + {error &&
    {error}
    } +
    + ); + } +} + +export default ToggleSlider; diff --git a/src/components/common/ckeditor/TextEditor.jsx b/src/components/common/ckeditor/TextEditor.jsx new file mode 100644 index 0000000..00fbdcc --- /dev/null +++ b/src/components/common/ckeditor/TextEditor.jsx @@ -0,0 +1,356 @@ +import { useState, useEffect, useRef } from 'react'; +import { CKEditor } from '@ckeditor/ckeditor5-react'; +import Field from './plugins/field/field'; + +import { + DecoupledEditor, + AccessibilityHelp, + Alignment, + Autoformat, + AutoImage, + AutoLink, + Autosave, + BalloonToolbar, + Base64UploadAdapter, + BlockQuote, + Bold, + Code, + CodeBlock, + Essentials, + FindAndReplace, + FontBackgroundColor, + FontColor, + FontFamily, + FontSize, + Heading, + Highlight, + HorizontalLine, + ImageBlock, + ImageCaption, + ImageInline, + ImageInsert, + ImageInsertViaUrl, + ImageResize, + ImageStyle, + ImageTextAlternative, + ImageToolbar, + ImageUpload, + Indent, + IndentBlock, + Italic, + Link, + LinkImage, + List, + ListProperties, +// Mention, + Paragraph, + PasteFromOffice, + RemoveFormat, + SelectAll, + SpecialCharacters, + SpecialCharactersArrows, + SpecialCharactersCurrency, + SpecialCharactersEssentials, + SpecialCharactersLatin, + SpecialCharactersMathematical, + SpecialCharactersText, + Strikethrough, + Subscript, + Superscript, + Table, + TableCaption, + TableCellProperties, + TableColumnResize, + TableProperties, + TableToolbar, + TextTransformation, + TodoList, + Underline, + Undo +} from 'ckeditor5'; + +import 'ckeditor5/ckeditor5.css'; + +// export interface TextEditorProps { +// className : String; +// name : String; +// data : String; +// customFields : []; +// onChange : ( name : String, data : String ) => void; +// } + +export default function TextEditor( props ) { + const editorContainerRef = useRef(null); + const editorMenuBarRef = useRef(null); + const editorToolbarRef = useRef(null); + const editorRef = useRef(null); + const [isLayoutReady, setIsLayoutReady] = useState(false); + + useEffect(() => { + setIsLayoutReady(true); + + return () => setIsLayoutReady(false); + }, []); + + let toobarItems = [ + 'undo', + 'redo', + '|', + 'heading', + '|', + 'fontSize', + 'fontFamily', + 'fontColor', + 'fontBackgroundColor', + '|', + 'bold', + 'italic', + 'underline', + '|', + 'link', + 'insertImage', + 'insertTable', + 'highlight', + 'blockQuote', + 'codeBlock', + '|', + 'alignment', + '|', + 'bulletedList', + 'numberedList', + 'todoList', + 'outdent', + 'indent', + ]; + + let plugins = [ + AccessibilityHelp, + Alignment, + Autoformat, + AutoImage, + AutoLink, + Autosave, + BalloonToolbar, + Base64UploadAdapter, + BlockQuote, + Bold, + Code, + CodeBlock, + Essentials, + FindAndReplace, + FontBackgroundColor, + FontColor, + FontFamily, + FontSize, + Heading, + Highlight, + HorizontalLine, + ImageBlock, + ImageCaption, + ImageInline, + ImageInsert, + ImageInsertViaUrl, + ImageResize, + ImageStyle, + ImageTextAlternative, + ImageToolbar, + ImageUpload, + Indent, + IndentBlock, + Italic, + Link, + LinkImage, + List, + ListProperties, +// Mention, + Paragraph, + PasteFromOffice, + RemoveFormat, + SelectAll, + SpecialCharacters, + SpecialCharactersArrows, + SpecialCharactersCurrency, + SpecialCharactersEssentials, + SpecialCharactersLatin, + SpecialCharactersMathematical, + SpecialCharactersText, + Strikethrough, + Subscript, + Superscript, + Table, + TableCaption, + TableCellProperties, + TableColumnResize, + TableProperties, + TableToolbar, + TextTransformation, + TodoList, + Underline, + Undo + ]; + + let customfields = [] + + if ( props.customFields && props.customFields.length > 0 ) { + toobarItems.push( '|' ); + toobarItems.push( 'Field' ); + plugins.push( Field ); + customfields = props.customFields; + } + + const editorConfig = { + toolbar: { + items: toobarItems, + shouldNotGroupWhenFull: false + }, + plugins: plugins, + balloonToolbar: ['bold', 'italic', '|', 'link', 'insertImage', '|', 'bulletedList', 'numberedList'], + fontFamily: { + supportAllValues: true + }, + fontSize: { + options: [10, 12, 14, 'default', 18, 20, 22], + supportAllValues: true + }, + heading: { + options: [ + { + model: 'paragraph', + title: 'Paragraph', + class: 'ck-heading_paragraph' + }, + { + model: 'heading1', + view: 'h1', + title: 'Heading 1', + class: 'ck-heading_heading1' + }, + { + model: 'heading2', + view: 'h2', + title: 'Heading 2', + class: 'ck-heading_heading2' + }, + { + model: 'heading3', + view: 'h3', + title: 'Heading 3', + class: 'ck-heading_heading3' + }, + { + model: 'heading4', + view: 'h4', + title: 'Heading 4', + class: 'ck-heading_heading4' + }, + { + model: 'heading5', + view: 'h5', + title: 'Heading 5', + class: 'ck-heading_heading5' + }, + { + model: 'heading6', + view: 'h6', + title: 'Heading 6', + class: 'ck-heading_heading6' + } + ] + }, + image: { + toolbar: [ + 'toggleImageCaption', + 'imageTextAlternative', + '|', + 'imageStyle:inline', + 'imageStyle:wrapText', + 'imageStyle:breakText', + '|', + 'resizeImage' + ] + }, + //initialData: props.data, + link: { + addTargetToExternalLinks: true, + defaultProtocol: 'https://', + decorators: { + toggleDownloadable: { + mode: 'manual', + label: 'Downloadable', + attributes: { + download: 'file' + } + } + } + }, + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + }, + // mention: { + // feeds: [ + // { + // marker: '@', + // feed: [ + // /* See: https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html */ + // ] + // } + // ] + // }, + menuBar: { + isVisible: true + }, + placeholder: 'Type or paste your content here!', + table: { + contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties'] + }, + fieldConfig: { + fields : customfields + } + }; + + const editorClasses = "editor-container__editor " + props.className; + + return ( +
    +
    +
    +
    +
    +
    +
    +
    + {isLayoutReady && ( + { + Array.from(editorToolbarRef.current?.children).forEach(child => child.remove()); + Array.from(editorMenuBarRef.current?.children).forEach(child => child.remove()); + editorToolbarRef.current.appendChild(editor.ui.view.toolbar.element); + editorMenuBarRef.current.appendChild(editor.ui.view.menuBarView.element); + }} + onAfterDestroy={() => { + Array.from(editorToolbarRef.current.children).forEach(child => child.remove()); + Array.from(editorMenuBarRef.current.children).forEach(child => child.remove()); + }} + editor={DecoupledEditor} + config={editorConfig} + data={props.data} + onChange={ ( event, editor ) => { + const data = editor.getData(); + + props.onChange( props.name, data ); + //console.log( { event, editor, data } ); + } } + /> + )} +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/components/common/ckeditor/plugins/abbreviation/abbreviation.js b/src/components/common/ckeditor/plugins/abbreviation/abbreviation.js new file mode 100644 index 0000000..0718560 --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/abbreviation.js @@ -0,0 +1,14 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { Plugin } from 'ckeditor5'; +import AbbreviationEditing from './abbreviationediting'; +import AbbreviationUI from './abbreviationui'; + +export default class Abbreviation extends Plugin { + static get requires() { + return [ AbbreviationEditing, AbbreviationUI ]; + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/abbreviation/abbreviationcommand.js b/src/components/common/ckeditor/plugins/abbreviation/abbreviationcommand.js new file mode 100644 index 0000000..8243046 --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/abbreviationcommand.js @@ -0,0 +1,109 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { Command, findAttributeRange, toMap } from 'ckeditor5'; +import getRangeText from './utils.js'; + +export default class AbbreviationCommand extends Command { + refresh() { + const model = this.editor.model; + const selection = model.document.selection; + const firstRange = selection.getFirstRange(); + + // When the selection is collapsed, the command has a value if the caret is in an abbreviation. + if ( firstRange.isCollapsed ) { + if ( selection.hasAttribute( 'abbreviation' ) ) { + const attributeValue = selection.getAttribute( 'abbreviation' ); + + // Find the entire range containing the abbreviation under the caret position. + const abbreviationRange = findAttributeRange( selection.getFirstPosition(), 'abbreviation', attributeValue, model ); + + this.value = { + abbr: getRangeText( abbreviationRange ), + title: attributeValue, + range: abbreviationRange + }; + } else { + this.value = null; + } + } + // When the selection is not collapsed, the command has a value if the selection contains a subset of a single abbreviation + // or an entire abbreviation. + else { + if ( selection.hasAttribute( 'abbreviation' ) ) { + const attributeValue = selection.getAttribute( 'abbreviation' ); + + // Find the entire range containing the abbreviation under the caret position. + const abbreviationRange = findAttributeRange( selection.getFirstPosition(), 'abbreviation', attributeValue, model ); + + if ( abbreviationRange.containsRange( firstRange, true ) ) { + this.value = { + abbr: getRangeText( firstRange ), + title: attributeValue, + range: firstRange + }; + } else { + this.value = null; + } + } else { + this.value = null; + } + } + + // The command is enabled when the "abbreviation" attribute can be set on the current model selection. + this.isEnabled = model.schema.checkAttributeInSelection( selection, 'abbreviation' ); + } + + execute( { abbr, title } ) { + const model = this.editor.model; + const selection = model.document.selection; + + model.change( writer => { + // If selection is collapsed then update the selected abbreviation or insert a new one at the place of caret. + if ( selection.isCollapsed ) { + // When a collapsed selection is inside text with the "abbreviation" attribute, update its text and title. + if ( this.value ) { + const { end: positionAfter } = model.insertContent( + writer.createText( abbr, { abbreviation: title } ), + this.value.range + ); + // Put the selection at the end of the inserted abbreviation. + writer.setSelection( positionAfter ); + } + // If the collapsed selection is not in an existing abbreviation, insert a text node with the "abbreviation" attribute + // in place of the caret. Because the selection is collapsed, the attribute value will be used as a data for text. + // If the abbreviation is empty, do not do anything. + else if ( abbr !== '' ) { + const firstPosition = selection.getFirstPosition(); + + // Collect all attributes of the user selection (could be "bold", "italic", etc.) + const attributes = toMap( selection.getAttributes() ); + + // Put the new attribute to the map of attributes. + attributes.set( 'abbreviation', title ); + + // Inject the new text node with the abbreviation text with all selection attributes. + const { end: positionAfter } = model.insertContent( writer.createText( abbr, attributes ), firstPosition ); + + // Put the selection at the end of the inserted abbreviation. Using an end of a range returned from + // insertContent() just in case nodes with the same attributes were merged. + writer.setSelection( positionAfter ); + } + + // Remove the "abbreviation" attribute attribute from the selection. It stops adding a new content into the abbreviation + // if the user starts to type. + writer.removeSelectionAttribute( 'abbreviation' ); + } else { + // If the selection has non-collapsed ranges, change the attribute on nodes inside those ranges + // omitting nodes where the "abbreviation" attribute is disallowed. + const ranges = model.schema.getValidRanges( selection.getRanges(), 'abbreviation' ); + + for ( const range of ranges ) { + writer.setAttribute( 'abbreviation', title, range ); + } + } + } ); + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/abbreviation/abbreviationediting.js b/src/components/common/ckeditor/plugins/abbreviation/abbreviationediting.js new file mode 100644 index 0000000..e9cc0a3 --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/abbreviationediting.js @@ -0,0 +1,60 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { Plugin } from 'ckeditor5'; +import AbbreviationCommand from './abbreviationcommand'; + +export default class AbbreviationEditing extends Plugin { + init() { + this._defineSchema(); + this._defineConverters(); + + this.editor.commands.add( + 'addAbbreviation', new AbbreviationCommand( this.editor ) + ); + } + _defineSchema() { + const schema = this.editor.model.schema; + + // Extend the text node's schema to accept the abbreviation attribute. + schema.extend( '$text', { + allowAttributes: [ 'abbreviation' ] + } ); + } + _defineConverters() { + const conversion = this.editor.conversion; + + // Conversion from a model attribute to a view element + conversion.for( 'downcast' ).attributeToElement( { + model: 'abbreviation', + + // Callback function provides access to the model attribute value + // and the DowncastWriter + view: ( modelAttributeValue, conversionApi ) => { + const { writer } = conversionApi; + return writer.createAttributeElement( 'abbr', { + title: modelAttributeValue + } ); + } + } ); + + // Conversion from a view element to a model attribute + conversion.for( 'upcast' ).elementToAttribute( { + view: { + name: 'abbr', + attributes: [ 'title' ] + }, + model: { + key: 'abbreviation', + + // Callback function provides access to the view element + value: viewElement => { + const title = viewElement.getAttribute( 'title' ); + return title; + } + } + } ); + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/abbreviation/abbreviationui.js b/src/components/common/ckeditor/plugins/abbreviation/abbreviationui.js new file mode 100644 index 0000000..c0d994a --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/abbreviationui.js @@ -0,0 +1,133 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { ButtonView, ContextualBalloon, Plugin, clickOutsideHandler } from 'ckeditor5'; +import FormView from './abbreviationview'; +import getRangeText from './utils.js'; +import './styles.css'; + +export default class AbbreviationUI extends Plugin { + static get requires() { + return [ ContextualBalloon ]; + } + + init() { + const editor = this.editor; + + // Create the balloon and the form view. + this._balloon = this.editor.plugins.get( ContextualBalloon ); + this.formView = this._createFormView(); + + editor.ui.componentFactory.add( 'abbreviation', () => { + const button = new ButtonView(); + + button.label = 'Abbreviation'; + button.tooltip = true; + button.withText = true; + + // Show the UI on button click. + this.listenTo( button, 'execute', () => { + this._showUI(); + } ); + + return button; + } ); + } + + _createFormView() { + const editor = this.editor; + const formView = new FormView( editor.locale ); + + // Execute the command after clicking the "Save" button. + this.listenTo( formView, 'submit', () => { + // Grab values from the abbreviation and title input fields. + const value = { + abbr: formView.abbrInputView.fieldView.element.value, + title: formView.titleInputView.fieldView.element.value + }; + editor.execute( 'addAbbreviation', value ); + + // Hide the form view after submit. + this._hideUI(); + } ); + + // Hide the form view after clicking the "Cancel" button. + this.listenTo( formView, 'cancel', () => { + this._hideUI(); + } ); + + // Hide the form view when clicking outside the balloon. + clickOutsideHandler( { + emitter: formView, + activator: () => this._balloon.visibleView === formView, + contextElements: [ this._balloon.view.element ], + callback: () => this._hideUI() + } ); + + formView.keystrokes.set( 'Esc', ( data, cancel ) => { + this._hideUI(); + cancel(); + } ); + + return formView; + } + + _showUI() { + const selection = this.editor.model.document.selection; + + // Check the value of the command. + const commandValue = this.editor.commands.get( 'addAbbreviation' ).value; + + this._balloon.add( { + view: this.formView, + position: this._getBalloonPositionData() + } ); + + // Disable the input when the selection is not collapsed. + this.formView.abbrInputView.isEnabled = selection.getFirstRange().isCollapsed; + + // Fill the form using the state (value) of the command. + if ( commandValue ) { + this.formView.abbrInputView.fieldView.value = commandValue.abbr; + this.formView.titleInputView.fieldView.value = commandValue.title; + } + // If the command has no value, put the currently selected text (not collapsed) + // in the first field and empty the second in that case. + else { + const selectedText = getRangeText( selection.getFirstRange() ); + + this.formView.abbrInputView.fieldView.value = selectedText; + this.formView.titleInputView.fieldView.value = ''; + } + + this.formView.focus(); + } + + _hideUI() { + // Clear the input field values and reset the form. + this.formView.abbrInputView.fieldView.value = ''; + this.formView.titleInputView.fieldView.value = ''; + this.formView.element.reset(); + + this._balloon.remove( this.formView ); + + // Focus the editing view after inserting the abbreviation so the user can start typing the content + // right away and keep the editor focused. + this.editor.editing.view.focus(); + } + + _getBalloonPositionData() { + const view = this.editor.editing.view; + const viewDocument = view.document; + let target = null; + + // Set a target position by converting view selection range to DOM + target = () => view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() ); + + return { + target + }; + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/abbreviation/abbreviationview.js b/src/components/common/ckeditor/plugins/abbreviation/abbreviationview.js new file mode 100644 index 0000000..9526268 --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/abbreviationview.js @@ -0,0 +1,122 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { + ButtonView, + FocusCycler, + FocusTracker, KeystrokeHandler, + LabeledFieldView, + View, + createLabeledInputText, + icons, + submitHandler +} from 'ckeditor5'; + +export default class FormView extends View { + constructor( locale ) { + super( locale ); + + this.focusTracker = new FocusTracker(); + this.keystrokes = new KeystrokeHandler(); + + this.abbrInputView = this._createInput( 'Add abbreviation' ); + this.titleInputView = this._createInput( 'Add title' ); + + this.saveButtonView = this._createButton( 'Save', icons.check, 'ck-button-save' ); + + // Submit type of the button will trigger the submit event on entire form when clicked + //(see submitHandler() in render() below). + this.saveButtonView.type = 'submit'; + + this.cancelButtonView = this._createButton( 'Cancel', icons.cancel, 'ck-button-cancel' ); + + // Delegate ButtonView#execute to FormView#cancel. + this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' ); + + this.childViews = this.createCollection( [ + this.abbrInputView, + this.titleInputView, + this.saveButtonView, + this.cancelButtonView + ] ); + + this._focusCycler = new FocusCycler( { + focusables: this.childViews, + focusTracker: this.focusTracker, + keystrokeHandler: this.keystrokes, + actions: { + // Navigate form fields backwards using the Shift + Tab keystroke. + focusPrevious: 'shift + tab', + + // Navigate form fields forwards using the Tab key. + focusNext: 'tab' + } + } ); + + this.setTemplate( { + tag: 'form', + attributes: { + class: [ 'ck', 'ck-abbr-form' ], + tabindex: '-1' + }, + children: this.childViews + } ); + } + + render() { + super.render(); + + submitHandler( { + view: this + } ); + + this.childViews._items.forEach( view => { + // Register the view in the focus tracker. + this.focusTracker.add( view.element ); + } ); + + // Start listening for the keystrokes coming from #element. + this.keystrokes.listenTo( this.element ); + } + + destroy() { + super.destroy(); + + this.focusTracker.destroy(); + this.keystrokes.destroy(); + } + + focus() { + // If the abbreviation text field is enabled, focus it straight away to allow the user to type. + if ( this.abbrInputView.isEnabled ) { + this.abbrInputView.focus(); + } + // Focus the abbreviation title field if the former is disabled. + else { + this.titleInputView.focus(); + } + } + + _createInput( label ) { + const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText ); + + labeledInput.label = label; + + return labeledInput; + } + + _createButton( label, icon, className ) { + const button = new ButtonView(); + + button.set( { + label, + icon, + tooltip: true, + class: className + } ); + + return button; + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/abbreviation/styles.css b/src/components/common/ckeditor/plugins/abbreviation/styles.css new file mode 100644 index 0000000..2b6a253 --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/styles.css @@ -0,0 +1,24 @@ +.ck.ck-abbr-form { + padding: var(--ck-spacing-large); + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + grid-column-gap: 0px; + grid-row-gap: var(--ck-spacing-standard); +} + +.ck.ck-abbr-form .ck.ck-labeled-field-view:nth-of-type(1) { + grid-area: 1 / 1 / 2 / 3; +} + +.ck.ck-abbr-form .ck.ck-labeled-field-view:nth-of-type(2) { + grid-area: 2 / 1 / 3 / 3; +} + +.ck.ck-abbr-form .ck-button:nth-of-type(1) { + grid-area: 3 / 1 / 4 / 2; +} + +.ck.ck-abbr-form .ck-button:nth-of-type(2) { + grid-area: 3 / 2 / 4 / 3; +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/abbreviation/utils.js b/src/components/common/ckeditor/plugins/abbreviation/utils.js new file mode 100644 index 0000000..ca42b97 --- /dev/null +++ b/src/components/common/ckeditor/plugins/abbreviation/utils.js @@ -0,0 +1,15 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +// A helper function that retrieves and concatenates all text within the model range. +export default function getRangeText( range ) { + return Array.from( range.getItems() ).reduce( ( rangeText, node ) => { + if ( !( node.is( 'text' ) || node.is( 'textProxy' ) ) ) { + return rangeText; + } + + return rangeText + node.data; + }, '' ); +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/field/field.ts b/src/components/common/ckeditor/plugins/field/field.ts new file mode 100644 index 0000000..32736e0 --- /dev/null +++ b/src/components/common/ckeditor/plugins/field/field.ts @@ -0,0 +1,9 @@ +import { Plugin } from 'ckeditor5'; +import FieldEditing from './fieldediting'; +import FieldUI from './fieldui'; + +export default class Field extends Plugin { + static get requires() { + return [ FieldEditing, FieldUI ]; + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/field/fieldcommand.js b/src/components/common/ckeditor/plugins/field/fieldcommand.js new file mode 100644 index 0000000..b4fad89 --- /dev/null +++ b/src/components/common/ckeditor/plugins/field/fieldcommand.js @@ -0,0 +1,38 @@ +import { Command } from "ckeditor5"; + +export default class FieldCommand extends Command { +// execute( args: any ) : void { + execute( args ) { + const editor = this.editor; + const selection = editor.model.document.selection; + + editor.model.change( writer => { + // Create a element with the "name" attribute (and all the selection attributes)... + const placeholder = writer.createElement( 'field', { + ...Object.fromEntries( selection.getAttributes() ), + field: args.value + } ); + + // ... and insert it into the document. Put the selection on the inserted element. + editor.model.insertObject( placeholder, null, null, { setSelection: 'on' } ); + } ); + } + + refresh() { + const model = this.editor.model; + const selection = model.document.selection; + + let isAllowed = true; + + if (selection.focus) + { + const parent = selection.focus.parent; + + if (parent){ + isAllowed = model.schema.checkChild( parent, 'field' ); + } + } + + this.isEnabled = isAllowed; + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/field/fieldediting.js b/src/components/common/ckeditor/plugins/field/fieldediting.js new file mode 100644 index 0000000..218f4e5 --- /dev/null +++ b/src/components/common/ckeditor/plugins/field/fieldediting.js @@ -0,0 +1,119 @@ +import { Plugin, Widget, toWidget, viewToModelPositionOutsideModelElement } from 'ckeditor5'; +import FieldCommand from './fieldcommand'; + +String.prototype.rtrim = function (s) { + if (s === undefined) + s = '\\s'; + return this.replace(new RegExp("[" + s + "]*$"), ''); +}; +String.prototype.ltrim = function (s) { + if (s === undefined) + s = '\\s'; + return this.replace(new RegExp("^[" + s + "]*"), ''); +}; + +export class FieldConfig{ + fields = [ ] +} + +export default class FieldEditing extends Plugin { + static get requires() { + return [ Widget ]; + } + + init() { + this._defineSchema(); + this._defineConverters(); + + this.editor.commands.add( 'field', new FieldCommand( this.editor ) ); + + this.editor.editing.mapper.on( + 'viewToModelPosition', + viewToModelPositionOutsideModelElement( this.editor.model, viewElement => viewElement.hasClass( 'field' ) ) + ); + this.editor.config.define( 'fieldConfig', FieldConfig ); + } + + _defineSchema() { + const schema = this.editor.model.schema; + + schema.register( 'field', { + // Behaves like a self-contained inline object (e.g. an inline image) + // allowed in places where $text is allowed (e.g. in paragraphs). + // The inline widget can have the same attributes as text (for example linkHref, bold). + inheritAllFrom: '$inlineObject', + + // The field can have many types, like date, name, surname, etc: + allowAttributes: [ 'field' ] + } ); + } + + _defineConverters() { + const conversion = this.editor.conversion; + + conversion.for( 'upcast' ).elementToElement( { + view: { + name: 'span', + classes: [ 'field' ] + }, + model: (viewElement, conversionApi) => { + // Extract the "name" from "{name}". + if (viewElement === undefined) { + return null; + } + + const { writer } = conversionApi; + + const name = viewElement.getChild(0)?._textData; + + const fieldtype = viewElement.getAttribute("fieldtype"); + const guid = viewElement.getAttribute("guid"); + const fieldid = viewElement.getAttribute("fieldid"); + + const field = { + name, + type: fieldtype, + guid, + fieldid + }; + + + return writer.createElement('field', { field }); + } + } ); + + conversion.for( 'editingDowncast' ).elementToElement( { + model: 'field', + view: ( modelItem, { writer: viewWriter } ) => { + const widgetElement = createfieldView( modelItem, viewWriter ); + + // Enable widget handling on a field element inside the editing view. + return toWidget( widgetElement, viewWriter ); + } + } ); + + conversion.for( 'dataDowncast' ).elementToElement( { + model: 'field', + view: ( modelItem, { writer: viewWriter } ) => createfieldView( modelItem, viewWriter ) + } ); + + // Helper method for both downcast converters. +// function createfieldView( modelItem : Element, viewWriter : DowncastWriter ) { + function createfieldView( modelItem, viewWriter ) { + const field = modelItem.getAttribute( 'field' ); //todo any is a cop out. + + const fieldView = viewWriter.createContainerElement( 'span', { + class: 'field', + fieldType: field.type, + guid: field.guid, + fieldid: (field.guid !== undefined) ? null : field.id + } ); + + // Insert the field name (as a text). + const innerText = viewWriter.createText( '{' + field.name.ltrim('{').rtrim('}') + '}' ); + viewWriter.insert( viewWriter.createPositionAt( fieldView, 0 ), innerText ); + + return fieldView; + } + } +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/field/fieldui.js b/src/components/common/ckeditor/plugins/field/fieldui.js new file mode 100644 index 0000000..eec8508 --- /dev/null +++ b/src/components/common/ckeditor/plugins/field/fieldui.js @@ -0,0 +1,67 @@ +import { Plugin, Collection, addListToDropdown, createDropdown, ViewModel } from 'ckeditor5'; +import './styles.css'; + + export default class FieldUI extends Plugin { + init() { + const editor = this.editor; + const t = editor.t; + const fields = editor.config.get( 'fieldConfig' ).fields; + + // The "field" dropdown must be registered among the UI components of the editor + // to be displayed in the toolbar. + editor.ui.componentFactory.add( 'field', locale => { + const dropdownView = createDropdown( locale ); + + // Populate the list in the dropdown with items. + addListToDropdown( dropdownView, getDropdownItemsDefinitions( fields ) ); + + dropdownView.buttonView.set( { + // The t() function helps localize the editor. All strings enclosed in t() can be + // translated and change when the language of the editor changes. + label: t( 'Insert Field' ), + tooltip: true, + withText: true + } ); + + // Disable the field button when the command is disabled. + const command = editor.commands.get( 'field' ); + dropdownView.bind( 'isEnabled' ).to( command ); + + // Execute the command when the dropdown item is clicked (executed). + this.listenTo( dropdownView, 'execute', evt => { + editor.execute( 'field', { value: evt.source.commandParam } ); + editor.editing.view.focus(); + } ); + + return dropdownView; + } ); + + editor.ui.extendMenuBar( { + item: 'field', + position: 'after:bold' + } ); + } +} + +function getDropdownItemsDefinitions( fields ) { + const itemDefinitions = new Collection(); + + if (fields !== undefined) + { + for ( const field of fields ) { + const definition = { + type: 'button', + model: new ViewModel( { + commandParam:field, + label: field.name, + withText: true + } ) + }; + + // Add the item definition to the collection. + itemDefinitions.add( definition ); + } + } + + return itemDefinitions; +} \ No newline at end of file diff --git a/src/components/common/ckeditor/plugins/field/styles.css b/src/components/common/ckeditor/plugins/field/styles.css new file mode 100644 index 0000000..294cc7e --- /dev/null +++ b/src/components/common/ckeditor/plugins/field/styles.css @@ -0,0 +1,12 @@ +.field { + background: #ffff00; + color: black; + padding: 4px 2px; + outline-offset: -2px; + line-height: 1em; + margin: 0 1px; +} + +.field::selection { + display: none; +} \ No newline at end of file diff --git a/src/components/common/columns.tsx b/src/components/common/columns.tsx new file mode 100644 index 0000000..f30a68e --- /dev/null +++ b/src/components/common/columns.tsx @@ -0,0 +1,9 @@ +export default interface Column{ + key : string, + path? : string, + label : string, + link? : string, + order? : string, + searchable? : boolean, + content? : (item : T) => JSX.Element +} diff --git a/src/components/common/expando.tsx b/src/components/common/expando.tsx new file mode 100644 index 0000000..6e86288 --- /dev/null +++ b/src/components/common/expando.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import Button, { ButtonType } from "./Button"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons"; + +interface ExpandoProps { + name:string, + title : JSX.Element, + children:JSX.Element, + error: string; +} + +interface ExpandoState { + expanded : boolean; +} + +class Expando extends React.Component { + state : ExpandoState = { + expanded : false + } + + DropDownClick = () => { + this.setState({expanded :true}) + } + + CloseUpClick = () => { + this.setState({expanded :false}) + } + + render() { + const { title, children } = this.props; + const { expanded } = this.state; + + if (!expanded){ + return (
    + +
    ); + } + else { + return (
    + + {children} +
    ); + } + } +} + +export default Expando; \ No newline at end of file diff --git a/src/components/common/option.tsx b/src/components/common/option.tsx new file mode 100644 index 0000000..1a64817 --- /dev/null +++ b/src/components/common/option.tsx @@ -0,0 +1,6 @@ +import { GeneralIdRef } from "../../utils/GeneralIdRef"; + +export default interface Option { + _id: string | number; + name: string; +} diff --git a/src/components/common/table.css b/src/components/common/table.css new file mode 100644 index 0000000..bbc8588 --- /dev/null +++ b/src/components/common/table.css @@ -0,0 +1,3 @@ +tr.selected { + background-color: lightblue; +} \ No newline at end of file diff --git a/src/components/pickers/CustomFieldPicker.tsx b/src/components/pickers/CustomFieldPicker.tsx new file mode 100644 index 0000000..c596070 --- /dev/null +++ b/src/components/pickers/CustomFieldPicker.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import Select from "../common/Select"; +import Option from "../common/option"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; +import customFieldsService, { CustomField } from "../../modules/manager/customfields/services/customFieldsService"; + +interface CustomFieldPickerProps { + name: string; + label: string; + error?: string; + value: any; + exclude? : CustomField[]; + onChange?: (name: string, id: GeneralIdRef, displayValue : string) => void; +} + +interface CustomFieldPickerState { + options?: Option[]; +} + +class CustomFieldPicker extends React.Component { + state = { options: [] as Option[] }; + + async componentDidMount() { + const pagedData = await customFieldsService.getFields(0, 10, "name", true); + if (pagedData) { + const options: Option[] = (pagedData.data as any[]).map((x: { id: any; name: any }) => { + return { + _id: x.id, + name: x.name, + }; + }); + + this.setState({ options }); + } + } + + GetOptionById = (value: string ) => { + const { options } = this.state; + + for( var option of options) + { + if (String(option._id) === value) + return option.name; + } + return ""; + } + + handleChange = (e: React.ChangeEvent) => { + const { onChange } = this.props; + const input = e.currentTarget; + + const generalIdRef = MakeGeneralIdRef(BigInt(input.value)); + const displayValue = this.GetOptionById(input.value); + + if (onChange) onChange(input.name, generalIdRef, displayValue); + }; + + getFilteredOptions = () => { + const { exclude } = this.props; + const { options } = this.state; + + let filteredOptions = options.filter( (o) => { + + if (exclude) + { + for( var exlcudedItem of exclude) + { + const idAsString : string = String(exlcudedItem.id); + const oid : string = String(o._id); + if (oid === idAsString) { + return false; + } + } + } + + return true; + } + ); + + return filteredOptions; + } + + render() { + const { name, label, error, value } = this.props; + + const filteredOptions = this.getFilteredOptions(); + + return ( + + // ); + if (maxEntries == 1) { + let value = selectedOptions[0]?._id; + return ( + + ); + } +} + +export default FormTemplatePicker; diff --git a/src/components/pickers/GlossaryPicker.tsx b/src/components/pickers/GlossaryPicker.tsx new file mode 100644 index 0000000..9a1cb52 --- /dev/null +++ b/src/components/pickers/GlossaryPicker.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import Select from "../common/Select"; +import Option from "../common/option"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; +import glossariesService, { CustomFieldValue, SystemGlossaries } from "../../modules/manager/glossary/services/glossaryService"; +import MultiSelect from "../common/MultiSelect"; + +interface GlossaryPickerProps { + includeLabel?: boolean; + name: string; + label: string; + rootItem?: GeneralIdRef; + error?: string; + values: CustomFieldValue[]; + maxEntries?: number; + onChange?: (name: string, values: CustomFieldValue[]) => void; +} + +interface GlossaryPickerState { + options?: Option[]; + selectedOptions: Option[]; +} + +class GlossaryPicker extends React.Component { + state = { + options: [] as Option[], + selectedOptions: [] as Option[], + }; + + async componentDidMount() { + const { rootItem, values } = this.props; + const actualRootItem = rootItem ?? SystemGlossaries; + + const glossary = await glossariesService.getGlossaryItem(actualRootItem); + if (glossary) { + const options: Option[] | undefined = glossary.children.map((x: { id: any; name: any }) => { + return { + _id: x.id, + name: x.name, + }; + }); + + const selectedOptions: Option[] = []; + + if (values) { + for (const option of values) { + const foundOption = options.filter((x) => Number(x._id) === Number((option.value as GeneralIdRef).id))[0]; + selectedOptions.push(foundOption); + } + } + + this.setState({ options, selectedOptions }); + } + } + + doOnChange = (newSelectedOptions: Option[]) => { + const { onChange } = this.props; + + const { name } = this.props; + + var values: CustomFieldValue[] = newSelectedOptions.map((x) => { + return { + value: MakeGeneralIdRef(x._id as unknown as bigint), + displayValue: x.name, + }; + }); + + if (onChange) onChange(name, values); + }; + + handleChange = (e: React.ChangeEvent) => { + let { options, selectedOptions } = this.state; + const input = e.currentTarget; + + const id: number = Number(input.value); + + selectedOptions = options.filter((x) => x._id === id); + + this.setState({ selectedOptions }); + this.doOnChange(selectedOptions); + }; + + handleAdd = (item: Option) => { + const { selectedOptions } = this.state; + + selectedOptions.push(item); + + this.setState({ selectedOptions }); + this.doOnChange(selectedOptions); + }; + + handleDelete = (item: Option) => { + let { selectedOptions } = this.state; + + selectedOptions = selectedOptions.filter((x) => x !== item); + + this.setState({ selectedOptions }); + this.doOnChange(selectedOptions); + }; + + render() { + const { includeLabel, name, label, error, maxEntries } = this.props; + const { options, selectedOptions } = this.state; + + if (maxEntries == 1) { + let value = selectedOptions[0]?._id; + return ( + + ); + } +} + +export default SequencePicker; diff --git a/src/components/pickers/SsoProviderPicker.tsx b/src/components/pickers/SsoProviderPicker.tsx new file mode 100644 index 0000000..2957c64 --- /dev/null +++ b/src/components/pickers/SsoProviderPicker.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import Select from "../common/Select"; +import Option from "../common/option"; +import { GeneralIdRef } from "../../utils/GeneralIdRef"; +import ssoManagerService from "../../modules/manager/ssoManager/services/ssoManagerService"; + +interface SsoProviderPickerProps { + name: string; + label: string; + error?: string; + value: any; + domain? : GeneralIdRef; + onChange?: (name: string, value: GeneralIdRef) => void; +} + +interface SsoProviderPickerState { + options?: Option[]; +} + +class SsoProviderPicker extends React.Component { + state = { options: undefined }; + + async componentDidMount() { + const pagedData = await ssoManagerService.getSsoProviders(0, 10, "name", true); + if (pagedData) { + const options: Option[] = (pagedData.data as any[]).map(x => { + return { + _id: x.id, + name: x.name, + }; + }); + + this.setState({ options }); + } + } + + handleChange = (e: React.ChangeEvent) => { + const { onChange } = this.props; + const input = e.currentTarget; + + const generalIdRef: GeneralIdRef = { + id: BigInt(input.value), + }; + + if (onChange) onChange(input.name, generalIdRef); + }; + + render() { + const { name, label, error, value } = this.props; + const { options } = this.state; + + return ( + + ); + } +} + +export default UserPicker; diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..7741555 --- /dev/null +++ b/src/config.json @@ -0,0 +1,3 @@ +{ + "applicationName":"e-suite" +} \ No newline at end of file diff --git a/src/img/E-SUITE_logo.svg b/src/img/E-SUITE_logo.svg new file mode 100644 index 0000000..164e470 --- /dev/null +++ b/src/img/E-SUITE_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/logo.tsx b/src/img/logo.tsx new file mode 100644 index 0000000..6919f27 --- /dev/null +++ b/src/img/logo.tsx @@ -0,0 +1,15 @@ +import { FunctionComponent } from "react"; +import logo from "./E-SUITE_logo.svg" + +interface LogoProps { + className? : string; + height? : string + width? : string + alt?: string +} + +const Logo: FunctionComponent = (props:LogoProps) => { + return ( {props.alt}); +} + +export default Logo; \ No newline at end of file diff --git a/src/img/logo_esuite-white.svg b/src/img/logo_esuite-white.svg new file mode 100644 index 0000000..25e87a6 --- /dev/null +++ b/src/img/logo_esuite-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..a1070c9 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; +import { BrowserRouter } from "react-router-dom"; + + +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); +root.render( + + + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/modules/audit/audit.tsx b/src/modules/audit/audit.tsx new file mode 100644 index 0000000..739e197 --- /dev/null +++ b/src/modules/audit/audit.tsx @@ -0,0 +1,114 @@ +import React, { Component } from 'react'; +import Column from '../../components/common/columns'; +import { Paginated } from '../../services/Paginated'; +import withRouter from '../../utils/withRouter'; +import AuditTable from './components/auditTable'; +import auditService, { AuditLogEntry } from './services/auditService'; +import equal from "fast-deep-equal"; +import Loading from '../../components/common/Loading'; + +interface AuditState{ + loaded: boolean; + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class Audit extends Component< any, any, AuditState> { + state = { + loaded: false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "dateTime", label: "Timing", order: "desc" }, + filters: new Map() + } + + componentDidMount = () => { + this.loadData(); + }; + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: AuditState | undefined): void { + if (!(equal(this.props?.router.location.search, prevProps?.router.location.search) && equal(this.props?.router.params.auditId, prevProps?.router.params.auditId))) + { + let {pagedData, sortColumn} = this.state; + + pagedData = { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }; + sortColumn = { key: "dateTime", label: "Timing", order: "desc" }; + + this.setState({pagedData, sortColumn}); + + this.changePage(pagedData.page, pagedData.pageSize); + } + } + + loadData = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { pagedData } = this.state; + + pagedData.page = page; + pagedData.pageSize = pageSize; + + this.setState( {loaded: false, pagedData } ); + await this.doSearch(); + } + + onSort = async(sortColumn : Column) => { + this.setState({loaded: false, sortColumn}); + + await this.doSearch(); + } + + onSearch = async ( name: string, value: string) => { + const { filters } = this.state; + filters.set(name, value); + + this.setState( { filters }); + + await this.doSearch(); + }; + + doSearch = async () => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + const auditId = this.props?.router?.params?.auditId ?? ""; + var primaryOnly = this.props.router.query.get("primaryOnly"); + const isPrimaryOnly : boolean = (primaryOnly === null) || (primaryOnly.toLowerCase() === "true"); + + const pagedData = await auditService.getLog(auditId, isPrimaryOnly, page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + render(): JSX.Element { + const {loaded, pagedData, sortColumn } = this.state; + + return ( + +
    + +
    +
    + ); + } +}; + + +const HOCAudit = withRouter(Audit); + +export default HOCAudit; \ No newline at end of file diff --git a/src/modules/audit/components/auditTable.tsx b/src/modules/audit/components/auditTable.tsx new file mode 100644 index 0000000..e84d171 --- /dev/null +++ b/src/modules/audit/components/auditTable.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import Column from "../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../components/common/Table"; +import { Paginated } from "../../../services/Paginated"; +import { AuditLogEntry } from "../services/auditService"; + +interface AuditFieldValues{ + OldDisplayName? : string, + OldValue? : string, + NewDisplayName? : string + NewValue? : string +} + +interface AuditFieldChanges{ + [fieldname: string]: AuditFieldValues; +} + +interface AuditFieldChangeValues{ + fieldName : string; + oldDisplayName? : string, + oldValue? : string, + newDisplayName? : string + newValue? : string +} + +class AuditTable extends React.Component> { + fieldColumns : Column[] = [ + { key: "fieldName", label: "Field" }, + { key: "oldDisplayName", label: "Old Value", content: (item) => { + if (item.oldDisplayName !== undefined) + return <>{item.oldDisplayName} + return <>{item.oldValue !== undefined ? String(item.oldValue) : ""} + } }, + { key: "newDisplayName", label: "New Value", content: (item) => { + if (item.newDisplayName !== undefined) + return <>{item.newDisplayName} + return <>{item.newValue !== undefined ? String(item.newValue) : ""} + } }, + ] + + columns : Column[] = [ + { key: "dateTime", label: "Timing", order: "asc", searchable: false }, + { key: "userDisplayName", label: "User Name", order: "asc" }, + { key: "comment", label: "Comment", order: "asc" }, + { key: "entityDisplayName", label: "Entity Display Name", order: "asc", searchable: false }, + { key: "type", label: "Type", order: "asc" }, + { key: "displayName", label: "DisplayName", order: "asc" }, + + { key: "fields", label: "Changes", order: "asc", searchable: false, content: (item)=>{ + if (item.type === "Delete" || item.type === "Purge") + { + if (item.displayName !== "") + return <> + } + + const fieldsObject : AuditFieldChanges = JSON.parse(item!.fields); + + let data : AuditFieldChangeValues[] = []; + Object.keys(fieldsObject).forEach( + (key, index) => { + let dataItem : AuditFieldChangeValues = + { + fieldName: key, + oldValue : fieldsObject[key].OldValue, + oldDisplayName : fieldsObject[key].OldDisplayName, + newValue : fieldsObject[key].NewValue, + newDisplayName : fieldsObject[key].NewDisplayName + } + + data.push( dataItem ); + }); + + let paginated : Paginated = { + count: 0, + page: 1, + pageSize: 10, + totalPages: 0, + data: data + } + + let fieldColumns : Column[]; + fieldColumns = this.fieldColumns; + + if (item.type === "Create" || item.type === "Restore") + { + fieldColumns = this.fieldColumns.filter( x => x.key !== "oldDisplayName"); + } + + if (item.type === "Delete" || item.type === "Purge") + { + fieldColumns = this.fieldColumns.filter( x => x.key !== "newDisplayName"); + } + + + return ; + } }, + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if (this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : item.entityName, + primaryKey : item.primaryKey + } + } + + render() { + const { data, sortColumn, onChangePage, onSearch } = this.props; + + return
    ; + } +} + +export default AuditTable; \ No newline at end of file diff --git a/src/modules/audit/services/auditService.ts b/src/modules/audit/services/auditService.ts new file mode 100644 index 0000000..abef187 --- /dev/null +++ b/src/modules/audit/services/auditService.ts @@ -0,0 +1,42 @@ +import httpService from "../../../services/httpService"; +import { Paginated } from "../../../services/Paginated"; +import MapToJson from "../../../utils/MapToJson"; + +const apiEndpoint = "/Audit"; + + +export interface AuditLogEntry +{ + id : bigint; + userId : bigint; + userDisplayName : string; + type : string; + dateTime : Date; + fields : string; + comment : string; + entityName : string; + primaryKey : string; + entityDisplayName : string; + displayName : string; +} + +export async function getLog(logEntry : string, primaryOnly : boolean, page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/log", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString, + logEntry : logEntry, + primaryOnly : primaryOnly + } } ); + return response?.data; +} + +const auditService = { + getLog +}; + +export default auditService; diff --git a/src/modules/blockedIPs/blockedIPs.tsx b/src/modules/blockedIPs/blockedIPs.tsx new file mode 100644 index 0000000..20aab28 --- /dev/null +++ b/src/modules/blockedIPs/blockedIPs.tsx @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import { toast } from 'react-toastify'; +import Column from '../../components/common/columns'; +import { Paginated } from '../../services/Paginated'; +import BlockedIPsTable from './components/blockedIPsTable'; +import blockedIPsService, { BlockedIPEntry } from './services/blockedIPsService'; + +interface BlockedIPsState { + pagedData: Paginated, + sortColumn: Column, + filters: Map; +} + +class BlockedIPs extends Component { + state = { + pagedData: { + page: 1, + pageSize: 10, + count: 0, + totalPages: 1, + data: [] + }, + sortColumn: { key: "ipAddress", label: "IP Address", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async (page: number, pageSize: number) => { + const { sortColumn, filters } = this.state; + + const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ pagedData }); + } + } + + onSort = async (sortColumn: Column) => { + const { page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ pagedData, sortColumn }); + } + } + + onSearch = async (name: string, value: string) => { + const { page, pageSize } = this.state.pagedData; + const { sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ filters, pagedData }); + } + }; + + onUnblock = async (item?: BlockedIPEntry) => { + const response = await blockedIPsService.UnBlockIp(item?.ipAddress); + if (response) { + this.componentDidMount(); + toast.info(`IP Address '${item?.ipAddress}' Unblocked.`); + } + } + + render(): JSX.Element { + const { pagedData, sortColumn } = this.state; + + return ( +
    + +
    + ); + } +}; + +export default BlockedIPs; diff --git a/src/modules/blockedIPs/components/blockedIPsTable.tsx b/src/modules/blockedIPs/components/blockedIPsTable.tsx new file mode 100644 index 0000000..c475189 --- /dev/null +++ b/src/modules/blockedIPs/components/blockedIPsTable.tsx @@ -0,0 +1,39 @@ +import { faUnlock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React from "react"; +import { ButtonType } from "../../../components/common/Button"; +import Column from "../../../components/common/columns"; +import ConfirmButton from "../../../components/common/ConfirmButton"; +import Table, { PublishedTableProps } from "../../../components/common/Table"; +import authentication from "../../frame/services/authenticationService"; +import { BlockedIPEntry } from "../services/blockedIPsService"; + +class BlockedIPsTable extends React.Component> { + canUnblockBlockedIPAddress = authentication.hasAccess("UnlockIPAddress"); + + columns : Column[] = [ + { key: "ipAddress", label: "IP Address", order: "asc" }, + { key: "numberOfAttempts", label: "Number of Attempts", order: "asc", searchable: false }, + { key: "blockedAt", label: "Date", order: "asc", searchable: false }, + { key: "unblockedIn", label: "Unblocked In (Minutes)", order: "asc", searchable: false }, + { + key: "action", label: "", searchable: false, content: (item) => { + if (this.canUnblockBlockedIPAddress) return <>; return (<>) + } + } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if (this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + render() { + const { data, sortColumn, onChangePage, onSearch } = this.props; + + return
    ; + } +} + +export default BlockedIPsTable; \ No newline at end of file diff --git a/src/modules/blockedIPs/services/blockedIPsService.ts b/src/modules/blockedIPs/services/blockedIPsService.ts new file mode 100644 index 0000000..71c6b23 --- /dev/null +++ b/src/modules/blockedIPs/services/blockedIPsService.ts @@ -0,0 +1,39 @@ +import httpService from "../../../services/httpService"; +import { Paginated } from "../../../services/Paginated"; +import MapToJson from "../../../utils/MapToJson"; + +const apiEndpoint = "/BlockedIPs"; + + +export type BlockedIPEntry = + { + ipAddress: string; + numberOfAttempts: number; + blockedAt: Date; + unblockedin: number; + } + +export async function getBlockedIps(page: number, pageSize: number, sortKey: string, sortAscending: boolean, filters?: Map): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/blockedIPs", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function UnBlockIp(ipAddress?: string): Promise { + return await httpService.delete(apiEndpoint + "/delete", { data: { ipAddress } }); +} + +const blockedIPsService = { + getBlockedIps, + UnBlockIp +}; + +export default blockedIPsService; diff --git a/src/modules/errorLogs/components/errorLogsTable.tsx b/src/modules/errorLogs/components/errorLogsTable.tsx new file mode 100644 index 0000000..4e68c1f --- /dev/null +++ b/src/modules/errorLogs/components/errorLogsTable.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import Column from "../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../components/common/Table"; +import { ErrorLog } from "../services/errorLogsService"; + +class ErrorLogsTable extends React.Component> { + columns: Column[] = [ + { key: "id", label: "Id", order: "asc" }, + { key: "application", label: "Application", order: "asc" }, + { key: "message", label: "Message", order: "asc" }, + { key: "occuredAt", label: "Occured At", order: "asc", searchable: false } + ]; + + raiseSort = (sortColumn: Column) => { + this.setState({ sortColumn }); + if (this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + render() { + const { data, sortColumn, onChangePage, onSearch } = this.props; + + return
    ; + } +} + +export default ErrorLogsTable; \ No newline at end of file diff --git a/src/modules/errorLogs/errorLogs.tsx b/src/modules/errorLogs/errorLogs.tsx new file mode 100644 index 0000000..06653dc --- /dev/null +++ b/src/modules/errorLogs/errorLogs.tsx @@ -0,0 +1,72 @@ +import { Component } from 'react'; +import Column from '../../components/common/columns'; +import { Paginated } from '../../services/Paginated'; +import ErrorLogsTable from './components/errorLogsTable'; +import errorLogsService, { ErrorLog } from './services/errorLogsService'; + +interface ErrorLogState { + pagedData: Paginated, + sortColumn: Column, + filters: Map; +} + +class ErrorLogs extends Component { + state = { + pagedData: { + page: 1, + pageSize: 10, + count: 0, + totalPages: 1, + data: [] + }, + sortColumn: { key: "Id", label: "Id", order: "desc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async (page: number, pageSize: number) => { + const { sortColumn, filters } = this.state; + + const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ pagedData }); + } + } + + onSort = async (sortColumn: Column) => { + const { page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ pagedData, sortColumn }); + } + } + + onSearch = async (name: string, value: string) => { + const { page, pageSize } = this.state.pagedData; + const { sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ filters, pagedData }); + } + }; + + render(): JSX.Element { + const { pagedData, sortColumn } = this.state; + + return ( +
    + +
    + ); + } +}; + +export default ErrorLogs; diff --git a/src/modules/errorLogs/services/errorLogsService.ts b/src/modules/errorLogs/services/errorLogsService.ts new file mode 100644 index 0000000..5048bd2 --- /dev/null +++ b/src/modules/errorLogs/services/errorLogsService.ts @@ -0,0 +1,34 @@ +import httpService from "../../../services/httpService"; +import { Paginated } from "../../../services/Paginated"; +import MapToJson from "../../../utils/MapToJson"; + +const apiEndpoint = "/exceptionlogs"; + + +export type ErrorLog = + { + id: bigint; + application: string; + aessage: string; + occuredAt: Date; + } + +export async function getErrorLogs(page: number, pageSize: number, sortKey: string, sortAscending: boolean, filters?: Map): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/exceptionlogs", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +const errorLogsService = { + getErrorLogs +}; + +export default errorLogsService; diff --git a/src/modules/frame/components/EmailUserAction.tsx b/src/modules/frame/components/EmailUserAction.tsx new file mode 100644 index 0000000..e6a9fed --- /dev/null +++ b/src/modules/frame/components/EmailUserAction.tsx @@ -0,0 +1,30 @@ +import { Buffer } from "buffer"; +import { Navigate, useParams } from "react-router-dom"; +import { IEmailUserAction, EmailActionType } from "../models/IEmailUserAction"; +import EmailUserActionDiableTwoFactorAuthentication from "./EmailUserActionDisableTwoFactorAuthentication"; +// import EmailUserActionConfirmEmail from "./EmailUserActionConfirmEmail"; +import EmailUserActionPasswordReset from "./EmailUserActionPasswordReset"; +import LoadingPanel from "../../../components/common/LoadingPanel"; + +function EmailUserAction() { + let { token } = useParams(); + + if (token) { + const base64ToString = Buffer.from(token, "base64").toString(); + const emailUserAction: IEmailUserAction = JSON.parse(base64ToString); + + switch (emailUserAction.emailActionType) { + case EmailActionType.PasswordReset: + return ; + case EmailActionType.DisableAuthenticator: + return ; + case EmailActionType.ConfirmEmailAddress: + window.location.replace("/account/confirmaccount/" + token); + return ; + } + } + + return ; +} + +export default EmailUserAction; diff --git a/src/modules/frame/components/EmailUserActionConfirmEmail.tsx b/src/modules/frame/components/EmailUserActionConfirmEmail.tsx new file mode 100644 index 0000000..60d7080 --- /dev/null +++ b/src/modules/frame/components/EmailUserActionConfirmEmail.tsx @@ -0,0 +1,141 @@ +import Form, { FormState, FormData, businessValidationResult } from "../../../components/common/Form"; +import Joi from "joi"; +import authentication from "../services/authenticationService"; +import { IEmailUserAction } from "../models/IEmailUserAction"; +import { InputType } from "../../../components/common/Input"; + +export interface EmailUserActionConfirmEmailData extends FormData { + password: string; + confirmPassword: string; + emailConfirmed: boolean; +} + +export interface EmailUserActionConfirmEmailState extends FormState { + data: EmailUserActionConfirmEmailData; + hasTwelveCharacters: boolean, + hasSpecialCharacter: boolean, + hasUppercaseLetter: boolean, + hasLowercaseLetter: boolean, + hasNumber: boolean +} + +class EmailUserActionConfirmEmail extends Form { + state = { + loaded: true, + passwordMaxLenght: 255, + data: { password: "", confirmPassword: "", emailConfirmed: false }, + errors: {}, + hasTwelveCharacters: false, + hasSpecialCharacter: false, + hasUppercaseLetter: false, + hasLowercaseLetter: false, + hasNumber: false + }; + + labelPassword = "Password"; + labelConfirmPassword = "Confirm Password"; + labelConfirmEmail = "Activate"; + + schema = { + password: Joi.string().required().min(12).label(this.labelPassword), + confirmPassword: Joi.string() + .when("password", { + is: "", + then: Joi.optional(), + otherwise: Joi.valid(Joi.ref("password")).error(() => { + const e = new Error("Passwords must match"); + e.name = "confirmPassword"; + return e; + }), + }) + .label(this.labelConfirmPassword), + + emailConfirmed: Joi.boolean(), + }; + + BusinessValidation(): businessValidationResult | null { + const { password, confirmPassword } = this.state.data; + + if (password !== confirmPassword) { + return { + details: [ + { + path: "confirmPassword", + message: "You need to confirm by typing exactly the same as the new password", + }, + ], + }; + } + return null; + } + + handlePasswordChange = async (e: React.ChangeEvent) => { + const stateData = this.state; + stateData.data.password = e.currentTarget.value; + stateData.data.confirmPassword = ""; + stateData.hasNumber = /\d+/g.test(stateData.data.password); + stateData.hasLowercaseLetter = /[a-z]/g.test(stateData.data.password); + stateData.hasUppercaseLetter = /[A-Z]/g.test(stateData.data.password);; + stateData.hasSpecialCharacter = /[ ~`! @#$%^&*()_+\-=[\]{};:\\|,.'"<>/?]/.test(stateData.data.password); + stateData.hasTwelveCharacters = stateData.data.password.length >= 12; + this.setState(stateData) + }; + + doSubmit = async (buttonName : string) => { + const { emailUserAction } = this.props; + const { password } = this.state.data; + + const action: IEmailUserAction = { + email: emailUserAction.email, + token: emailUserAction.token, + password: password, + emailActionType: emailUserAction.emailActionType, + }; + + try { + const callResult = await authentication.completeEmailAction(action); + if (callResult === 1) { + let data = { ...this.state.data }; + data.emailConfirmed = true; + this.setState({ data }); + setTimeout(function () { + window.location.replace('/login'); + }, 1000); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + render() { + const { emailConfirmed, password, confirmPassword } = this.state.data; + const { hasNumber, hasLowercaseLetter, hasSpecialCharacter, hasUppercaseLetter, hasTwelveCharacters, passwordMaxLenght } = this.state; + const isFormValid = password !== "" && password === confirmPassword && hasNumber && hasLowercaseLetter && hasSpecialCharacter && hasUppercaseLetter && hasTwelveCharacters; + if (emailConfirmed) { + return
    Success, your e-mail is confirmed. You can now log in.
    ; + } + + return ( + <> +
    To activate your account, please enter a password
    + +
    + {this.renderError("_general")} + {this.renderInputWithChangeEvent("password", "", InputType.password, undefined, this.handlePasswordChange, undefined, this.labelPassword, passwordMaxLenght)} +
    Password requires a minimum of 12 characters containing a combination of:
    +
      +
    • At least 1 symbol
    • +
    • At least 1 number
    • +
    • At least 1 lowercase letter
    • +
    • At least 1 uppercase letter
    • +
    + {this.renderInput("confirmPassword", "", InputType.password, undefined, undefined, this.labelConfirmPassword, passwordMaxLenght)} + {this.renderButton(this.labelConfirmEmail, "confirmEmail", undefined, undefined, isFormValid)} + + + ); + } +} + +export default EmailUserActionConfirmEmail; diff --git a/src/modules/frame/components/EmailUserActionDisableTwoFactorAuthentication.tsx b/src/modules/frame/components/EmailUserActionDisableTwoFactorAuthentication.tsx new file mode 100644 index 0000000..d77ae83 --- /dev/null +++ b/src/modules/frame/components/EmailUserActionDisableTwoFactorAuthentication.tsx @@ -0,0 +1,63 @@ +import Form, { FormState, FormData } from "../../../components/common/Form"; +import Joi from "joi"; +import authentication from "../services/authenticationService"; +import { IEmailUserAction } from "../models/IEmailUserAction"; +import Button, { ButtonType } from "../../../components/common/Button"; + +export interface EmailUserActionDiableTwoFactorAuthenticationData extends FormData { + authenticatorDisabled: boolean; +} + +export interface EmailUserActionDiableTwoFactorAuthenticationState extends FormState { + data: EmailUserActionDiableTwoFactorAuthenticationData; +} + +class EmailUserActionDiableTwoFactorAuthentication extends Form { + state = { + loaded: true, + data: { authenticatorDisabled: false }, + errors: {}, + }; + + labelChangePassword = "Disable Authenticator"; + + schema = { + authenticatorDisabled: Joi.boolean(), + }; + + doSubmit = async () => { + const { emailUserAction } = this.props; + + const action: IEmailUserAction = { + email: emailUserAction.email, + token: emailUserAction.token, + password: "", + emailActionType: emailUserAction.emailActionType, + }; + + const callResult = await authentication.completeEmailAction(action); + if (callResult === 1) { + let data = { ...this.state.data }; + data.authenticatorDisabled = true; + this.setState({ data }); + } + }; + + render() { + const { authenticatorDisabled } = this.state.data; + + if (authenticatorDisabled) { + return
    Your authenticator has been disabled. You can now log in without two factor authentication
    ; + } + + return ( + <> +
    Disable two factor authentication
    + + + + ); + } +} + +export default EmailUserActionDiableTwoFactorAuthentication; diff --git a/src/modules/frame/components/EmailUserActionPasswordReset.tsx b/src/modules/frame/components/EmailUserActionPasswordReset.tsx new file mode 100644 index 0000000..1bbd0a0 --- /dev/null +++ b/src/modules/frame/components/EmailUserActionPasswordReset.tsx @@ -0,0 +1,136 @@ +import Joi from "joi"; +import Form, { businessValidationResult, FormData, FormState } from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { IEmailUserAction } from "../models/IEmailUserAction"; +import authentication from "../services/authenticationService"; + +export interface EmailUserActionPasswordResetData extends FormData { + password: string; + confirmPassword: string; + passwordChanged: boolean; +} + +export interface EmailUserActionPasswordResetState extends FormState { + data: EmailUserActionPasswordResetData; +} + +class EmailUserActionPasswordReset extends Form { + state = { + loaded: true, + passwordMaxLenght: 255, + data: { password: "", confirmPassword: "", passwordChanged: false }, + errors: {}, + hasTwelveCharacters: false, + hasSpecialCharacter: false, + hasUppercaseLetter: false, + hasLowercaseLetter: false, + hasNumber: false + }; + + labelPassword = "New Password"; + labelConfirmPassword = "Confirm Password"; + labelChangePassword = "Save"; + + schema = { + password: Joi.string().required().min(12).label(this.labelPassword), + confirmPassword: Joi.string() + .when("password", { + is: "", + then: Joi.optional(), + otherwise: Joi.valid(Joi.ref("password")).error(() => { + const e = new Error("Passwords must match"); + e.name = "confirmPassword"; + return e; + }), + }) + .label(this.labelConfirmPassword), + + passwordChanged: Joi.boolean(), + }; + + BusinessValidation(): businessValidationResult | null { + const { password, confirmPassword } = this.state.data; + + if (password !== confirmPassword) { + return { + details: [ + { + path: "confirmPassword", + message: "You need to confirm by typing exactly the same as the new password", + }, + ], + }; + } + + return null; + } + + handlePasswordChange = async (e: React.ChangeEvent) => { + const { data } = this.state; + data.password = e.currentTarget.value; + data.confirmPassword = ""; + const stateData = this.state; + stateData.hasNumber = /\d+/g.test(data.password); + stateData.hasLowercaseLetter = /[a-z]/g.test(data.password); + stateData.hasUppercaseLetter = /[A-Z]/g.test(data.password);; + stateData.hasSpecialCharacter = /[ ~`! @#$%^&*()_+\-=[\]{};:\\|,.'"<>/?]/.test(data.password); + stateData.hasTwelveCharacters = data.password.length >= 12; + this.setState(stateData); + }; + + doSubmit = async (buttonName: string) => { + const { emailUserAction } = this.props; + const { password } = this.state.data; + + const action: IEmailUserAction = { + email: emailUserAction.email, + token: emailUserAction.token, + password: password, + emailActionType: emailUserAction.emailActionType, + }; + + try { + const callResult = await authentication.completeEmailAction(action); + if (callResult === 1) { + let data = { ...this.state.data }; + data.passwordChanged = true; + this.setState({ data }); + setTimeout(function () { + window.location.replace('/login'); + }, 1000); + } + } + catch (ex: any) { + this.handleGeneralError(ex); + } + }; + + render() { + const { passwordChanged, password, confirmPassword } = this.state.data; + const { hasNumber, hasLowercaseLetter, hasSpecialCharacter, hasUppercaseLetter, hasTwelveCharacters, passwordMaxLenght } = this.state; + const isFormValid = password !== "" && password === confirmPassword && hasNumber && hasLowercaseLetter && hasSpecialCharacter && hasUppercaseLetter && hasTwelveCharacters; + if (passwordChanged) { + return
    Your password has been reset. Please contact your admin if this wasn't you.
    ; + } + + return ( + <> +
    + {this.renderError("_general")} + {this.renderInputWithChangeEvent("password", "", InputType.password, undefined, this.handlePasswordChange, undefined, this.labelPassword, passwordMaxLenght)} +
    Password requires a minimum of 12 characters containing a combination of:
    +
      +
    • At least 1 symbol
    • +
    • At least 1 number
    • +
    • At least 1 lowercase letter
    • +
    • At least 1 uppercase letter
    • +
    + {this.renderInput("confirmPassword", "", InputType.password, undefined, undefined, this.labelConfirmPassword, passwordMaxLenght)} + {this.renderButton(this.labelChangePassword, "save", undefined, undefined, isFormValid)} + + + ); + } +} + +export default EmailUserActionPasswordReset; diff --git a/src/modules/frame/components/ForgotPassword.tsx b/src/modules/frame/components/ForgotPassword.tsx new file mode 100644 index 0000000..924dfb9 --- /dev/null +++ b/src/modules/frame/components/ForgotPassword.tsx @@ -0,0 +1,69 @@ +import Joi from "joi"; +import Form, { FormData, FormState } from "../../../components/common/Form"; +import authentication from "../services/authenticationService"; + +export interface ForgotPasswordData extends FormData { + username: string; + emailSent: boolean; +} + +export interface ForgotPasswordtate extends FormState { + data: ForgotPasswordData; +} + +class ForgotPassword extends Form { + state = { + loaded: true, + data: { username: "", emailSent: false }, + errors: {}, + }; + + schema = { + username: Joi.string() + .required() + .email({ tlds: { allow: false } }) + .label("Username"), + emailSent: Joi.boolean().required(), + }; + + doSubmit = async (buttonName : string) => { + try { + let { data } = this.state; + + const response = await authentication.forgotPassword(data.username); + if (response) { + data.emailSent = true; + this.setState({ data }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + render() { + const { emailSent } = this.state.data; + + let content = ( +
    + {this.renderError("_general")} + {this.renderInput("username", "Username")} + {this.renderButton("Reset password")} + + ); + if (emailSent) { + content =
    An email with a password reset link has been sent.
    ; + } + + return ( +
    +
    +

    Forgot password

    +
    + {content} +
    + ); + } +} + +export default ForgotPassword; diff --git a/src/modules/frame/components/LeftMenu.tsx b/src/modules/frame/components/LeftMenu.tsx new file mode 100644 index 0000000..c7c70c9 --- /dev/null +++ b/src/modules/frame/components/LeftMenu.tsx @@ -0,0 +1,92 @@ +import * as React from "react"; +import authentication from "../services/authenticationService"; +import '../../../Sass/_leftMenu.scss'; +import { faCog, faCogs, faHome, faPrint } from "@fortawesome/pro-thin-svg-icons"; +import LeftMenuItem from "./LeftMenuItem"; +import LeftMenuSubMenu, { LOCLeftMenuSubMenu } from "./LeftMenuSubMenu"; + +interface LeftMenuProps { + +} + +interface LeftMenuState { + openMenuItem? : LOCLeftMenuSubMenu; +} + +class LeftMenu extends React.Component { + state : LeftMenuState = { + openMenuItem : undefined + } + + componentDidMount(): void { + document.body.addEventListener('click', () => { + this.setState( { openMenuItem : undefined }) + }, true); + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + if (prevState === this.state) + { + this.setState( { openMenuItem : undefined }) + } + } + + handleClick = (menuItem: LOCLeftMenuSubMenu) => { + const { openMenuItem } = this.state; + + const newMenuItem = openMenuItem === menuItem ? undefined : menuItem; + + this.setState( { openMenuItem : newMenuItem }) + }; + + render() { + const viewOrganisation = authentication.hasAccess("ViewOrganisation"); + + const viewUser = authentication.hasAccess("ViewUser" ); + const viewDomain = authentication.hasAccess("ViewDomain" ); + const viewGlossary = authentication.hasAccess("ViewGlossary"); + const viewFormTemplate = authentication.hasAccess("ViewFormTemplate"); + const viewField = authentication.hasAccess("ViewField"); + const viewSequence = authentication.hasAccess("ViewSequence"); + const viewSsoManager = authentication.hasAccess("ViewSsoProviders"); + + const viewAdmin = viewUser || viewDomain || viewGlossary || viewFormTemplate || viewField || viewSequence; + + const viewAuditLog = authentication.hasAccess("ViewAuditLog"); + const viewBlockedIPAddresses = authentication.hasAccess("ViewBlockedIPAddresses"); + const viewErrorLogs = authentication.hasAccess("ViewErrorLogs"); + + const viewSupport = viewAuditLog || viewBlockedIPAddresses || viewErrorLogs; + + const { openMenuItem } = this.state; + + return ( + <> +
    + + {viewOrganisation && } + {viewAdmin && + {viewUser && } + {viewDomain && } + {viewGlossary && } + {viewFormTemplate && } + {viewField && } + {viewSequence && } + {viewSsoManager && } + + } + {viewSupport && + {viewAuditLog && } + {viewBlockedIPAddresses && } + {viewErrorLogs && } + } +
    + {openMenuItem &&
    {openMenuItem.props.children}
    } + + ); + } +} + + + +export default LeftMenu; diff --git a/src/modules/frame/components/LeftMenuItem.tsx b/src/modules/frame/components/LeftMenuItem.tsx new file mode 100644 index 0000000..ac0d6f0 --- /dev/null +++ b/src/modules/frame/components/LeftMenuItem.tsx @@ -0,0 +1,49 @@ +import { IconDefinition } from "@fortawesome/pro-thin-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { Component } from "react"; +import { Link } from "react-router-dom"; +import withRouter, { RouterProps } from "../../../utils/withRouter"; + +interface LeftMenuItemProps extends RouterProps { + to : string; + icon? : IconDefinition; + label : string; +} + +class LOCLeftMenuItem extends Component { + isSelected = ():boolean => { + const { to } = this.props; + const { pathname } = this.props.router.location; + let isSelected : boolean = false; + if (to === '/' ? (pathname === to) : pathname.toLowerCase().startsWith(to)){ + isSelected = true; + } + + return isSelected; + } + + render() { + const { to, icon, label } = this.props; + + let className = ""; + + if (this.isSelected()) { + className += " leftMenuSelected"; + } + + if ( icon) { + return ( +
    {label}
    + ); + } + + return ( + {label} + ); + } +} + +const LeftMenuItem = withRouter(LOCLeftMenuItem); + +export default LeftMenuItem; +export { LOCLeftMenuItem }; diff --git a/src/modules/frame/components/LeftMenuSubMenu.tsx b/src/modules/frame/components/LeftMenuSubMenu.tsx new file mode 100644 index 0000000..5f00544 --- /dev/null +++ b/src/modules/frame/components/LeftMenuSubMenu.tsx @@ -0,0 +1,73 @@ +import { IconDefinition } from "@fortawesome/fontawesome-common-types"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React from "react"; +import withRouter, { RouterProps } from "../../../utils/withRouter"; + +interface LeftMenuSubMenuProps extends RouterProps{ + icon : IconDefinition; + label : string; + openMenu? : LOCLeftMenuSubMenu; + children : (false | JSX.Element)[]; + onClick? : ( menuItem : LOCLeftMenuSubMenu ) => void; +} + +interface LeftMenuSubMenuState { + +} + +class LOCLeftMenuSubMenu extends React.Component { + state = { } + + handleClick = (): void => + { + const { onClick } = this.props; + + if (onClick !== undefined) + onClick(this); + } + + isChildSelected = (child : JSX.Element):boolean => { + const { to } = child.props; + const { pathname } = this.props.router.location; + let isSelected : boolean = false; + if (to === '/' ? (pathname === to) : pathname.toLowerCase().startsWith(to)){ + isSelected = true; + } + + return isSelected; + } + + isAnyChildSelected = ():boolean => { + const { children } = this.props; + + let childIsSelected = false; + children.forEach(child => { + if (child === false){ + return; + } + + if (this.isChildSelected(child)) + childIsSelected = true; + }); + + return childIsSelected; + } + + render() { + const { icon, label, openMenu } = this.props; + + const selected = this === openMenu || this.isAnyChildSelected(); + + let className = "LeftMenuItem leftMenuSubMenu"; + + if (selected) { + className += " leftMenuSubMenuOpen"; + } + + return (
    {label}
    ); + } +} + +const LeftMenuSubMenu = withRouter(LOCLeftMenuSubMenu); +export default LeftMenuSubMenu; +export {LOCLeftMenuSubMenu} \ No newline at end of file diff --git a/src/modules/frame/components/LoginForm.tsx b/src/modules/frame/components/LoginForm.tsx new file mode 100644 index 0000000..5e28cbf --- /dev/null +++ b/src/modules/frame/components/LoginForm.tsx @@ -0,0 +1,166 @@ +import { Link, Navigate } from "react-router-dom"; +import Joi from "joi"; +import Form, { FormState, FormData } from "../../../components/common/Form"; +import authentication from "../services/authenticationService"; +import { InputType } from "../../../components/common/Input"; +import { ButtonType } from "../../../components/common/Button"; + +//import '../../../Sass/login.scss'; + +export interface LoginFormStateData extends FormData { + username: string; + password: string; + tfaNeeded: boolean; + requestTfaRemoval: boolean; + securityCode: string; +} + +export interface LoginFormState extends FormState { + passwordMaxLength: number, + isInNextStage: boolean, + emailSent: boolean, + data: LoginFormStateData; +} + +class LoginForm extends Form { + state = { + loaded: true, + passwordMaxLength: 255, + isInNextStage: false, + emailSent: false, + data: { + username: "", + password: "", + tfaNeeded: false, + requestTfaRemoval: false, + securityCode: "", + }, + errors: {}, + }; + + schema = { + username: Joi.string() + .required() + .email({ tlds: { allow: false } }) + .label("Email"), + password: Joi.string().required().label("Password"), + tfaNeeded: Joi.boolean().required(), + requestTfaRemoval: Joi.boolean().required(), + securityCode: Joi.string().allow("").label("Authenticate"), + }; + + doSubmit = async (buttonName : string) => { + const { data } = this.state; + await this.performLogin(data); + }; + + handleNextClick = async (event: React.MouseEvent) => { + const data: LoginFormStateData = { ...this.state.data }; + var validationResult = this.schema.username.validate(data.username); + if (validationResult.error === undefined) { + const stateData = this.state; + stateData.isInNextStage = true; + this.setState(stateData); + } + } + + handleForgetPassword = async () => { + try { + const stateData = this.state; + await authentication.forgotPassword(stateData.data.username); + stateData.emailSent = true; + stateData.data.username = ""; + stateData.data.password = ""; + this.setState(stateData); + } + catch (ex: any) { + this.handleGeneralError(ex); + } + }; + + authenticationWorkAround = async () => { + const data: LoginFormStateData = { ...this.state.data }; + data.requestTfaRemoval = true; + + await this.performLogin(data); + + this.setState({ data }); + }; + + private async performLogin(data: LoginFormStateData) { + try { + let result = await authentication.login(data.username, data.password, data.securityCode, data.requestTfaRemoval); + + switch (result) { + case 1: //requires tfa + const { data } = this.state; + + if (data.tfaNeeded === true) { + //TFA removal Request accepted. + } else { + data.tfaNeeded = true; + + this.setState({ data }); + } + break; + case 2: //logged in + window.location.href = "/"; + break; + default: + break; //treat at though not logged in. + } + } catch (ex: any) { + this.handleGeneralError(ex); + } + } + + render() { + window.location.replace("/login"); + + const { tfaNeeded, requestTfaRemoval} = this.state.data; + const { isInNextStage, data, emailSent, passwordMaxLength } = this.state; + const result = this.schema.username.validate(data.username); + const validEmail = (result.error === undefined) ? true : false; + + if (authentication.getCurrentUser()) return ; + + const requestTfaRemovalPanel =
    An email has been sent to you so that you can regain control of your account.
    ; + + const loginPanel = ( + <>
    + {this.renderInput("username", "", InputType.text, isInNextStage, undefined, "Email", undefined, undefined,"username")} + {this.renderInput("password", "", InputType.password, emailSent, undefined, "Password", passwordMaxLength, isInNextStage, "current-password")} + {!isInNextStage && this.renderButton("Next", "login", this.handleNextClick, "next", validEmail, ButtonType.primary, true)} + {isInNextStage &&
    + {this.renderButton("Login", "login", undefined, "login", !emailSent)} +
    + } + + {isInNextStage &&
    + {this.renderButton("Forgotten Password", "forgot-password", this.handleForgetPassword, "forgot-password", validEmail, ButtonType.secondary, true)} +
    } + {emailSent &&
    If you have a registered account, you will receive an email.
    } + {this.renderError("_general")} + + ); + + const tfaPanel = ( +
    + {this.renderError("_general")} + {this.renderInput("securityCode", "Authenticate")} + {this.renderButton("Authenticate")} + + My Authenticator is not working + + + ); + + return ( +
    + {requestTfaRemoval ? requestTfaRemovalPanel : tfaNeeded ? tfaPanel : loginPanel} +
    + ); + } +} + +export default LoginForm; diff --git a/src/modules/frame/components/Logout.tsx b/src/modules/frame/components/Logout.tsx new file mode 100644 index 0000000..bd11cab --- /dev/null +++ b/src/modules/frame/components/Logout.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import authentication from "../services/authenticationService"; + +class Logout extends React.Component { + componentDidMount() { + authentication.logout(); + if (window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN) { + window.location.href = "/account/logout" + } + else { + window.location.href = "/" + } + } + + render() { + return
    Logging out
    ; + } +} + +export default Logout; diff --git a/src/modules/frame/components/Mainframe.tsx b/src/modules/frame/components/Mainframe.tsx new file mode 100644 index 0000000..7df4f09 --- /dev/null +++ b/src/modules/frame/components/Mainframe.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import TopMenu from "./TopMenu"; +import LeftMenu from "./LeftMenu"; + +import "../../../Sass/_frame.scss"; + + +type MainFrameProps = { + title?: string; + children?: React.ReactNode; // 👈️ type children +}; + +const Mainframe = (props: MainFrameProps): JSX.Element => { + return ( +
    + +
    +
    + +
    +
    + {props.children} +
    +
    +
    + ); +}; + +export default Mainframe; diff --git a/src/modules/frame/components/NotFound.tsx b/src/modules/frame/components/NotFound.tsx new file mode 100644 index 0000000..aef445d --- /dev/null +++ b/src/modules/frame/components/NotFound.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +function NotFound() { + return

    Not found

    ; +} + +export default NotFound; diff --git a/src/modules/frame/components/Switch.tsx b/src/modules/frame/components/Switch.tsx new file mode 100644 index 0000000..f1142bd --- /dev/null +++ b/src/modules/frame/components/Switch.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; + +export interface SwitchProps { + children: React.ReactNode; +} + +export class Switch extends React.PureComponent { + render() { + const children = React.Children.toArray(this.props.children); + + let caseComponent: any = children.filter((c:any) => { + return c.type === Case && c.props.condition === true; + }); + + if (!caseComponent || caseComponent.length === 0) { + caseComponent = children.filter((c: any) => c.type === Else); + } + + return ( + + { caseComponent } + + ); + } +} + +export interface CaseProps { + condition: boolean; + children: React.ReactNode; +} + +export class Case extends React.PureComponent { + render() { + const { condition, children } = this.props; + + return ( + + { condition ? children : null } + + ); + } +} + +export class Else extends React.PureComponent +{ + render() { + return this.props.children; + } +} \ No newline at end of file diff --git a/src/modules/frame/components/TopMenu.tsx b/src/modules/frame/components/TopMenu.tsx new file mode 100644 index 0000000..5a36ae0 --- /dev/null +++ b/src/modules/frame/components/TopMenu.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { Navbar, Nav, NavDropdown } from "react-bootstrap"; +import "bootstrap/dist/css/bootstrap.css"; +import Logo from "../../../img/logo"; + +import '../../../Sass/_nav.scss'; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import {faArrowRightFromBracket} from "@fortawesome/free-solid-svg-icons"; +import {faUser} from "@fortawesome/free-solid-svg-icons"; +import { getCurrentUser } from "../services/authenticationService"; + +export interface TopMenuProps{ + title?: string; +} + +function TopMenu(props : TopMenuProps) { + const user = getCurrentUser(); + + return ( + + + + +
    {props.title}
    +
    + + Account + Logout {user?.name} + +
    +
    + ); +} + +export default TopMenu; diff --git a/src/modules/frame/components/loginFrame.tsx b/src/modules/frame/components/loginFrame.tsx new file mode 100644 index 0000000..8e2f426 --- /dev/null +++ b/src/modules/frame/components/loginFrame.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +import "../../../Sass/login.scss"; + +import Logo from "../../../img/logo"; + +interface LoginFrameProps { + children?: JSX.Element +} + +interface LoginFrameState { + +} + +class LoginFrame extends React.Component { + + render() { + const { children } = this.props; + + return (
    +
    +
    +
    + +
    +
    + {children} +
    +
    +
    + +
    + +
    ); + } +} + +export default LoginFrame; \ No newline at end of file diff --git a/src/modules/frame/models/IEmailUserAction.ts b/src/modules/frame/models/IEmailUserAction.ts new file mode 100644 index 0000000..634984b --- /dev/null +++ b/src/modules/frame/models/IEmailUserAction.ts @@ -0,0 +1,12 @@ +export enum EmailActionType { + PasswordReset = 0, + DisableAuthenticator = 1, + ConfirmEmailAddress = 2, +} + +export interface IEmailUserAction { + email: string; + token: string; + password: string; + emailActionType: EmailActionType; +} diff --git a/src/modules/frame/models/JwtToken.ts b/src/modules/frame/models/JwtToken.ts new file mode 100644 index 0000000..3a8ba4e --- /dev/null +++ b/src/modules/frame/models/JwtToken.ts @@ -0,0 +1,8 @@ +export default interface JwtToken { + expiry: Date; + primarysid: bigint; + name: string; + email: string; + domainid: bigint; + securityPrivileges: []; +} diff --git a/src/modules/frame/services/authenticationService.ts b/src/modules/frame/services/authenticationService.ts new file mode 100644 index 0000000..4fdd099 --- /dev/null +++ b/src/modules/frame/services/authenticationService.ts @@ -0,0 +1,137 @@ +import jwt_decode from "jwt-decode"; +import Cookies from "js-cookie"; +import httpService from "../../../services/httpService"; +import { IEmailUserAction } from "../models/IEmailUserAction"; +import JwtToken from "../models/JwtToken"; + +const apiEndpoint = "/Authentication"; +//const tokenKey = "token"; + +export async function login(email: string, password: string, securityCode: string, requestTfaRemoval: boolean) { + const loginResponse = await httpService.post(apiEndpoint + "/login", { email, password, securityCode, requestTfaRemoval }); + + if (loginResponse?.status === 202) return 1; //TFA information needed, or TFA Removal request accepted. + + if (loginResponse?.status === 200) { + return 2; + } + + return 0; +} + +export function logout() { +} + +async function refreshToken() { + const currentUser = getCurrentUser(); + if (currentUser) { + const fiveMinutesFromNow: Date = new Date(Date.now() + 5 * 60 * 1000); + + if (currentUser.expiry < fiveMinutesFromNow) { + const refreshTokenRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? "/../account/refreshToken" : apiEndpoint + "/refreshToken"; + + const { status } = await httpService.get(refreshTokenRoute); + if (status === 200) { + } + } + } +} + +export function hasToken(): boolean{ + const jwt = getJwt(); + if (jwt) + return true; + + return false; +} + +export function tokenExpired(): boolean{ + const jwt = getJwt(); + if (jwt) { + const decodedToken: any = jwt_decode(jwt); + + const expiry: Date = new Date(decodedToken.exp * 1000); + const now: Date = new Date(Date.now()); + + if (expiry > now) { + return false; + } + }; + return true; +} + +export function getCurrentUser(): JwtToken | null { + try { + const jwt = getJwt(); + if (jwt) { + const decodedToken: any = jwt_decode(jwt); + + const expiry: Date = new Date(decodedToken.exp * 1000); + const now: Date = new Date(Date.now()); + + if (expiry < now) { + logout(); //The JWT Has expired, so there's no point keeping it laying around + return null; + } + + const jwtToken: JwtToken = { + expiry: expiry, + email: decodedToken.email, + name: decodedToken.unique_name, + primarysid: decodedToken.primarysid, + domainid: decodedToken.domainid, + securityPrivileges: JSON.parse(decodedToken.securityPrivileges) + }; + + return jwtToken; + } else return null; + } catch (ex) { + return null; + } +} + +function getJwt(): string | null { + const eSuiteSession = Cookies.get("eSuiteSession"); + if (eSuiteSession){ + return eSuiteSession; + } + return null; +} + +export async function forgotPassword(email: string) { + return await httpService.post(apiEndpoint + "/forgotPassword", { email }); +} + +async function completeEmailAction(action: IEmailUserAction) { + const response = await httpService.post(apiEndpoint + "/completeEmailAction", action); + + if (response?.status === 200) { + return 1; + } + + return 0; +} + +export function hasAccess(accessKey: string): boolean { + var currentUser = getCurrentUser()!; + + const result = currentUser?.securityPrivileges!.filter(x => x === accessKey); + if (result?.length > 0) + return true; + + return false; +} + +const authentication = { + login, + logout, + getCurrentUser, + refreshToken, + forgotPassword, + completeEmailAction, + hasToken, + tokenExpired, + hasAccess +}; + +export default authentication; diff --git a/src/modules/homepage/Env.tsx b/src/modules/homepage/Env.tsx new file mode 100644 index 0000000..da44ce3 --- /dev/null +++ b/src/modules/homepage/Env.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +function EnvPage() { + return ( + <> +

    This is the Environment

    +

    +

    window.__RUNTIME_CONFIG__.API_URL = {window.__RUNTIME_CONFIG__.API_URL}

    + + ); +} + +export default EnvPage; diff --git a/src/modules/homepage/HomePage.tsx b/src/modules/homepage/HomePage.tsx new file mode 100644 index 0000000..3e933c7 --- /dev/null +++ b/src/modules/homepage/HomePage.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; + +function HomePage() { + const redirect = ()=> { + window.location.href = '/organisations' + } + + return ( +
    +

    Applications

    +
    +
    +
    E-print
    +
    +
    +
    + ); +} + +export default HomePage; diff --git a/src/modules/manager/customfields/components/CustomFieldsTable.tsx b/src/modules/manager/customfields/components/CustomFieldsTable.tsx new file mode 100644 index 0000000..34848cb --- /dev/null +++ b/src/modules/manager/customfields/components/CustomFieldsTable.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { CustomField } from "../services/customFieldsService"; + +class CustomFieldsTable extends React.Component> { + columns : Column[] = [ + { key: "name", label: "Name", order: "asc" }, + { key: "fieldType", label: "Field Type", order: "asc", searchable: false } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if (this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.CustomFields.CustomField", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditField") ? "edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteField") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return
    ; + } +} + +export default CustomFieldsTable; \ No newline at end of file diff --git a/src/modules/manager/customfields/customFieldDetails.tsx b/src/modules/manager/customfields/customFieldDetails.tsx new file mode 100644 index 0000000..c859c5d --- /dev/null +++ b/src/modules/manager/customfields/customFieldDetails.tsx @@ -0,0 +1,359 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import customFieldsService, { numberParams, textParams } from "./services/customFieldsService"; +import Option from "../../../components/common/option"; +import { GeneralIdRef } from "./../../../utils/GeneralIdRef"; +import { Case, Else, Switch } from "../../frame/components/Switch"; +import { CustomFieldValue, SystemGlossaries } from "../glossary/services/glossaryService"; +import Loading from "../../../components/common/Loading"; + +interface CustomFieldDetailsState extends FormState { + data: { + name: string; + fieldType: string; + multiLine: boolean; + defaultValue: string; + minEntries: number; + maxEntries: string | number | undefined; + refElementId: CustomFieldValue[] | GeneralIdRef | undefined; + minValue: number | undefined; + maxValue: number | undefined; + step: number | undefined; + required: boolean; + }; + redirect: string; +} + +class CustomFieldDetails extends Form { + state: CustomFieldDetailsState = { + loaded: false, + data: { + name: "", + fieldType: "Text", + defaultValue: "", + multiLine: false, + minEntries: 0, + maxEntries: 1, + refElementId: undefined, + minValue: undefined, + maxValue: undefined, + step: undefined, + required: false, + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + labelFieldType = "Field Type"; + labelMultiLine = "Multi-line"; + labelDefaultValue = "Default Value"; + labelMinValue = "Minimum Value"; + labelMaxValue = "Maximum Value"; + labelStep = "Step"; + labelRequired = "Required"; + labelMinEntries = "Min Entries"; + labelMaxEntries = "Max Entries (empty=unlimited)"; + labelRefElementId = "Sequence/Form/Glossary"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + fieldType: Joi.string().required().label(this.labelFieldType), + multiLine: Joi.boolean().label(this.labelMultiLine), + minEntries: Joi.number().min(0).label(this.labelMinEntries), + maxEntries: Joi.number().empty("").label(this.labelMaxEntries), + refElementId: Joi.when("fieldType", { + is: Joi.string().valid("Sequence"), + then: Joi.object({ + id: Joi.optional(), + guid: Joi.optional(), + }).required(), + }).when("fieldType", { + is: Joi.string().valid("Glossary"), + then: Joi.array() + .min(1) + .items( + Joi.object({ + displayValue: Joi.string().optional(), + value: Joi.object({ + id: Joi.optional(), + guid: Joi.optional(), + }).required(), + }) + ) + .required(), + }), + minValue: Joi.number().allow("").label(this.labelMinValue), + maxValue: Joi.number().allow("").label(this.labelMaxValue), + step: Joi.number().optional().allow("").min(0).label(this.labelStep), + required: Joi.boolean().label(this.labelRequired), + + //defaultValue: Joi.string().allow("").label(this.labelDefaultValue) + + defaultValue: Joi.when("fieldType", { + is: Joi.string().valid("Number"), + then: Joi.when("minValue", { + is: Joi.any().valid(null, ""), + then: Joi.number(), + otherwise: Joi.number() + .min(Joi.ref("minValue")) + .message('"Default Value" must be greater than or equal to "' + this.labelMinValue + '"'), + }) + .when("maxValue", { + is: Joi.any().valid(null, ""), + then: Joi.number(), + otherwise: Joi.number() + .max(Joi.ref("maxValue")) + .message('"Default Value" must be less than or equal to "' + this.labelMaxValue + '"'), + }) + .allow(""), + otherwise: Joi.string().allow(""), + }).label(this.labelDefaultValue), + }; + + doSubmit = async (buttonName: string) => { + try { + const { name, fieldType } = this.state.data; + let { refElementId, defaultValue, minEntries, maxEntries, required } = this.state.data; + let numberParams: numberParams | undefined = undefined; + let textParams: textParams | undefined = undefined; + let params; + let refElementIdValue: GeneralIdRef | undefined; + + switch (fieldType) { + case "Sequence": + minEntries = 1; + maxEntries = 1; + defaultValue = ""; + refElementIdValue = refElementId as GeneralIdRef; + break; + case "FormTemplate": + minEntries = 1; + maxEntries = 1; + defaultValue = ""; + break; + case "Domain": + minEntries = required ? 1 : 0; + maxEntries = maxEntries === 0 ? undefined : maxEntries; + defaultValue = ""; + break; + case "Glossary": + minEntries = required ? 1 : 0; + maxEntries = maxEntries === 0 ? undefined : maxEntries; + defaultValue = ""; + refElementIdValue = (refElementId as CustomFieldValue[])[0].value as GeneralIdRef; + break; + case "Text": + minEntries = 1; + maxEntries = 1; + let { multiLine } = this.state.data; + textParams = { multiLine }; + params = textParams; + refElementIdValue = undefined; + break; + case "Number": + refElementIdValue = undefined; + let { minValue, maxValue, step } = this.state.data; + numberParams = { minValue, maxValue, step }; + params = numberParams; + minEntries = required ? 1 : 0; + maxEntries = 1; + break; + default: + refElementIdValue = undefined; + } + + const cleanMaxEntries: Number | undefined = maxEntries === "" ? undefined : Number(maxEntries); + + if (this.isEditMode()) { + const { customFieldId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(customFieldId); + const response = await customFieldsService.putField( + generalIdRef, + name, + fieldType, + defaultValue, + minEntries, + cleanMaxEntries, + refElementIdValue, + params + ); + if (response) { + toast.info("Custom Field edited"); + } + } else { + const response = await customFieldsService.postField(name, fieldType, defaultValue, minEntries, cleanMaxEntries, refElementIdValue, params); + if (response) { + toast.info("New Custom Field added"); + } + } + + if (buttonName === this.labelSave) this.setState({ redirect: "/customfields" }); + } catch (ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { customFieldId } = this.props.router.params; + + if (customFieldId !== undefined) { + try { + const loadedData = await customFieldsService.getField(customFieldId); + + const { data } = this.state; + if (loadedData) { + data.name = loadedData.name; + data.fieldType = loadedData.fieldType; + data.defaultValue = loadedData.defaultValue; + data.minEntries = loadedData.minEntries; + data.maxEntries = loadedData.maxEntries; + switch (data.fieldType) { + case "Glossary": + let convertedRefElementId: CustomFieldValue = { + value: loadedData.refElementId, + }; + + data.refElementId = [convertedRefElementId]; + data.required = loadedData.minEntries > 0; + break; + case "Sequence": + data.refElementId = loadedData.refElementId; + break; + case "Domain": + data.required = loadedData.minEntries > 0; + break; + } + + if (loadedData.parameters !== undefined) { + switch (data.fieldType) { + case "Number": + data.required = loadedData.minEntries > 0; + const parameters: numberParams = JSON.parse(loadedData.parameters); + data.minValue = parameters.minValue ?? undefined; + data.maxValue = parameters.maxValue ?? undefined; + data.step = parameters.step ?? undefined; + break; + case "Text": + const textParameters: textParams = JSON.parse(loadedData.parameters); + data.multiLine = textParameters.multiLine ?? false; + break; + } + } + + this.setState({ loaded: true, data }); + } else { + this.setState({ loaded: false }); + } + } catch (ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) this.setState({ loaded: true }); + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + const { fieldType, minValue, maxValue, step } = this.state.data; + + let mode = "Add"; + if (this.isEditMode()) mode = "Edit"; + + const fieldTypeOptions: Option[] = [ + { _id: "Text", name: "Text" }, + { _id: "Number", name: "Number" }, + // { _id: "Boolean", name: "Boolean" }, + // { _id: "Date", name: "Date" }, + // { _id: "Time", name: "Time" }, + // { _id: "DateTime", name: "DateTime" }, + { _id: "Sequence", name: "Sequence" }, + { _id: "FormTemplate", name: "Form Template" }, + { _id: "Glossary", name: "Glossary" }, + { _id: "Domain", name: "Domain" }, + ]; + + switch (fieldType) { + case "Sequence": + this.labelRefElementId = "Sequence"; + break; + case "FormTemplate": + this.labelRefElementId = "Form"; + break; + case "Glossary": + this.labelRefElementId = "Glossary"; + break; + } + + return ( + +

    {mode} Custom Field

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderSelect("fieldType", this.labelFieldType, fieldTypeOptions)} + + + {this.renderInput("required", this.labelRequired, InputType.checkbox)} + {this.renderInput("maxEntries", this.labelMaxEntries, InputType.number)} + + + {this.renderGlossaryPicker(true, "refElementId", this.labelRefElementId, 1, SystemGlossaries)} + {this.renderInput("required", this.labelRequired, InputType.checkbox)} + {this.renderInput("maxEntries", this.labelMaxEntries, InputType.number)} + + + {this.renderSequencePicker(true, "refElementId", this.labelRefElementId)} + + + <> + + + {this.renderInput("multiLine", this.labelMultiLine, InputType.checkbox)} + {this.renderInputTextarea(true, "defaultValue", this.labelDefaultValue)} + + {this.renderInput("defaultValue", this.labelDefaultValue, InputType.text)} + + + + {this.renderInput("required", this.labelRequired, InputType.checkbox)} + {this.renderInputNumber("minValue", this.labelMinValue, false, undefined, undefined, maxValue, undefined)} + {this.renderInputNumber("maxValue", this.labelMaxValue, false, undefined, minValue, undefined, undefined)} + {this.renderInput("step", this.labelStep, InputType.number)} + {this.renderInputNumber("defaultValue", this.labelDefaultValue, false, undefined, minValue, maxValue, step)} + + + {this.renderInput("defaultValue", this.labelDefaultValue, InputType.text)} + {this.renderInput("minEntries", this.labelMinEntries, InputType.number)} + {this.renderInput("maxEntries", this.labelMaxEntries, InputType.number)} + + + {this.isEditMode() && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCCustomFieldDetails = withRouter(CustomFieldDetails); + +export default HOCCustomFieldDetails; diff --git a/src/modules/manager/customfields/customFields.tsx b/src/modules/manager/customfields/customFields.tsx new file mode 100644 index 0000000..a471211 --- /dev/null +++ b/src/modules/manager/customfields/customFields.tsx @@ -0,0 +1,99 @@ +import React, { Component } from 'react'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import CustomFieldsTable from './components/CustomFieldsTable'; +import customFieldsService, { CustomField } from './services/customFieldsService'; +import Button, { ButtonType } from '../../../components/common/Button'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface CustomFieldsState { + loaded: boolean, + pagedData: Paginated, + sortColumn: Column, + filters: Map; +} + +class CustomFields extends Component { + state = { + loaded: false, + pagedData: { + page: 1, + pageSize: 10, + count: 0, + totalPages: 1, + data: [] + }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async (page: number, pageSize: number) => { + const { sortColumn, filters } = this.state; + + const pagedData = await customFieldsService.getFields(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async (sortColumn: Column) => { + const { page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await customFieldsService.getFields(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async (name: string, value: string) => { + const { page, pageSize } = this.state.pagedData; + const { sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await customFieldsService.getFields(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async (item?: CustomField) => { + const response = await customFieldsService.deleteField(item?.id, item?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +}; + +export default CustomFields; diff --git a/src/modules/manager/customfields/services/customFieldsService.ts b/src/modules/manager/customfields/services/customFieldsService.ts new file mode 100644 index 0000000..bb101e1 --- /dev/null +++ b/src/modules/manager/customfields/services/customFieldsService.ts @@ -0,0 +1,94 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/CustomFields"; + +export type CustomField = { + id : bigint; + name : string; + fieldType: string; + defaultValue: string; + minEntries : number; + maxEntries? : number; + refElementId? : GeneralIdRef; + parameters? : string; + guid? : string; +} + +export type numberParams = { + minValue?: number, + maxValue?: number, + step?: number +} + +export type textParams = { + multiLine: boolean +} + +export async function getFields( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/fields", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } }); + + return response?.data; +} + +export async function getField( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/field?" + params ); + + return response?.data; +} + + +export async function postField(name : string, fieldType : string, defaultValue : string, minEntries : number, maxEntries? : Number, refElementId? : GeneralIdRef, params? : numberParams | textParams): Promise{ + const paramsJson = JSON.stringify(params); + return await httpService.post(apiEndpoint + "/field", { + name, + fieldType, + defaultValue, + minEntries, + maxEntries, + refElementId, + params : paramsJson + }); +} + +export async function putField( id : GeneralIdRef, name : string, fieldType : string, defaultValue : string, minEntries : number, maxEntries : Number | undefined, refElementId? : GeneralIdRef, params? : numberParams | textParams): Promise{ + const paramsJson = JSON.stringify(params); + return await httpService.put(apiEndpoint + "/field", { + id, + name, + fieldType, + defaultValue, + minEntries, + maxEntries, + refElementId, + params : paramsJson + }); +} + +export async function deleteField( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/field", { data: generalIdRef }); +} + +const customFieldsService = { + getFields, + getField, + postField, + putField, + deleteField +}; + +export default customFieldsService; diff --git a/src/modules/manager/domains/Domains.tsx b/src/modules/manager/domains/Domains.tsx new file mode 100644 index 0000000..2d346b5 --- /dev/null +++ b/src/modules/manager/domains/Domains.tsx @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import DomainsTable from './components/domainsTable'; +import domainsService, { GetDomain } from './serrvices/domainsService'; +import Button, { ButtonType } from '../../../components/common/Button'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface DomainsState{ + loaded: boolean; + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class Domains extends Component< any, any, DomainsState> { + state = { + loaded : false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + + const pagedData = await domainsService.getDomains(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await domainsService.getDomains(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await domainsService.getDomains(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async ( keyValue? : GetDomain) => { + const response = await domainsService.deleteDomain( keyValue?.id, keyValue?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +}; + +export default Domains; diff --git a/src/modules/manager/domains/DomainsDetails.tsx b/src/modules/manager/domains/DomainsDetails.tsx new file mode 100644 index 0000000..4cdbe5d --- /dev/null +++ b/src/modules/manager/domains/DomainsDetails.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import HorizontalTabs from "../../../components/common/HorizionalTabs"; +import Tab from "../../../components/common/Tab"; +import authentication from "../../frame/services/authenticationService"; +import GeneralTab from "./components/GeneralTab"; +import MailTemplatesTab from "./components/MailTemplatesTab"; +import SecurityRolesTab from "./components/SecurityRolesTab"; + +interface DomainsDetailsProps { + editMode : boolean; +} + +class DomainsDetails extends React.Component { + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + render() { + const isEditMode = this.isEditMode(); + const canViewMailTemplates = authentication.hasAccess("ViewDomain") + const canViewSecurityRoles = authentication.hasAccess("ViewRole") + + let mode = "Add"; + if (isEditMode) mode = "Edit"; + + + let tabs : JSX.Element[] = []; + + tabs.push( + + ); + + if (isEditMode) { + if (canViewMailTemplates) { + tabs.push( + + + ); + } + if (canViewSecurityRoles) { + tabs.push( + + + ); + } + } + + return ( +
    +

    {mode} Domain

    + + {tabs} + +
    + ); + } +} + +export default DomainsDetails; diff --git a/src/modules/manager/domains/components/AddUserToRole.tsx b/src/modules/manager/domains/components/AddUserToRole.tsx new file mode 100644 index 0000000..8be8e0f --- /dev/null +++ b/src/modules/manager/domains/components/AddUserToRole.tsx @@ -0,0 +1,82 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form, { FormState } from "../../../../components/common/Form"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import roleService from "../serrvices/rolesService"; +import Loading from "../../../../components/common/Loading"; + +interface LocAddUserToRoleProps extends RouterProps { + isEditMode : boolean; +} + +interface LocAddUserToRoleState extends FormState { + data: { + userId?: GeneralIdRef; + }; + redirect: string; +} + +class LocAddUserToRole extends Form { + state: LocAddUserToRoleState = { + loaded: true, + data: { + //userId: null; + }, + errors: {}, + redirect: "", + }; + + labelUserId = "User"; + + labelApply = "Save"; + labelSave = "Save and close"; + + + schema = { + userId: Joi.any().required().label(this.labelUserId), + }; + + doSubmit = async (buttonName : string) => { + try { + const { userId } = this.state.data; + const { domainId, roleId } = this.props.router.params; + + const response = await roleService.postRoleUser(userId!, MakeGeneralIdRef(roleId)); + if (response) { + toast.info("User added to role"); + + if (buttonName === this.labelSave) + this.setState({ redirect: "/domains/edit/" + domainId }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + const { isEditMode } = this.props; + + return ( + +
    + {this.renderError("_general")} + {this.renderUserPicker("userId", this.labelUserId)} + + {isEditMode && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const AddUserToRole = withRouter(LocAddUserToRole); + +export default AddUserToRole; diff --git a/src/modules/manager/domains/components/EmailTemplateEditor.tsx b/src/modules/manager/domains/components/EmailTemplateEditor.tsx new file mode 100644 index 0000000..bcdc1d7 --- /dev/null +++ b/src/modules/manager/domains/components/EmailTemplateEditor.tsx @@ -0,0 +1,130 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form, { FormState } from "../../../../components/common/Form"; +import { InputType } from "../../../../components/common/Input"; +import { MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter from "../../../../utils/withRouter"; +import mailTemplatesService from "../serrvices/mailTemplatesService"; +import Loading from "../../../../components/common/Loading"; + +interface EmailTemplateEditorProps{ + domainId : number, + currentMailType : string +} + +interface EmailTemplateEditorState extends FormState { + data: { + currentMailType? : string + isOverridden : boolean + subject: string; + definition: string; + }; + redirect: string; +} + +class EmailTemplateEditor extends Form { + state: EmailTemplateEditorState = { + loaded : false, + data: { + currentMailType : undefined, + isOverridden : false, + subject: "", + definition: "", + }, + errors: {}, + redirect: "", + }; + + labelName = "Subject"; + labelDefinition = "Definition"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + currentMailType : Joi.optional(), + isOverridden : Joi.optional(), + subject: Joi.string().required().max(450).label(this.labelName), + definition: Joi.string().required().label(this.labelDefinition), + }; + + componentDidMount = async () => { + this.handleTemplateChange(); + }; + + componentDidUpdate = async (prevProps : EmailTemplateEditorProps) => { + if (prevProps.domainId !== this.props.domainId || prevProps.currentMailType !== this.props.currentMailType ) { + this.handleTemplateChange(); + } + } + + handleTemplateChange = async() => { + const { domainId, currentMailType } = this.props; + + try{ + var mailTemplate = await mailTemplatesService.getTemplate( MakeGeneralIdRef(domainId, undefined), currentMailType ) + + if (mailTemplate !== null) + { + this.setState({ + loaded: true, + data : { + currentMailType : currentMailType, + isOverridden : mailTemplate.isOverridden, + subject : mailTemplate.subject, + definition : mailTemplate.templateDefinition + }}); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + doSubmit = async (buttonName : string) => { + try { + const { domainId, currentMailType } = this.props; + const { subject, definition } = this.state.data; + + var domainGeneralIdRef = MakeGeneralIdRef(domainId); + const response = await mailTemplatesService.postTemplate(domainGeneralIdRef, currentMailType, subject, definition); + if (response) { + toast.info("Email Template saved"); + if (buttonName === this.labelSave) + this.setState({ redirect: "/domains" }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + render() { + const { loaded, redirect } = this.state; + const { currentMailType, isOverridden } = this.state.data; + if (redirect !== "") return ; + + + return ( + + {isOverridden &&

    This template is custom for this domain only

    } + {!isOverridden &&

    The details below are loaded from the master template. Saving this template will mean that any changes to the master template will not automatically appear here.

    } + +
    + {this.renderError("_general")} + {this.renderInput("subject", this.labelName, InputType.text)} + {this.renderTemplateEditor("mailTemplate-editor","definition", this.labelDefinition, false)} + + {this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCEmailTemplateEditor = withRouter(EmailTemplateEditor); + +export default HOCEmailTemplateEditor; diff --git a/src/modules/manager/domains/components/GeneralTab.tsx b/src/modules/manager/domains/components/GeneralTab.tsx new file mode 100644 index 0000000..3fb23b2 --- /dev/null +++ b/src/modules/manager/domains/components/GeneralTab.tsx @@ -0,0 +1,148 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form, { FormState } from "../../../../components/common/Form"; +import { InputType } from "../../../../components/common/Input"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import domainsService from "../serrvices/domainsService"; +import Loading from "../../../../components/common/Loading"; + +interface GeneralTabProps extends RouterProps { + isEditMode: boolean; +} + +interface GeneralTabState extends FormState { + data: { + name: string; + ssoProviderId: GeneralIdRef | null; + sunriseHostName: string; + sunriseAppId: string; + sunriseCategoryId: string; + sigmaId: bigint | null; + }; + redirect: string; +} + +class LocGeneralTab extends Form { + state: GeneralTabState = { + loaded: false, + data: { + name: "", + ssoProviderId: null, + sunriseHostName: "", + sunriseAppId: "", + sunriseCategoryId: "", + sigmaId: null, + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + labelSsoProvider = "Sso Provider"; + labelSunriseHostName = "e-flow hostname"; + labelSunriseAppId = "e-flow AppId"; + labelSunriseCategoryId = "e-flow CategoryId"; + labelSigmaId = "Sigma Id"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + ssoProviderId: Joi.object().allow(null).optional().label(this.labelSsoProvider), + sunriseHostName: Joi.string().required().allow("").max(16).label(this.labelSunriseHostName), + sunriseAppId: Joi.string().required().allow("").label(this.labelSunriseAppId), + sunriseCategoryId: Joi.string().required().allow("").label(this.labelSunriseCategoryId), + sigmaId: Joi.number().allow(null).optional().label(this.labelSigmaId), + }; + + doSubmit = async (buttonName: string) => { + try { + const { name, ssoProviderId, sunriseHostName, sunriseAppId, sunriseCategoryId, sigmaId } = this.state.data; + const { isEditMode } = this.props; + + if (isEditMode) { + const { domainId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(domainId); + const response = await domainsService.putDomain(generalIdRef, name, sunriseHostName, sunriseAppId, sunriseCategoryId, ssoProviderId, sigmaId); + if (response) { + toast.info("Domain edited"); + } + } else { + const response = await domainsService.postDomain(name, sunriseHostName, sunriseAppId, sunriseCategoryId, ssoProviderId, sigmaId); + if (response) { + toast.info("New Domain added"); + } + } + + if (buttonName === this.labelSave) this.setState({ redirect: "/domains" }); + } catch (ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { domainId } = this.props.router.params; + try { + if (domainId !== undefined) { + const loadedData = await domainsService.getDomain(domainId); + if (loadedData) { + const { data } = this.state; + + data.name = loadedData.name; + data.ssoProviderId = loadedData.ssoProviderId; + data.sunriseHostName = loadedData.sunriseHostName; + data.sunriseAppId = loadedData.sunriseAppId; + data.sunriseCategoryId = loadedData.sunriseCategoryId; + data.sigmaId = loadedData.sigmaId; + + this.setState({ loaded: true, data }); + } else { + this.setState({ loaded: false }); + } + } + } catch (ex: any) { + this.handleGeneralError(ex); + } + + if (!this.isEditMode()) this.setState({ loaded: true }); + }; + + render() { + const { loaded } = this.state; + const isEditMode = this.isEditMode(); + + const { redirect } = this.state; + if (redirect !== "") return ; + + return ( + +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderInput("sunriseHostName", this.labelSunriseHostName, InputType.text)} + {this.renderInput("sunriseAppId", this.labelSunriseAppId, InputType.text)} + {this.renderInput("sunriseCategoryId", this.labelSunriseCategoryId, InputType.text)} + {this.renderSsoProviderPicker("ssoProviderId", this.labelSsoProvider)} + {this.renderInput("sigmaId", this.labelSigmaId, InputType.number)} + + {isEditMode && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const GeneralTab = withRouter(LocGeneralTab); + +export default GeneralTab; diff --git a/src/modules/manager/domains/components/MailTemplatesTab.tsx b/src/modules/manager/domains/components/MailTemplatesTab.tsx new file mode 100644 index 0000000..c86f65b --- /dev/null +++ b/src/modules/manager/domains/components/MailTemplatesTab.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import mailTemplatesService from "../serrvices/mailTemplatesService"; +import HOCEmailTemplateEditor from "./EmailTemplateEditor"; +import Loading from "../../../../components/common/Loading"; + +interface MailType{ + mailType : string; + description : string; +} + +interface MailTemplatesTabState{ + loaded : boolean; + currentMailType : string; + types : MailType[] +} + +interface MailTemplatesTabProps extends RouterProps{ + +} + +class MailTemplatesTabNoRouter extends React.Component { + state : MailTemplatesTabState = { + loaded : false, + currentMailType : "", + types: [] + } + + async componentDidMount() { + const types = await mailTemplatesService.getTypes(0,10,"",true); + if (types) { + this.setState({ types: types.data as MailType[] }); + + if ((types.data as MailType[])?.length > 0) { + this.SelectTemplate((types.data as MailType[])[0].mailType); + } + } + } + + onClick = (e: React.MouseEvent) => { + const { target } = e; + + const value = (target as HTMLElement).getAttribute("value"); + + this.SelectTemplate(value!); + }; + + SelectTemplate = ( emailType : string) => { + const { currentMailType } = this.state; + + if (currentMailType !== emailType) + { + console.log("Selecting", emailType); + this.setState({ loaded: true, currentMailType : emailType}); + } + } + + render() { + const { domainId } = this.props.router.params; + const { loaded, types, currentMailType } = this.state; + + return ( + +
    +
    +
      + { types.map( (x) => { + return
    • {x.description}
    • + })} +
    +
    +
    + +
    +
    +
    + ); + } +} + +const MailTemplatesTab = withRouter(MailTemplatesTabNoRouter); + +export default MailTemplatesTab; diff --git a/src/modules/manager/domains/components/RoleAccessEditor.tsx b/src/modules/manager/domains/components/RoleAccessEditor.tsx new file mode 100644 index 0000000..a5b97e9 --- /dev/null +++ b/src/modules/manager/domains/components/RoleAccessEditor.tsx @@ -0,0 +1,166 @@ +import React from "react"; +import { toast } from "react-toastify"; +import Column from "../../../../components/common/columns"; +import { Paginated } from "../../../../services/Paginated"; +import { MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import roleService, { GetRoleSecurityAccess, GetSecurityAccess } from "../serrvices/rolesService"; +import RollAccessTable from "./RollAccessTable"; +import Loading from "../../../../components/common/Loading"; + +interface RoleAccessEditorState{ + loaded: boolean, + accessList : Paginated, + accessRightsForRole : Paginated, + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +interface RoleAccessEditorProps extends RouterProps{ + role : any | undefined +} + +class RoleAccessEditorNoRouter extends React.Component { + state : RoleAccessEditorState = { + loaded : false, + accessList : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + accessRightsForRole : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + await this.updatePage(); + } + + async componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { + if (prevProps.role?.id !== this.props.role?.id) { + await this.updatePage(); + } + } + + updatePage = async() => + { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) => { + let { accessList } = this.state; + const { role } = this.props; + + if (accessList?.count === 0) + { + accessList = await roleService.getAccessList(0, 10, "name", true ); + this.setState({ accessList }); + } + + const roleAccessfilters = new Map(); + roleAccessfilters.set( "roleId", String(role?.id)); + const accessRightsForRole = await roleService.getRoleAccess(0,10,"name",true, roleAccessfilters); + if (accessRightsForRole) { + const pagedData = this.complieAccessRightsPagedData(accessList?.data, accessRightsForRole?.data); + if (pagedData) { + this.setState({ loaded: true, accessRightsForRole, pagedData }); + } + } + else { + this.setState({ loaded: false }); + } + } + + IsItemSelected = (securityAccess: string, roleAccess: unknown[] | undefined) : boolean => { + if (roleAccess === undefined) + return false; + + const filtered = roleAccess?.filter( (x) => (x as any).securityAccess === securityAccess); + + return filtered!.length > 0; + } + + complieAccessRightsPagedData = (masterList: GetSecurityAccess[] | undefined, roleAccess: unknown[] | undefined) : Paginated => { + if ( masterList === undefined ) { + return { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] } + } + + if (roleAccess === undefined) { + return { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: (masterList as GetRoleSecurityAccess[]) } + } + + const accessrightsForRole : Paginated = { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] + } + + accessrightsForRole.data = masterList.map( value => { + let item : any = value + item.selected = this.IsItemSelected(item.securityAccess, roleAccess); + return item + }) + + return accessrightsForRole; + } + + handleSave = async ( additions : string[], deletions : string[] ) => { + const { role } = this.props; + + const roleGeneralIdRef = MakeGeneralIdRef(role?.id, role?.guid); + let response = undefined; + if (additions.length > 0) + { + response = await roleService.addRoleAccess( roleGeneralIdRef, additions); + } + + if (deletions.length > 0) + { + response = await roleService.deleteRoleAccess( roleGeneralIdRef, deletions); + } + if (response) { + toast.info("Role access updated successfully.") + await this.updatePage(); + } + } + + render() { + const { loaded, pagedData } = this.state; + + const { role } = this.props; + const isAdministrator = role.isAdministrator || role.isSuperUser; + + return ( + + + + ); + } +} + +const RoleAccessEditor = withRouter(RoleAccessEditorNoRouter); + +export default RoleAccessEditor; + diff --git a/src/modules/manager/domains/components/RolesDetails.tsx b/src/modules/manager/domains/components/RolesDetails.tsx new file mode 100644 index 0000000..d9a633d --- /dev/null +++ b/src/modules/manager/domains/components/RolesDetails.tsx @@ -0,0 +1,129 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form, { FormState } from "../../../../components/common/Form"; +import { InputType } from "../../../../components/common/Input"; +import { MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import roleService from "../serrvices/rolesService"; +import Loading from "../../../../components/common/Loading"; + +interface RolesDetailsProps extends RouterProps { + isEditMode : boolean; +} + +interface RolesDetailsState extends FormState { + data: { + name: string; + }; + redirect: string; +} + +class LocRolesDetails extends Form { + state: RolesDetailsState = { + loaded: false, + data: { + name: "" + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + + labelApply = "Save"; + labelSave = "Save and close"; + + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + }; + + doSubmit = async (buttonName : string) => { + try { + const { name } = this.state.data; + + const { domainId } = this.props.router.params; + var domainIdGeneralIdRef = MakeGeneralIdRef(domainId); + + if (this.isEditMode()) { + const { roleId } = this.props.router.params; + var generalIdRef = MakeGeneralIdRef(roleId); + const response = await roleService.putRole(generalIdRef, name); + if (response) { + toast.info("Role edited"); + } + } else { + const response = await roleService.postRole(domainIdGeneralIdRef, name); + if (response) { + toast.info("New Role added"); + } + } + + if (buttonName === this.labelSave) + this.setState({ redirect: "/domains/edit/" + domainId }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { roleId } = this.props.router.params; + + if (roleId !== undefined) { + try { + const loadedData = await roleService.getRole(roleId); + + const { data } = this.state; + if (loadedData) { + data.name = loadedData.name; + + this.setState({ loaded: true, data }); + } + else { + this.setState({ loaded: false }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + else { + this.setState({ loaded : true }); + } + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + const isEditMode = this.isEditMode(); + + let mode = "Add"; + if (isEditMode) mode = "Edit"; + + return ( + +

    {mode} Role

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + + {isEditMode && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const RolesDetails = withRouter(LocRolesDetails); + +export default RolesDetails; diff --git a/src/modules/manager/domains/components/RolesEditor.tsx b/src/modules/manager/domains/components/RolesEditor.tsx new file mode 100644 index 0000000..b1b659b --- /dev/null +++ b/src/modules/manager/domains/components/RolesEditor.tsx @@ -0,0 +1,141 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import { Paginated } from "../../../../services/Paginated"; +import { MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import roleService, { GetRoleResponse } from "../serrvices/rolesService"; +import RolesTable from "./RolesTable"; +import Button, { ButtonType } from "../../../../components/common/Button"; +import Loading from "../../../../components/common/Loading"; +import Permission from "../../../../components/common/Permission"; + +interface RolesEditorState{ + loaded : boolean, + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +interface RolesEditorProps extends RouterProps{ + selectedRole? : GetRoleResponse; + onSelectRole?: (keyValue: any) => void; + onUnselectRole?: () => void; +} + +class RolesEditorTabNoRouter extends React.Component { + state : RolesEditorState = { + loaded : false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + const { domainId } = this.props.router.params; + + const domainGeneralIdRef = MakeGeneralIdRef( domainId); + + const pagedData = await roleService.getRoles(page, pageSize, sortColumn.key, sortColumn.order === "asc", domainGeneralIdRef, filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const { domainId } = this.props.router.params; + + const domainGeneralIdRef = MakeGeneralIdRef( domainId); + const pagedData = await roleService.getRoles(page, pageSize, sortColumn.key, sortColumn.order === "asc", domainGeneralIdRef, filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + const { domainId } = this.props.router.params; + const domainGeneralIdRef = MakeGeneralIdRef( domainId); + + filters.set(name, value); + + const pagedData = await roleService.getRoles(page, pageSize, sortColumn.key, sortColumn.order === "asc", domainGeneralIdRef, filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + canEdit = ( keyValue : any) => { + return keyValue.canDelete; + } + + canDelete = ( keyValue : any) => { + return keyValue.canDelete; + } + + onDelete = async ( keyValue? : GetRoleResponse) => { + const response = await roleService.deleteRole( keyValue!.id, keyValue!.guid); + if (response) { + this.componentDidMount(); + } + } + + onSelectRow = ( id : GetRoleResponse ) => + { + const { onSelectRole } = this.props; + + if (onSelectRole !== undefined){ + onSelectRole(id); + } + } + + onUnselectRole = () => { + const { onUnselectRole } = this.props; + + if (onUnselectRole !== undefined) { + onUnselectRole(); + } + } + + render() { + const { loaded, pagedData, sortColumn } = this.state; + + const { selectedRole, onUnselectRole } = this.props; + + return ( + + + + +
    + +
    + ); + } +} + +const RolesEditor = withRouter(RolesEditorTabNoRouter); + +export default RolesEditor; diff --git a/src/modules/manager/domains/components/RolesTable.tsx b/src/modules/manager/domains/components/RolesTable.tsx new file mode 100644 index 0000000..d76319c --- /dev/null +++ b/src/modules/manager/domains/components/RolesTable.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { GetRoleResponse } from "../serrvices/rolesService"; + +class RolesTable extends React.Component> { + columns : Column[] = [ + { key: "name", label: "Name", order: "asc" } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if ( this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Domain.Role", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { data, sortColumn, selectedRow, onChangePage, onSearch, onSelectRow, canEdit, canDelete, onDelete, onUnselectRow } = this.props; + const editPath = authentication.hasAccess("EditRole") ? "editRole/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteRole") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return
    ; + } +} + +export default RolesTable; \ No newline at end of file diff --git a/src/modules/manager/domains/components/RollAccessTable.tsx b/src/modules/manager/domains/components/RollAccessTable.tsx new file mode 100644 index 0000000..4e1bf1d --- /dev/null +++ b/src/modules/manager/domains/components/RollAccessTable.tsx @@ -0,0 +1,150 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import Button, { ButtonType } from "../../../../components/common/Button"; +import { GetSecurityAccess } from "../serrvices/rolesService"; +import authentication from "../../../frame/services/authenticationService"; + +interface RollAccessTableProps extends PublishedTableProps{ + role : any; + isAdministrator : boolean; + onSave: ( additions : string[], deletions : string[] ) => void; +} + +interface RollAccessTableState{ + deltaAdditions : string[]; + deltaDeletions : string[]; +} + +class RollAccessTable extends React.Component { + + state : RollAccessTableState = { + deltaAdditions : [], + deltaDeletions : [] + }; + + ResetState = () => { + this.setState( { + deltaAdditions : [], + deltaDeletions : [] + } ); + } + + componentDidMount = async () => { + this.ResetState(); + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + if (prevProps.role !== this.props.role) { + this.ResetState(); + } + } + + checkboxChange = (e : any) => { + const accessKey = e.target.id; + const newSelectedState = e.target.checked; + let { deltaAdditions, deltaDeletions } = this.state; + + const { data } = this.props; + const filteredData = data.data?.filter( (x: any) => x.securityAccess === accessKey ); + + if (filteredData?.length === 1) + { + const item : any = filteredData[0]; + const oldSelectedState = item.selected; + + if (oldSelectedState===true) { + if (newSelectedState===true){ + deltaDeletions = deltaDeletions.filter( x => x !== accessKey); + } + else { + deltaDeletions.push(accessKey); + } + } + else { + if (newSelectedState===true){ + deltaAdditions.push(accessKey); + } + else { + deltaAdditions = deltaAdditions.filter( x => x !== accessKey); + } + } + } + + this.setState( { deltaAdditions, deltaDeletions }) + } + + itemIsSelected = (item : any) => { + const { deltaAdditions, deltaDeletions } = this.state; + const { isAdministrator } = this.props; + if (isAdministrator) + return true; + + let selected = item.selected; + + if (selected === true) { + selected = !(deltaDeletions.filter( x => x === item.securityAccess).length > 0); + } + else { + selected = (deltaAdditions.filter( x => x === item.securityAccess).length > 0); + } + + return selected; + } + + columns : Column[] = [ + { key: "selected", label: "Allowed", order: "asc", content:(item : any) => { + return <> } }, //todo refactor this to use the normal forms code? + { key: "name", label: "Name", order: "asc" }, + { key: "groupName", label: "Group", order: "asc" }, + { key: "description", label: "Description", order: "asc" } + ]; + + raiseSort = (sortColumn : Column) => { + if ( this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Domain.UserRole", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + HandleClickCancel = () => { + this.setState( { deltaAdditions : [], deltaDeletions : [] }) + } + + HandleClickSave = () => { + const { onSave } = this.props; + const { deltaAdditions, deltaDeletions } = this.state; + + onSave(deltaAdditions, deltaDeletions); + + this.setState( { deltaAdditions : [], deltaDeletions : [] }) + } + + render() { + const { deltaAdditions, deltaDeletions } = this.state; + const { data, isAdministrator } = this.props; + const canEditRoleAccess = authentication.hasAccess("EditRoleAccess"); + const canChangeRoleAccess = isAdministrator || canEditRoleAccess; + + const hasChanges = !(deltaAdditions.length + deltaDeletions.length === 0); + + return <> +
    + {canChangeRoleAccess && <> + + + } + ; + } +} + +export default RollAccessTable; \ No newline at end of file diff --git a/src/modules/manager/domains/components/SecurityRolesTab.tsx b/src/modules/manager/domains/components/SecurityRolesTab.tsx new file mode 100644 index 0000000..c9b60c5 --- /dev/null +++ b/src/modules/manager/domains/components/SecurityRolesTab.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import HorizontalTabs from "../../../../components/common/HorizionalTabs"; +import Tab from "../../../../components/common/Tab"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import RoleAccessEditor from "./RoleAccessEditor"; +import RolesEditor from "./RolesEditor"; +import UserRoleEditor from "./UserRoleEditor"; +import { GetRoleResponse } from "../serrvices/rolesService"; +import authentication from "../../../frame/services/authenticationService"; + +interface SecurityRolesTabState{ + selectedRole? : GetRoleResponse +} + +interface SecurityRolesTabProps extends RouterProps{ + +} + +class SecurityRolesTabNoRouter extends React.Component { + state : SecurityRolesTabState = { + selectedRole: undefined + } + + async componentDidMount() { + } + + onSelectRow = ( id : any ) => + { + this.setState({selectedRole:id}); + } + + onUnselectRow =() => { + this.setState({ selectedRole: undefined }); + } + + render() { + const { selectedRole } = this.state; + const canViewRoleAccess = authentication.hasAccess("ViewRoleAccess") + const canViewRoleUsers = authentication.hasAccess("ViewRoleUsers") + + let tabs : JSX.Element[] = []; + + if (canViewRoleAccess) { + tabs.push( + + ); + } + if (canViewRoleUsers) { + tabs.push( + + ); + } + + return ( +
    +
    + +
    +
    + {((selectedRole !== undefined) && (canViewRoleAccess || canViewRoleUsers)) && + {tabs} + + } +
    +
    + + ); + } +} + +const SecurityRolesTab = withRouter(SecurityRolesTabNoRouter); + +export default SecurityRolesTab; diff --git a/src/modules/manager/domains/components/UserRoleEditor.tsx b/src/modules/manager/domains/components/UserRoleEditor.tsx new file mode 100644 index 0000000..9b44b31 --- /dev/null +++ b/src/modules/manager/domains/components/UserRoleEditor.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import { Paginated } from "../../../../services/Paginated"; +import { MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import UserRolesTable from "./UserRolesTable"; +import roleService, { RoleUser } from "../serrvices/rolesService"; +import Button, { ButtonType } from "../../../../components/common/Button"; +import Loading from "../../../../components/common/Loading"; +import Permission from "../../../../components/common/Permission"; + +interface UserRoleEditorState{ + loaded: boolean, + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +interface UserRoleEditorProps extends RouterProps{ + role : any | undefined +} + +class UserRoleEditorNoRouter extends React.Component { + state : UserRoleEditorState = { + loaded : false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + async componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { + if (prevProps.role?.id !== this.props.role?.id) { + this.componentDidMount(); + } + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + + await this.getRoleUsers(page, pageSize, sortColumn, filters); + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + + await this.getRoleUsers(page, pageSize, sortColumn, filters); + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + filters.set(name, value); + + await this.getRoleUsers(page, pageSize, sortColumn, filters); + }; + + getRoleUsers = async ( page : number, pageSize : number, sortColumn : Column, filters : Map) => { + + const { role } = this.props; + const roleGeneralIdRef = MakeGeneralIdRef( role?.id, role?.guid); + const pagedData = await roleService.getRoleUsers(page, pageSize, sortColumn.key, sortColumn.order === "asc", roleGeneralIdRef, filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onDelete = async ( user? : RoleUser) => { + const { role } = this.props; + + const roleId = MakeGeneralIdRef(role?.id, role?.guid); + const userId = MakeGeneralIdRef(user?.id, user?.guid); + + const response = await roleService.deleteRoleUser(userId, roleId); + if (response) { + this.componentDidMount(); + } + } + + render() { + const {loaded, pagedData, sortColumn } = this.state; + + const { role } = this.props; + + const addLink = "editRole/" + role.id + "/addUserToRole"; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +} + +const UserRoleEditor = withRouter(UserRoleEditorNoRouter); + +export default UserRoleEditor; diff --git a/src/modules/manager/domains/components/UserRolesTable.tsx b/src/modules/manager/domains/components/UserRolesTable.tsx new file mode 100644 index 0000000..9a52a65 --- /dev/null +++ b/src/modules/manager/domains/components/UserRolesTable.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { RoleUser } from "../serrvices/rolesService"; + +class UserRolesTable extends React.Component> { + columns : Column[] = [ + { key: "displayName", label: "Name", order: "asc" } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if ( this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Domain.UserRole", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { data, sortColumn, selectedRow, onChangePage, onSearch, onSelectRow, onDelete } = this.props; + const doDelete = authentication.hasAccess("DeleteRoleUser") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return
    ; + } +} + +export default UserRolesTable; \ No newline at end of file diff --git a/src/modules/manager/domains/components/domainsTable.tsx b/src/modules/manager/domains/components/domainsTable.tsx new file mode 100644 index 0000000..32dc6d5 --- /dev/null +++ b/src/modules/manager/domains/components/domainsTable.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { GetDomain } from "../serrvices/domainsService"; + +class DomainsTable extends React.Component> { + columns : Column[] = [ + { key: "name", label: "Name", order: "asc" } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if ( this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Domain.Domain", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditDomain") ? "edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteDomain") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return
    ; + } +} + +export default DomainsTable; \ No newline at end of file diff --git a/src/modules/manager/domains/serrvices/domainsService.ts b/src/modules/manager/domains/serrvices/domainsService.ts new file mode 100644 index 0000000..7454565 --- /dev/null +++ b/src/modules/manager/domains/serrvices/domainsService.ts @@ -0,0 +1,100 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/Domain"; + +export type GetDomain = { + id: bigint; + guid: string; + name: string; + sunriseHostName: string; + sunriseAppId: string; + sunriseCategoryId: string; + ssoProviderId: GeneralIdRef | null; + sigmaId: bigint | null; +}; + +export async function getDomains( + page: number, + pageSize: number, + sortKey: string, + sortAscending: boolean, + filters?: Map | undefined +): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/domains", { + params: { + page: page, + pageSize: pageSize, + sortKey: sortKey, + sortAscending: sortAscending, + filters: filterString, + }, + }); + + return response?.data; +} + +export async function getDomain(id?: bigint, guid?: string) { + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/domain?" + params); + + return response?.data; +} + +export async function postDomain( + name: string, + sunriseHostName: string, + sunriseAppId: string, + sunriseCategoryId: string, + ssoProviderId: GeneralIdRef | null, + sigmaId: bigint | null +): Promise { + return await httpService.post(apiEndpoint + "/domain", { + name, + ssoProviderId, + sunriseHostName, + sunriseAppId, + sunriseCategoryId, + sigmaId, + }); +} + +export async function putDomain( + id: GeneralIdRef, + name: string, + sunriseHostName: string, + sunriseAppId: string, + sunriseCategoryId: string, + ssoProviderId: GeneralIdRef | null, + sigmaId: bigint | null +): Promise { + return await httpService.put(apiEndpoint + "/domain", { + id, + name, + ssoProviderId, + sunriseHostName, + sunriseAppId, + sunriseCategoryId, + sigmaId, + }); +} + +export async function deleteDomain(id?: bigint, guid?: string): Promise { + const generalIdRef = MakeGeneralIdRef(id, guid); + + return await httpService.delete(apiEndpoint + "/domain", { data: generalIdRef }); +} + +const domainsService = { + getDomains, + getDomain, + postDomain, + putDomain, + deleteDomain, +}; + +export default domainsService; diff --git a/src/modules/manager/domains/serrvices/mailTemplatesService.ts b/src/modules/manager/domains/serrvices/mailTemplatesService.ts new file mode 100644 index 0000000..8f515c8 --- /dev/null +++ b/src/modules/manager/domains/serrvices/mailTemplatesService.ts @@ -0,0 +1,69 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/MailTemplates"; + +export interface MailTemplateType +{ + mailType : string; + description? : string; +} + +export interface GetMailTemplate{ + isOverridden : boolean, + subject : string, + templateDefinition : string +} + +export type GetFormResponse = { + id : bigint; + guid : string, + name : string; + definition : string; + version : number; +} + +export async function getTypes( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/types", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function getTemplate( domain : GeneralIdRef, mailType : string ): Promise { + const response = await httpService.get(apiEndpoint + "/template", { params: { + Id:domain.id, + Guid: domain.guid, + mailType : mailType + } } ); + + return response?.data; +} + +export async function postTemplate( domainGeneralIdRef : GeneralIdRef, mailType : string, subject : string, definition : string ) { + + const response = await httpService.post(apiEndpoint + "/template", { + domain: domainGeneralIdRef, + mailType: mailType, + subject: subject, + templateDefinition: definition + } ); + return response; +} + +const mailTemplatesService = { + getTypes, + getTemplate, + postTemplate +}; + +export default mailTemplatesService; diff --git a/src/modules/manager/domains/serrvices/rolesService.ts b/src/modules/manager/domains/serrvices/rolesService.ts new file mode 100644 index 0000000..a3d72c8 --- /dev/null +++ b/src/modules/manager/domains/serrvices/rolesService.ts @@ -0,0 +1,188 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/Role"; + +export interface GetRoleResponse { + id : bigint; + guid : string, + name : string; + definition : string; + version : number; +} + +export interface RoleUser +{ + id : bigint; + guid : string; + displayName : string; +} + +export interface GetSecurityAccess +{ + securityAccess : string; + name : string; + description: string; + groupName : string; +} + +export interface GetRoleSecurityAccess extends GetSecurityAccess +{ + RoleId : GeneralIdRef +} + +export async function getRoles( page : number, pageSize : number, sortKey : string, sortAscending : boolean, domainGeneralIdRef : GeneralIdRef, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + + const response = await httpService.get>(apiEndpoint + "/roles", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString, + id: domainGeneralIdRef.id, + guid: domainGeneralIdRef.guid + } } ); + + return response?.data; +} + +export async function getRole( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/role?" + params ); + + return response?.data; +} + +export async function postRole(id: GeneralIdRef, name : string): Promise{ + return await httpService.post(apiEndpoint + "/role", { + name, + isSuperUser: false, + isAdministrator: false, + canDelete: true, + domainId: id + }); +} + +export async function putRole( generalIdRef : GeneralIdRef, name : string): Promise{ + return await httpService.put(apiEndpoint + "/role", { + generalIdRef, + name + }); +} + +export async function deleteRole( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/role", { data: generalIdRef }); +} + + +export async function getRoleUsers( page : number, pageSize : number, sortKey : string, sortAscending : boolean, roleGeneralIdRef : GeneralIdRef, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + + // var params = ""; + + // if (roleGeneralIdRef.id !== undefined) + // { + // params += "&Id=" + roleGeneralIdRef.id; + // } + + // if (roleGeneralIdRef.guid !== undefined) + // { + // params += "&Guid=" + roleGeneralIdRef.guid; + // } + + const response = await httpService.get>(apiEndpoint + "/roleUsers", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function postRoleUser(userId : GeneralIdRef, roleId: GeneralIdRef): Promise{ + return await httpService.post(apiEndpoint + "/roleUsers", { + userId, + roleId + }); +} + +export async function deleteRoleUser( userId : GeneralIdRef, roleId: GeneralIdRef): Promise{ + return await httpService.delete(apiEndpoint + "/roleUsers", { data : { + userId, + roleId + } + }); +} + +export async function getAccessList( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + + const response = await httpService.get>(apiEndpoint + "/accessList", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function getRoleAccess( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + + const response = await httpService.get>(apiEndpoint + "/roleAccess", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function addRoleAccess( roleId: GeneralIdRef, additions : string[]): Promise{ + return await httpService.post(apiEndpoint + "/roleAccess", { + roleId, + securityAccess: additions + } + ); +} + +export async function deleteRoleAccess( roleId: GeneralIdRef, deletions : string[]): Promise{ + return await httpService.delete(apiEndpoint + "/roleAccess", { data : { + roleId, + securityAccess: deletions + } + }); +} + +const roleService = { + getRoles, + getRole, + postRole, + putRole, + deleteRole, + getRoleUsers, + postRoleUser, + deleteRoleUser, + getAccessList, + getRoleAccess, + addRoleAccess, + deleteRoleAccess +}; + +export default roleService; diff --git a/src/modules/manager/forms/Forms.tsx b/src/modules/manager/forms/Forms.tsx new file mode 100644 index 0000000..77c2aa1 --- /dev/null +++ b/src/modules/manager/forms/Forms.tsx @@ -0,0 +1,97 @@ +import { Component } from 'react'; +import Button, { ButtonType } from '../../../components/common/Button'; +import Column from '../../../components/common/columns'; +import Permission from '../../../components/common/Permission'; +import { Paginated } from '../../../services/Paginated'; +import FormsTable from './components/formsTable'; +import formsService, { GetFormResponse } from './services/formsService'; +import Loading from '../../../components/common/Loading'; + +interface FormsState{ + loaded: boolean; + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class Forms extends Component< any, any, FormsState> { + state = { + loaded: false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + + const pagedData = await formsService.getForms(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await formsService.getForms(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await formsService.getForms(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async ( item? : GetFormResponse) => { + const response = await formsService.deleteForm( item?.id, item?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +} + +export default Forms; diff --git a/src/modules/manager/forms/FormsDetails.tsx b/src/modules/manager/forms/FormsDetails.tsx new file mode 100644 index 0000000..15ca4f2 --- /dev/null +++ b/src/modules/manager/forms/FormsDetails.tsx @@ -0,0 +1,141 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import formsService from "./services/formsService"; +import Loading from "../../../components/common/Loading"; +// import Tab from "../../../components/common/Tab"; +// import HorizontalTabs from "../../../components/common/HorizionalTabs"; + +interface FormsDetailsState extends FormState { + data: { + name: string; + definition: string; + }; + redirect: string; +} + +class FormsDetails extends Form { + state: FormsDetailsState = { + loaded: false, + data: { + name: "", + definition: "", + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + labelDefinition = "Definition"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + definition: Joi.string().required().label(this.labelDefinition), + }; + + doSubmit = async (buttonName : string) => { + try { + const { name, definition } = this.state.data; + + if (this.isEditMode()) { + const { formId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(formId); + const response = await formsService.putForm(generalIdRef, name, definition); + if (response) { + toast.info("Form template edited"); + } + } else { + const response = await formsService.postForm(name, definition); + if (response) { + toast.info("New Form Template added"); + } + } + + if (buttonName === this.labelSave) + this.setState({ redirect: "/forms" }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { formId } = this.props.router.params; + + if (formId !== undefined) { + try { + const loadedData = await formsService.getForm(formId); + + const { data } = this.state; + if (loadedData) { + data.name = loadedData.name; + data.definition = loadedData.definition; + + this.setState({ loaded: true, data }); + } + else { + this.setState({ loaded: false }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) + this.setState({ loaded: true } ); + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + let mode = "Add"; + if (this.isEditMode()) mode = "Edit"; + + // let tabs : JSX.Element[] = []; + + // tabs.push( + // {this.renderTemplateEditor("definition", this.labelDefinition, true)} + // ); + + // tabs.push( + // <>This is Wizard Mode + // ); + + return ( + +

    {mode} Form Template

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {/* + {tabs} + */} + {this.renderTemplateEditor("form-editor","definition", this.labelDefinition, true)} + {this.isEditMode() && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCFormsDetails = withRouter(FormsDetails); + +export default HOCFormsDetails; diff --git a/src/modules/manager/forms/components/formsTable.tsx b/src/modules/manager/forms/components/formsTable.tsx new file mode 100644 index 0000000..1355885 --- /dev/null +++ b/src/modules/manager/forms/components/formsTable.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { GetFormResponse } from "../services/formsService"; + +class FormsTable extends React.Component> { + columns : Column[] = [ + { key: "name", label: "Name", order: "asc" }, + { key: "version", label: "Version", order: "asc" } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if ( this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Forms.FormTemplate", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditFormTemplate") ? "edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteFormTemplate") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return
    ; + } +} + +export default FormsTable; \ No newline at end of file diff --git a/src/modules/manager/forms/services/formsService.ts b/src/modules/manager/forms/services/formsService.ts new file mode 100644 index 0000000..811d000 --- /dev/null +++ b/src/modules/manager/forms/services/formsService.ts @@ -0,0 +1,118 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; +import { CustomField } from "../../customfields/services/customFieldsService"; +import { CustomFieldValues } from "../../glossary/services/glossaryService"; + +const apiEndpoint = "/Forms"; + +export interface GetFormResponse { + id : bigint; + guid : string, + name : string; + definition : string; + version : bigint; + customFieldDefinitions : CustomField[]; +} + +export interface CreateFormInstance{ + templateId : GeneralIdRef, + version : bigint, + customFieldValues : CustomFieldValues[] +} + +export interface EditFormInstance{ + formInstanceId : GeneralIdRef, + templateId : GeneralIdRef, + version : bigint, + customFieldValues : CustomFieldValues[] +} + +export interface ReadFormInstance +{ + id : bigint; + guid : string, + name : string; + definition : string; + version : bigint; + templateId : GeneralIdRef; + customFieldDefinitions : CustomField[]; + customFieldValues : CustomFieldValues[]; + updatedVersion : bigint; +} + +export async function getForms( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/forms", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function getForm( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/form?" + params ); + + return response?.data; +} + +export async function postForm(name : string, definition : string): Promise{ + return await httpService.post(apiEndpoint + "/form", { + name, + definition + }); +} + +export async function putForm( id : GeneralIdRef, name : string, definition : string): Promise{ + return await httpService.put(apiEndpoint + "/form", { + id, + name, + definition + }); +} + +export async function deleteForm( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/form", { data: generalIdRef }); +} + + +export async function getFormInstance( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/formInstance?" + params ); + + return response?.data; +} + +export async function postFormInstance(formInstance : CreateFormInstance): Promise{ + var response = await httpService.post(apiEndpoint + "/formInstance", formInstance); + + return response?.data; +} + +export async function putFormInstance(editFormInstance : EditFormInstance): Promise { + return await httpService.put(apiEndpoint + "/formInstance", editFormInstance ); +} + +const formsService = { + getForms, + getForm, + postForm, + putForm, + deleteForm, + getFormInstance, + postFormInstance, + putFormInstance +}; + +export default formsService; diff --git a/src/modules/manager/glossary/GlossariesDetails.tsx b/src/modules/manager/glossary/GlossariesDetails.tsx new file mode 100644 index 0000000..48a980b --- /dev/null +++ b/src/modules/manager/glossary/GlossariesDetails.tsx @@ -0,0 +1,177 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import glossariesService, { Glossary, SystemGlossaries } from "./services/glossaryService"; +import { CustomField } from "../customfields/services/customFieldsService"; +import Loading from "../../../components/common/Loading"; + +interface GlossariesState extends FormState { + data: { + id? : bigint, + guid? : string, + name: string, + parent?: Glossary, + childCustomFieldDefinition : CustomField[] + }; + redirect: string; +} + +class GlossariesDetails extends Form { + state: GlossariesState = { + loaded: false, + data: { + id: undefined, + guid: undefined, + name: "", + parent : undefined, + childCustomFieldDefinition : [] + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + labelParent = "Parent" + labelChildCustomFieldDefinition = "Custom field for child entries" + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + id : Joi.optional(), + guid : Joi.optional(), + name: Joi.string().required().max(450).label(this.labelName), + parent: Joi.optional(), + childCustomFieldDefinition : Joi.optional().label(this.labelChildCustomFieldDefinition) + }; + + doSubmit = async (buttonName : string) => { + try { + const { id, guid, name, parent, childCustomFieldDefinition } = this.state.data; + + const customfieldValues = this.CustomFieldValues(); + + if (this.isEditMode()) { + const generalIdRef = MakeGeneralIdRef(id, guid); + + const response = await glossariesService.putGlossaryItem(generalIdRef, parent, name, childCustomFieldDefinition, customfieldValues); + if (response) { + toast.info("Glossary Item edited"); + } + } else { + const generalIdRef = parent ? MakeGeneralIdRef(parent.id, parent.guid) : SystemGlossaries; + const response = await glossariesService.postGlossaryItem(generalIdRef, name, childCustomFieldDefinition, customfieldValues); + if (response) { + toast.info("New Glossary Item added"); + } + } + + const navigateId = parent ? parent.id : ""; + if (buttonName === this.labelSave) + this.setState({ redirect: "/glossaries/" + navigateId }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { data } = this.state; + + if (this.isEditMode()) { + const { glossaryId } = this.props.router.params; + + const generalIdRef = MakeGeneralIdRef(glossaryId); + try { + const loadedData = await glossariesService.getGlossaryItem( generalIdRef) + if (loadedData) { + data.name = loadedData.name; + data.parent = loadedData.parent; + data.id = loadedData.id; + data.guid = loadedData.guid; + data.childCustomFieldDefinition = loadedData.childCustomFieldDefinition ?? []; + + this.setCustomFieldValues(data, loadedData.customFieldValues, (data.parent as Glossary)?.childCustomFieldDefinition); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + else{ + const { glossaryId } = this.props.router.params; + + const generalIdRef = MakeGeneralIdRef(glossaryId); + try{ + const loadedData = await glossariesService.getGlossaryItem( generalIdRef) + if (loadedData) { + data.parent = loadedData; + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + this.setState({ loaded: true, data, customFields : data.parent?.childCustomFieldDefinition }); + }; + + handleAdd = ( customfield : CustomField) => { + let { data } = this.state; + + data.childCustomFieldDefinition.push( customfield); + + this.setState({ data }); + } + + handleDelete = ( fieldToDelete : CustomField ) => { + let { data } = this.state; + + if (fieldToDelete){ + data.childCustomFieldDefinition = data.childCustomFieldDefinition.filter( x => x !== fieldToDelete); + } + + this.setState({ data }); + } + + render() { + const { loaded, redirect, data } = this.state; + if (redirect !== "") return ; + + let mode = "Add"; + const isEditMode = this.isEditMode(); + if (isEditMode) mode = "Edit"; + + return ( + +

    {mode} Glossary Item

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderCustomFields(data.parent?.childCustomFieldDefinition)} +
    + {this.renderCustomFieldsEditor("childCustomFieldDefinition", this.labelChildCustomFieldDefinition, data.childCustomFieldDefinition, this.handleAdd, this.handleDelete)} +
    + {isEditMode && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCGlossariesDetails = withRouter(GlossariesDetails); + +export default HOCGlossariesDetails; + diff --git a/src/modules/manager/glossary/Glossary.tsx b/src/modules/manager/glossary/Glossary.tsx new file mode 100644 index 0000000..4d2f13c --- /dev/null +++ b/src/modules/manager/glossary/Glossary.tsx @@ -0,0 +1,168 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import withRouter from "../../../utils/withRouter"; +import GlossariesTable from "./components/GlossariesTable"; +import glossariesService, { Glossary, SystemGlossaries } from "./services/glossaryService"; +import { GeneralIdRef } from "./../../../utils/GeneralIdRef"; +import equal from "fast-deep-equal"; +import Button, { ButtonType } from "../../../components/common/Button"; +import Loading from "../../../components/common/Loading"; +import Permission from "../../../components/common/Permission"; + +interface GlossariesState { + loaded: boolean; + data: Glossary | undefined; + // sortColumn : Column, + // filters: Map; + parentAddress: string; +} + +class Glossaries extends Component { + state: GlossariesState = { + loaded: false, + data: undefined, + // sortColumn: { key: "name", label: "Name", order: "asc" }, + // filters: new Map() + parentAddress: "", + }; + + componentDidMount = () => { + this.loadData(); + }; + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: GlossariesState | undefined): void { + if (!equal(this.props?.router?.params?.glossaryId, prevProps?.router?.params?.glossaryId)){ + this.setState({ loaded: false }); + this.loadData(); + } + } + + loadData = async () => { + let parentItem: GeneralIdRef = SystemGlossaries; + + const { glossaryId } = this.props.router.params; + + if (glossaryId !== undefined) + parentItem = MakeGeneralIdRef(glossaryId); + + const data = await glossariesService.getGlossaryItem(parentItem); + if (data) { + let parentAddress = "/glossaries"; + if (data.parent?.guid?.toLowerCase() !== SystemGlossaries.guid?.toLowerCase()) + parentAddress += "/" + data.parent?.id; + + this.setState({ loaded: true, data, parentAddress }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async (keyValue: any) => { + const response = await glossariesService.deleteGlossaryItem(keyValue.id); + if (response) { + this.componentDidMount(); + } + }; + + render(): JSX.Element { + const { loaded, data, parentAddress } = this.state; + + const id = data?.id; + + let table = <>; + let upButton = <>; + let editButton = <>; + + table = ( +
    + Items + +
    + ); + + if (data?.parent) + { + if (data.parent?.id !== BigInt(0) ) { + upButton = ( + + ); + } + + if (data.parent?.id !== BigInt(0) ) { + editButton = ( + + ); + } + } + + const addPath = concatinate( ["/glossaries/add", id], "/"); + + let pathBlock = this.createPathBlock(); + + return ( + +
    + + {upButton} + + + + + + {editButton} + +
    +
    + {pathBlock} + {table} +
    + ); + } + + createPathBlock = () : JSX.Element | undefined => + { + const { data } = this.state; + + let item : Glossary | undefined = data; + const empty = <>; + let pathBlock : JSX.Element = empty; + + while (item !== undefined && (item?.guid.toLowerCase() !== SystemGlossaries.guid && item.id !== BigInt(0)) ){ + let divider = pathBlock === empty ? <> : <>/; + + pathBlock = <>{item?.name}{divider}{pathBlock}; + + item = item?.parent; + } + + if (pathBlock === undefined) + { + pathBlock = <>; + } + + pathBlock = <>Glossaries/{pathBlock} + + return pathBlock; + } +} + +const HOCGlossaries = withRouter(Glossaries); + +export default HOCGlossaries; + +function concatinate( args : (unknown | undefined)[], seperator : string) : string { + let result = "" + for( const arg of args) + { + if (arg !== undefined){ + if ( result !== "") + result += seperator; + result += arg; + } + } + + return result; +} + diff --git a/src/modules/manager/glossary/components/GlossariesTable.tsx b/src/modules/manager/glossary/components/GlossariesTable.tsx new file mode 100644 index 0000000..d66a9cb --- /dev/null +++ b/src/modules/manager/glossary/components/GlossariesTable.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table from "../../../../components/common/Table"; +import { Paginated } from "../../../../services/Paginated"; +import { Glossary } from "../services/glossaryService"; +import equal from 'fast-deep-equal'; +import authentication from "../../../frame/services/authenticationService"; + +export interface GlossaryParams{ + id : number +} + +interface GlossariesTableProps{ + data? : Glossary, + onDelete?: ( keyValue : any ) => void; + params : GlossaryParams; +} + +interface CustomColumn extends Column{ + isCustom : boolean; +} + +interface customField { + [key: string]: any +} + +class GlossariesTable extends React.Component { + findValueById = (data: Glossary, key: string) => { + if (data.customFieldValues){ + for( const value of data.customFieldValues ){ + if (String(value.id.id) === key) + { + const displayValue : string = value.values[0].displayValue ?? ""; + + return displayValue; + } + } + } + + return undefined; + } + + state = { columnsList: ([] as CustomColumn[]) } + + columns : CustomColumn[] = [ + { key: "name", label: "Name", order: "asc", link:"/glossaries/{0}", isCustom: false } + ]; + + PaginatedData = () => { + const { data } = this.props; + const { columnsList } = this.state; + + const pagedData = data!.children.map( (x) => { + let dataItem : customField = { + id: x.id, + name : x.name } + + for( const column of columnsList) { + if (column.isCustom){ + dataItem[column.key] = this.findValueById(x, column.key); + } + } + + return dataItem; + } ) + + let paginated : Paginated = { + count : 1, + page: 1, + pageSize: 1, + totalPages: 1, + data : pagedData + } + + return paginated; + } + + componentDidMount = () => { + this.CompileColumns(); + } + + componentDidUpdate(prevProps: Readonly, prevState: Readonly<{}>, snapshot?: any): void { + if(!equal(this.props.data, prevProps.data)) + this.CompileColumns(); + } + + CompileColumns = () => { + const { data } = this.props; + + let columnsList = [...this.columns]; + + if (data?.childCustomFieldDefinition) + { + for( const customfield of data?.childCustomFieldDefinition) + { + columnsList.push({ + key: String(customfield.id), + label: customfield.name, + order: "asc", + isCustom: true + }) + } + } + + this.setState({columnsList}); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Glossaries.Glossary", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { onDelete } = this.props; + const { columnsList } = this.state; + const editPath = authentication.hasAccess("EditGlossary") ? "/glossaries/edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteGlossary") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + const paginated = this.PaginatedData(); + return
    ; + } +} + +export default GlossariesTable; \ No newline at end of file diff --git a/src/modules/manager/glossary/services/glossaryService.ts b/src/modules/manager/glossary/services/glossaryService.ts new file mode 100644 index 0000000..469c98b --- /dev/null +++ b/src/modules/manager/glossary/services/glossaryService.ts @@ -0,0 +1,106 @@ +import httpService from "../../../../services/httpService"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import { CustomField } from "../../customfields/services/customFieldsService"; + +const apiEndpoint = "/Glossaries"; + +export const SystemGlossaries : GeneralIdRef = +{ + guid: "FA6566F8-B4B0-48C5-9985-336C9284796E".toLowerCase() +} + +export const DomainGlossaries : GeneralIdRef = +{ + guid: "90C48CF1-1A2B-4A76-B30B-260ECB149CCC".toLowerCase() +} + + +export const PrintSpecificationsGlossary : GeneralIdRef = +{ + guid: "35EB8C23-4528-49A7-A798-B8BAE3B06DAF".toLowerCase() +} + + +export interface CustomFieldValue{ + value : string | GeneralIdRef, + displayValue? : string +} + +export interface CustomFieldValues{ + id : GeneralIdRef, + values : CustomFieldValue[] +} + +export interface Glossary{ + id: bigint, + guid: string, + childCustomFieldDefinition : CustomField[], + customFieldValues : CustomFieldValues[], + children: Glossary[], + name : string, + parent? : Glossary +} + +function AddOptionalParam ( params: string, name : string, value : any) +{ + if (value) + { + const operator = (params === "") ? "?" : "&"; + let newParam = name + "=" + value; + + return params + operator + newParam; + } + + return params; +} + +export async function getGlossaryItem( glossary : GeneralIdRef ): Promise { + + let params = AddOptionalParam("", "Id", glossary.id); + params = AddOptionalParam(params, "Guid", glossary.guid); + + const response = await httpService.get(apiEndpoint + "/glossaryItem" + params); + + return response?.data; +} + +export async function postGlossaryItem(parent : GeneralIdRef, name : string, childCustomFieldDefinition : CustomField[], customFieldValues : CustomFieldValues[] ): Promise{ + return await httpService.post(apiEndpoint + "/glossaryItem", { + parent, + name, + childCustomFieldDefinition, + customFieldValues + }); +} + +export async function putGlossaryItem( id : GeneralIdRef, parent : GeneralIdRef | undefined, name : string, customFieldDefinition : CustomField[], customFieldValues : CustomFieldValues[]): Promise{ + const childCustomFieldDefinition = customFieldDefinition.map( (x) => { + return { + id : x.id, + guid: x.guid + } + }); + + return await httpService.put(apiEndpoint + "/glossaryItem", { + id, + name, + parent, + childCustomFieldDefinition, + customFieldValues + }); +} + +export async function deleteGlossaryItem( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/glossaryItem", { data: generalIdRef }); +} + +const glossariesService = { + getGlossaryItem, + postGlossaryItem, + putGlossaryItem, + deleteGlossaryItem +}; + +export default glossariesService; diff --git a/src/modules/manager/organisations/Organisations.tsx b/src/modules/manager/organisations/Organisations.tsx new file mode 100644 index 0000000..d0ba037 --- /dev/null +++ b/src/modules/manager/organisations/Organisations.tsx @@ -0,0 +1,97 @@ +import { Component } from 'react'; +import Button, { ButtonType } from '../../../components/common/Button'; +import Column from '../../../components/common/columns'; +import Permission from '../../../components/common/Permission'; +import { Paginated } from '../../../services/Paginated'; +import OrganisationsTable from './components/OrganisationsTable'; +import organisationsService, { ReadOrganisation } from './services/organisationsService'; +import Loading from '../../../components/common/Loading'; + +interface OrganisationsState{ + loaded: boolean, + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class Organisations extends Component { + state = { + loaded : false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] + }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async (page: number, pageSize: number) => { + const { sortColumn, filters } = this.state; + + const pagedData = await organisationsService.getOrganisations(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async (sortColumn: Column) => { + const { page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await organisationsService.getOrganisations(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + + if (pagedData) { + this.setState({loaded: true, pagedData, sortColumn}); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async (name: string, value: string) => { + const { page, pageSize } = this.state.pagedData; + const { sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await organisationsService.getOrganisations(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState( { loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async (item?: ReadOrganisation) => { + const response = await organisationsService.deleteOrganisation(item?.id, item?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + + + +
    + +
    + ); + } +}; + +export default Organisations; diff --git a/src/modules/manager/organisations/OrganisationsDetails.tsx b/src/modules/manager/organisations/OrganisationsDetails.tsx new file mode 100644 index 0000000..13d7bd1 --- /dev/null +++ b/src/modules/manager/organisations/OrganisationsDetails.tsx @@ -0,0 +1,140 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import Option from "../../../components/common/option"; +import organisationsService from "./services/organisationsService"; +import Loading from "../../../components/common/Loading"; + +interface OrganisationsDetailsState extends FormState { + data: { + name: string; + address: string; + status: string; + }; + redirect: string; + organisationStatusOptions: Option[]; +} + +class OrganisationsDetails extends Form { + state: OrganisationsDetailsState = { + loaded: false, + data: { + name: "", + address: "", + status: "Active" + }, + errors: {}, + redirect: "", + organisationStatusOptions: [ + { _id: "Active", name: "Active" }, + { _id: "Pending", name: "Pending" }, + { _id: "Blocked", name: "Blocked" }, + ] + }; + + labelName = "Name"; + labelAddress = "Address"; + labelStatus = "Status"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + address:Joi.string().required().max(450).label(this.labelAddress), + status:Joi.string().required().max(450).label(this.labelAddress) + }; + + doSubmit = async (buttonName : string) => { + try { + const { name, address, status } = this.state.data; + + if (this.isEditMode()) { + const { organisationId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(organisationId); + const response = await organisationsService.putOrganisation(generalIdRef, name, address, status); + if (response) { + toast.info("Organisation edited"); + } + } else { + const response = await organisationsService.postOrganisation(name, address, status); + if (response) { + toast.info("New Organisation added"); + } + } + + if (buttonName === this.labelSave) + this.setState({ redirect: "/organisations" }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { organisationId } = this.props.router.params; + + if (organisationId !== undefined) { + try + { + const loadedData = await organisationsService.getOrganisation(organisationId); + + const { data } = this.state; + if (loadedData) { + data.name = loadedData.name; + data.address = loadedData.address; + data.status = loadedData.status; + + this.setState({ loaded: true, data }); + } + else { + this.setState({ loaded: false }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) + this.setState({loaded:true}); + }; + + render() { + const { loaded, redirect, organisationStatusOptions } = this.state; + if (redirect !== "") return ; + + let mode = "Add"; + if (this.isEditMode()) mode = "Edit"; + + return ( + +

    {mode} Organisation

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderInput("address", this.labelAddress, InputType.text)} + {this.renderSelect("status", this.labelStatus, organisationStatusOptions)} + {this.isEditMode() && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCOrganisationsDetails = withRouter(OrganisationsDetails); + +export default HOCOrganisationsDetails; diff --git a/src/modules/manager/organisations/components/OrganisationsTable.tsx b/src/modules/manager/organisations/components/OrganisationsTable.tsx new file mode 100644 index 0000000..ccc646e --- /dev/null +++ b/src/modules/manager/organisations/components/OrganisationsTable.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { ReadOrganisation } from "../services/organisationsService"; + +class OrganisationsTable extends React.Component> { + canViewSite = authentication.hasAccess("ViewSite"); + + columns: Column[] = [ + { key: "name", label: "Name", order: "asc", link: this.canViewSite ? "/site/{0}" : undefined }, + { key: "address", label: "Address", order: "asc" }, + { key: "status", label: "Status", order: "asc", searchable: false }, + ]; + + raiseSort = (sortColumn: Column) => { + this.setState({ sortColumn }); + if (this.props.onSort !== undefined) this.props.onSort(sortColumn); + }; + + handleAuditParams = (item: any) => { + return { + entityName: "e_suite.Database.Core.Tables.Printer.Organisation", + primaryKey: '{"Id":' + item.id + "}", + }; + }; + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditOrganisation") ? "{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteOrganisation") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return ( +
    + ); + } +} + +export default OrganisationsTable; diff --git a/src/modules/manager/organisations/services/organisationsService.ts b/src/modules/manager/organisations/services/organisationsService.ts new file mode 100644 index 0000000..332ac9e --- /dev/null +++ b/src/modules/manager/organisations/services/organisationsService.ts @@ -0,0 +1,70 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/Organisations"; + +export type ReadOrganisation = { + id : bigint; + guid? : string; + name : string; + address : string; + status : string; +} + +export async function getOrganisations( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/organisations", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function getOrganisation( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/organisation?" + params ); + + return response?.data; +} + + + export async function postOrganisation(name : string, address: string, status: string): Promise{ + return await httpService.post(apiEndpoint + "/organisation", { + name, + address, + status + }); +} + +export async function putOrganisation( id : GeneralIdRef, name : string, address: string, status: string): Promise{ + return await httpService.put(apiEndpoint + "/organisation", { + GeneralIdRef: id, + name, + address, + status + }); +} + +export async function deleteOrganisation( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/organisation", { data: generalIdRef }); +} + +const organisationsService = { + getOrganisations, + getOrganisation, + postOrganisation, + putOrganisation, + deleteOrganisation +}; + +export default organisationsService; diff --git a/src/modules/manager/sequence/SequenceDetails.tsx b/src/modules/manager/sequence/SequenceDetails.tsx new file mode 100644 index 0000000..de9757d --- /dev/null +++ b/src/modules/manager/sequence/SequenceDetails.tsx @@ -0,0 +1,152 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import sequenceService from "./services/sequenceService"; +import Option from "../../../components/common/option"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import Loading from "../../../components/common/Loading"; + +interface SequenceDetailsState extends FormState { + data: { + name: string; + seed: number; + increment: number; + pattern: string; + rolloverType: string; + }; + redirect: string; +} + +class SequenceDetails extends Form { + state: SequenceDetailsState = { + loaded : false, + data: { + name: "", + seed: 1, + increment: 1, + pattern: "[0]", + rolloverType: "Continuous", + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + labelSeed = "Seed"; + labelIncrement = "Increment"; + labelPattern = "Pattern"; + labelRolloverType = "Rollover Type"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + seed: Joi.number().required().label(this.labelSeed), + increment: Joi.number().required().label(this.labelIncrement), + pattern: Joi.string().required().label(this.labelPattern), + rolloverType: Joi.string().required().label(this.labelRolloverType), + }; + + doSubmit = async (buttonName : string) => { + try { + const { name, seed, increment, pattern, rolloverType } = this.state.data; + + if (this.isEditMode()) { + const { sequenceId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(sequenceId); + const response = await sequenceService.putSequence(generalIdRef, name, seed, increment, pattern, rolloverType); + if (response) { + toast.info("Sequence edited"); + } + } else { + const response = await sequenceService.postSequence(name, seed, increment, pattern, rolloverType); + if (response) { + toast.info("New sequence added"); + } + } + + if (buttonName === this.labelSave) + this.setState({ redirect: "/sequence" }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { sequenceId } = this.props.router.params; + + if (sequenceId !== undefined) { + try { + const loadedData = await sequenceService.getSequence(sequenceId); + if (loadedData) { + const { data } = this.state; + + data.increment = loadedData.increment; + data.name = loadedData.name; + data.pattern = loadedData.pattern; + data.rolloverType = loadedData.rolloverType; + data.seed = loadedData.seed; + + this.setState({ loaded: true, data }); + } + else { + this.setState({ loaded: false }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) + this.setState({ loaded: true } ); + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + const rolloverOptions: Option[] = [ + { _id: "Continuous", name: "Continuous" }, + { _id: "Day", name: "Day" }, + { _id: "Month", name: "Month" }, + { _id: "Year", name: "Year" }, + ]; + + let mode = "Add"; + if (this.isEditMode()) mode = "Edit"; + + return ( + +

    {mode} Sequence

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderInput("seed", this.labelSeed, InputType.text)} + {this.renderInput("increment", this.labelIncrement)} + {this.renderInput("pattern", this.labelPattern)} + {this.renderSelect("rolloverType", this.labelRolloverType, rolloverOptions)} + {this.isEditMode() && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCSequenceDetails = withRouter(SequenceDetails); + +export default HOCSequenceDetails; diff --git a/src/modules/manager/sequence/components/squenceTable.tsx b/src/modules/manager/sequence/components/squenceTable.tsx new file mode 100644 index 0000000..4c30eef --- /dev/null +++ b/src/modules/manager/sequence/components/squenceTable.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { ReadSequence } from "../services/sequenceService"; + +class SequenceTable extends React.Component> { + columns : Column[] = [ + { key: "name", label: "Name", order: "asc" } + ]; + + raiseSort = (sortColumn : Column) => { + this.setState({sortColumn}); + if ( this.props.onSort !== undefined) + this.props.onSort(sortColumn); + } + + handleAuditParams = (item: any) => { + return { + entityName : "e_suite.Database.Core.Tables.Sequences.Sequence", + primaryKey : "{\"Id\":"+item.id+"}" + } + } + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditSequence") ? "edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteSequence") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return
    ; + } +} + +export default SequenceTable; \ No newline at end of file diff --git a/src/modules/manager/sequence/sequence.tsx b/src/modules/manager/sequence/sequence.tsx new file mode 100644 index 0000000..8b1bc88 --- /dev/null +++ b/src/modules/manager/sequence/sequence.tsx @@ -0,0 +1,99 @@ +import React, { Component } from 'react'; +import sequenceService, { ReadSequence } from './services/sequenceService'; +import SequenceTable from './components/squenceTable'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import Button, { ButtonType } from '../../../components/common/Button'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface sequenceState { + loaded: boolean, + pagedData: Paginated, + sortColumn: Column, + filters: Map; +} + +class Sequence extends Component { + state = { + loaded: false, + pagedData: { + page: 1, + pageSize: 10, + count: 0, + totalPages: 1, + data: [] + }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async (page: number, pageSize: number) => { + const { sortColumn, filters } = this.state; + + const pagedData = await sequenceService.getSequences(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async (sortColumn: Column) => { + const { page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await sequenceService.getSequences(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async (name: string, value: string) => { + const { page, pageSize } = this.state.pagedData; + const { sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await sequenceService.getSequences(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async (item?: ReadSequence) => { + const response = await sequenceService.deleteSequence(item?.id, item?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +}; + +export default Sequence; diff --git a/src/modules/manager/sequence/services/sequenceService.ts b/src/modules/manager/sequence/services/sequenceService.ts new file mode 100644 index 0000000..01ed9af --- /dev/null +++ b/src/modules/manager/sequence/services/sequenceService.ts @@ -0,0 +1,81 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/Sequences"; + +export interface ReadSequence{ + id : bigint; + guid : string; + name : string; +} + +export type GetSequenceResponse = { + id : GeneralIdRef; + increment : number; + name : string; + pattern : string; + rolloverType : string; + seed : number; +} + +export async function getSequences( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/sequences", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function getSequence( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/sequence?" + params ); + + return response?.data; +} + + +export async function postSequence(name : string, seed : number, increment : number, pattern : string, rolloverType : string): Promise{ + return await httpService.post(apiEndpoint + "/sequence", { + name, + seed, + increment, + pattern, + rolloverType + }); +} + +export async function putSequence( id : GeneralIdRef, name : string, seed : number, increment : number, pattern : string, rolloverType : string): Promise{ + return await httpService.put(apiEndpoint + "/sequence", { + id, + name, + seed, + increment, + pattern, + rolloverType + }); +} + +export async function deleteSequence( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/sequence", { data: generalIdRef }); +} + +const sequenceService = { + getSequences, + getSequence, + postSequence, + putSequence, + deleteSequence +}; + +export default sequenceService; diff --git a/src/modules/manager/sites/SiteDetails.tsx b/src/modules/manager/sites/SiteDetails.tsx new file mode 100644 index 0000000..dfd357d --- /dev/null +++ b/src/modules/manager/sites/SiteDetails.tsx @@ -0,0 +1,143 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import Option from "../../../components/common/option"; +import siteService from "./services/sitessService"; +import Loading from "../../../components/common/Loading"; + +interface SiteDetailsState extends FormState { + data: { + name: string; + address: string; + status: string; + }; + redirect: string; + organisationStatusOptions: Option[]; +} + +class LocSiteDetails extends Form { + state: SiteDetailsState = { + loaded : false, + data: { + name: "", + address: "", + status: "Active" + }, + errors: {}, + redirect: "", + organisationStatusOptions: [ + { _id: "Active", name: "Active" }, + { _id: "Pending", name: "Pending" }, + { _id: "Blocked", name: "Blocked" }, + ] + }; + + labelName = "Name"; + labelAddress = "Address"; + labelStatus = "Status"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + address:Joi.string().required().max(450).label(this.labelAddress), + status:Joi.string().required().max(450).label(this.labelAddress) + }; + + doSubmit = async (buttonName : string) => { + try { + const { name, address, status } = this.state.data; + + const { organisationId } = this.props.router.params; + var organisationGeneralIdRef = MakeGeneralIdRef(organisationId); + + if (this.isEditMode()) { + const { siteId } = this.props.router.params; + var siteGeneralIdRef = MakeGeneralIdRef(siteId); + + const resposne = await siteService.putSite(siteGeneralIdRef, name, address, status, organisationGeneralIdRef); + if (resposne) { + toast.info("Site edited"); + } + } else { + const response = await siteService.postSite(name, address, status, organisationGeneralIdRef); + if (response) { + toast.info("New site added"); + } + } + + if (buttonName === this.labelSave) + this.setState({ redirect: "/site/" + organisationId }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { siteId } = this.props.router.params; + + if (siteId !== undefined) { + try + { + const loadedData = await siteService.getSite(siteId); + + const { data } = this.state; + if (loadedData) { + data.name = loadedData.name; + data.address = loadedData.address; + data.status = loadedData.status; + + this.setState({ loaded: true, data }); + } + else { + this.setState({ loaded: false }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) + this.setState({loaded:true}); + }; + + render() { + const { loaded, redirect, organisationStatusOptions } = this.state; + if (redirect !== "") return ; + + let mode = "Add"; + if (this.isEditMode()) mode = "Edit"; + + return ( + +

    {mode} Site

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderInput("address", this.labelAddress, InputType.text)} + {this.renderSelect("status", this.labelStatus, organisationStatusOptions)} + {this.isEditMode() && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const SiteDetails = withRouter(LocSiteDetails); + +export default SiteDetails; diff --git a/src/modules/manager/sites/Sites.tsx b/src/modules/manager/sites/Sites.tsx new file mode 100644 index 0000000..2d5aad9 --- /dev/null +++ b/src/modules/manager/sites/Sites.tsx @@ -0,0 +1,108 @@ +import React, { Component } from 'react'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import Button, { ButtonType } from '../../../components/common/Button'; +import withRouter from '../../../utils/withRouter'; +import siteService, { ReadSite } from './services/sitessService'; +import SitesTable from './components/SitesTable'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface SitesState{ + loaded: boolean; + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class LocSites extends Component< any, any, SitesState> { + state = { + loaded: false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + const { organisationId } = this.props.router.params; + + filters.set("organisationId", organisationId); + + const pagedData = await siteService.getSites(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, filters }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + + const { organisationId } = this.props.router.params; + filters.set("organisationId", organisationId); + + const pagedData = await siteService.getSites(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + filters.set(name, value); + + const { organisationId } = this.props.router.params; + filters.set("organisationId", organisationId); + + const pagedData = await siteService.getSites(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async ( item? : ReadSite) => { + await siteService.deleteSite( item?.id, item?.guid); + + this.componentDidMount(); + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +}; + +const Sites = withRouter(LocSites); + +export default Sites; diff --git a/src/modules/manager/sites/components/SitesTable.tsx b/src/modules/manager/sites/components/SitesTable.tsx new file mode 100644 index 0000000..10d0b44 --- /dev/null +++ b/src/modules/manager/sites/components/SitesTable.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import { ReadSite } from "../services/sitessService"; +import Button, { ButtonType } from "../../../../components/common/Button"; +import authentication from "../../../frame/services/authenticationService"; + +class SitesTable extends React.Component> { + canViewSpecification = authentication.hasAccess("ViewSpecification"); + + columns: Column[] = [ + { + key: "name", + label: "Name", + order: "asc", + content: (item) => { + return ( + <> + + + ); + }, + }, + { key: "address", label: "Address", order: "asc" }, + { key: "status", label: "Status", order: "asc" }, + ]; + + raiseSort = (sortColumn: Column) => { + this.setState({ sortColumn }); + if (this.props.onSort !== undefined) this.props.onSort(sortColumn); + }; + + handleAuditParams = (item: any) => { + return { + entityName: "e_suite.Database.Core.Tables.Printer.Site", + primaryKey: '{"Id":' + item.id + "}", + }; + }; + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditSite") ? "{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteSite") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return ( +
    + ); + } +} + +export default SitesTable; diff --git a/src/modules/manager/sites/services/sitessService.ts b/src/modules/manager/sites/services/sitessService.ts new file mode 100644 index 0000000..33d7fc9 --- /dev/null +++ b/src/modules/manager/sites/services/sitessService.ts @@ -0,0 +1,73 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/Site"; + +export type ReadSite = { + id : bigint; + guid? : string; + name : string; + address : string; + status : string; + organisationId : GeneralIdRef; +} + +export async function getSites( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/sites", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + +export async function getSite( id? : bigint, guid? : string ){ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/site?" + params ); + + return response?.data; +} + + + export async function postSite(name : string, address: string, status: string, organisationId : GeneralIdRef): Promise{ + return await httpService.post(apiEndpoint + "/site", { + name, + address, + status, + organisationId + }); +} + +export async function putSite( id : GeneralIdRef, name : string, address: string, status: string, organisationId : GeneralIdRef): Promise{ + return await httpService.put(apiEndpoint + "/site", { + id, + name, + address, + status, + organisationId + }); +} + +export async function deleteSite( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/site", { data: generalIdRef }); +} + +const siteService = { + getSites, + getSite, + postSite, + putSite, + deleteSite +}; + +export default siteService; diff --git a/src/modules/manager/specifications/Specifications.tsx b/src/modules/manager/specifications/Specifications.tsx new file mode 100644 index 0000000..beedea3 --- /dev/null +++ b/src/modules/manager/specifications/Specifications.tsx @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import Button, { ButtonType } from '../../../components/common/Button'; +import withRouter from '../../../utils/withRouter'; +import specificationService, { ReadSpecification } from './services/specificationService'; +import SpecificationsTable from './components/SpecificationsTable'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface SpecificationsState{ + loaded: boolean; + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class LocSpecifications extends Component { + state = { + loaded : false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] + }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + await this.changePage(page, pageSize); + } + + changePage = async (page: number, pageSize: number) => { + const { sortColumn, filters } = this.state; + const { siteId } = this.props.router.params; + filters.set("site.id", siteId); + + const pagedData = await specificationService.GetSSpecifications(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, filters }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async (sortColumn: Column) => { + const { page, pageSize } = this.state.pagedData; + const { filters } = this.state; + + const { siteId } = this.props.router.params; + filters.set("site.id", siteId); + + const pagedData = await specificationService.GetSSpecifications(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async (name: string, value: string) => { + const { page, pageSize } = this.state.pagedData; + const { sortColumn, filters } = this.state; + filters.set(name, value); + + const { siteId } = this.props.router.params; + filters.set("site.id", siteId); + + const pagedData = await specificationService.GetSSpecifications(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async (item?: ReadSpecification) => { + const response = await specificationService.DeleteSpecification(item?.id, item?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + + + +
    + +
    + ); + } +}; + +const Specifications = withRouter(LocSpecifications); + +export default Specifications; diff --git a/src/modules/manager/specifications/SpecificationsDetails.tsx b/src/modules/manager/specifications/SpecificationsDetails.tsx new file mode 100644 index 0000000..b95fff9 --- /dev/null +++ b/src/modules/manager/specifications/SpecificationsDetails.tsx @@ -0,0 +1,198 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form, { FormState } from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import Loading from "../../../components/common/Loading"; +import TemplateFiller from "../../../components/common/TemplateFiller"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import withRouter from "../../../utils/withRouter"; +import { CustomFieldValue, PrintSpecificationsGlossary } from "../glossary/services/glossaryService"; +import specificationService from "./services/specificationService"; + +interface SpecificationsDetailsState extends FormState { + formTemplate?: GeneralIdRef; + data: { + name: string; + printSpecifications?: CustomFieldValue[]; + formInstanceId?: GeneralIdRef; + sigmaId: bigint | null; + }; + redirect: string; + hasErrors: boolean; +} + +class LocSpecificationsDetails extends Form { + private TemplateFiller: React.RefObject; + + constructor(props: any) { + super(props); + this.TemplateFiller = React.createRef(); + } + + state: SpecificationsDetailsState = { + loaded: false, + formTemplate: undefined, + data: { + name: "", + sigmaId: null, + }, + errors: {}, + redirect: "", + hasErrors: true, + }; + + labelName = "Name"; + labelPrintSpecification = "Print Specification"; + labelStatus = "Status"; + labelSigmaId = "SigmaId"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + printSpecifications: Joi.optional(), + formInstanceId: Joi.optional(), + sigmaId: Joi.number().allow(null).label(this.labelSigmaId), + }; + + doSubmit = async (buttonName: string) => { + try { + const { name, formInstanceId, sigmaId } = this.state.data; + + const { siteId, organisationId, specificationId } = this.props.router.params; + const siteIdGeneralIdRef = MakeGeneralIdRef(siteId); + const templateFiller = this.TemplateFiller.current!; + + if (this.isEditMode()) { + await templateFiller.Save(); + const specificationIdGeneralIdRef = MakeGeneralIdRef(specificationId); + const response = await specificationService.PutSpecification(specificationIdGeneralIdRef, siteIdGeneralIdRef, name, formInstanceId!, sigmaId); + if (response) { + toast.info("Specifications edited"); + } + } else { + const formInstanceId = await templateFiller.Save(); + if (!formInstanceId) { + toast.error("Failed to save form instance"); + return; + } + + const response = await specificationService.PostSpecification(siteIdGeneralIdRef, name, formInstanceId!, sigmaId); + if (response) { + toast.info("New Specifications added"); + } + } + + if (buttonName === this.labelSave) this.setState({ redirect: "/Specifications/" + organisationId + "/" + siteId }); + } catch (ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { specificationId } = this.props.router.params; + + if (specificationId !== undefined) { + try { + const loadedData = await specificationService.GetSSpecification(BigInt(specificationId)); + + const { data } = this.state; + if (loadedData) { + data.name = loadedData.name; + data.formInstanceId = loadedData.formInstanceId; + + this.setState({ loaded: true, data }); + } else { + this.setState({ loaded: false }); + } + } catch (ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) this.setState({ loaded: true }); + }; + + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + let { printSpecifications } = this.state.data; + if (printSpecifications !== prevState.data.printSpecifications) { + if (printSpecifications && printSpecifications.length > 0) { + this.LoadFormTemplate(printSpecifications[0].value as GeneralIdRef); + } + } + } + + LoadFormTemplate = async (printSpecifications?: GeneralIdRef) => { + let { data } = this.state; + + if (data.printSpecifications && data.printSpecifications.length > 0) { + if (((data.printSpecifications[0] as CustomFieldValue).value as GeneralIdRef).id === BigInt(0)) data.printSpecifications = undefined; + } + + let formTemplate; + if (data.printSpecifications === undefined) { + formTemplate = undefined; + } else { + formTemplate = await specificationService.GetTemplateForPrintSpec(data.printSpecifications[0].value as GeneralIdRef); + } + if (formTemplate) { + this.setState({ loaded: true, data, formTemplate }); + } else { + this.setState({ loaded: false }); + } + }; + + handleValidationChanged = () => { + const templateFiller = this.TemplateFiller.current; + + let templateErrors: boolean = false; + + if (templateFiller) templateErrors = !templateFiller.hasValidationErrors(); + + this.setState({ hasErrors: templateErrors }); + }; + + render() { + const { loaded, redirect, formTemplate, data, hasErrors } = this.state; + if (redirect !== "") return ; + + let mode = "Add"; + const isEditMode = this.isEditMode(); + if (isEditMode) mode = "Edit"; + + return ( + +

    {mode} Specifications

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderInput("sigmaId", this.labelSigmaId, InputType.number)} + {!this.isEditMode() && this.renderGlossaryPicker(true, "printSpecifications", this.labelPrintSpecification, 1, PrintSpecificationsGlossary)} + + + +
    + {isEditMode && this.renderButton(this.labelApply, undefined, undefined, undefined, hasErrors)} + {this.renderButton(this.labelSave, undefined, undefined, undefined, hasErrors)} + +
    + ); + } +} + +const SpecificationsDetails = withRouter(LocSpecificationsDetails); + +export default SpecificationsDetails; diff --git a/src/modules/manager/specifications/components/SpecificationsTable.tsx b/src/modules/manager/specifications/components/SpecificationsTable.tsx new file mode 100644 index 0000000..88c2ec8 --- /dev/null +++ b/src/modules/manager/specifications/components/SpecificationsTable.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { ReadSpecification } from "../services/specificationService"; + +class SpecificationsTable extends React.Component> { + columns: Column[] = [{ key: "name", label: "Name", order: "asc" }]; + + raiseSort = (sortColumn: Column) => { + this.setState({ sortColumn }); + if (this.props.onSort !== undefined) this.props.onSort(sortColumn); + }; + + handleAuditParams = (item: any) => { + return { + entityName: "e_suite.Database.Core.Tables.Printer.Specification", + primaryKey: '{"Id":' + item.id + "}", + }; + }; + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditSpecification") ? "{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteSpecification") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return ( +
    + ); + } +} + +export default SpecificationsTable; diff --git a/src/modules/manager/specifications/services/specificationService.ts b/src/modules/manager/specifications/services/specificationService.ts new file mode 100644 index 0000000..dee866d --- /dev/null +++ b/src/modules/manager/specifications/services/specificationService.ts @@ -0,0 +1,87 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/Specification"; + +export type ReadSpecification = { + id: bigint; + guid?: string; + name: string; + sigmaId: bigint | null; + site: GeneralIdRef; + formInstanceId: GeneralIdRef; +}; + +export async function GetTemplateForPrintSpec(printerSpecification: GeneralIdRef): Promise { + const response = await httpService.get(apiEndpoint + "/getTemplateForPrintSpec?id=" + printerSpecification.id + "&guid=" + printerSpecification.guid); + return response?.data; +} + +export async function GetSSpecifications( + page: number, + pageSize: number, + sortKey: string, + sortAscending: boolean, + filters?: Map +): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/specifications", { + params: { + page: page, + pageSize: pageSize, + sortKey: sortKey, + sortAscending: sortAscending, + filters: filterString, + }, + }); + + return response?.data; +} + +export async function GetSSpecification(id?: bigint, guid?: string) { + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/specification?" + params); + + return response?.data; +} + +export async function PostSpecification(site: GeneralIdRef, name: string, formInstanceId: GeneralIdRef, sigmaId: bigint | null): Promise { + const response = await httpService.post(apiEndpoint + "/specification", { + site, + name, + formInstanceId, + sigmaId, + }); + + return response?.data; +} + +export async function PutSpecification(id: GeneralIdRef, site: GeneralIdRef, name: string, formInstanceId: GeneralIdRef, sigmaId: bigint | null): Promise { + return await httpService.put(apiEndpoint + "/specification", { + id, + site, + name, + formInstanceId, + sigmaId, + }); +} + +export async function DeleteSpecification(id?: bigint, guid?: string): Promise { + const generalIdRef = MakeGeneralIdRef(id, guid); + + return await httpService.delete(apiEndpoint + "/specification", { data: generalIdRef }); +} + +const specificationService = { + GetTemplateForPrintSpec, + GetSSpecifications, + GetSSpecification, + PostSpecification, + PutSpecification, + DeleteSpecification, +}; + +export default specificationService; diff --git a/src/modules/manager/ssoManager/SsoProviderDetails.tsx b/src/modules/manager/ssoManager/SsoProviderDetails.tsx new file mode 100644 index 0000000..8e56252 --- /dev/null +++ b/src/modules/manager/ssoManager/SsoProviderDetails.tsx @@ -0,0 +1,161 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form from "../../../components/common/Form"; +import { InputType } from "../../../components/common/Input"; +import { FormState } from "../../../components/common/Form"; +import withRouter from "../../../utils/withRouter"; +import { MakeGeneralIdRef } from "../../../utils/GeneralIdRef"; +import Loading from "../../../components/common/Loading"; +import ssoManagerService from "./services/ssoManagerService"; + +interface SsoProviderDetailsState extends FormState { + data: { + name: string; + clientId: string, + clientSecret: string, + validIssuer: string, + authorizationEndpoint: string, + tokenEndpoint: string, + isPublic: boolean, + }; + redirect: string; +} + +class SsoProviderDetails extends Form { + state: SsoProviderDetailsState = { + loaded : false, + data: { + name: "", + clientId: "", + clientSecret: "", + validIssuer: "", + authorizationEndpoint: "", + tokenEndpoint: "", + isPublic: true, + }, + errors: {}, + redirect: "", + }; + + labelName = "Name"; + labelClientId = "Client Id"; + labelClientSecret = "Client Secret"; + labelValidIssuer = "Valid Issuer"; + labelAuthorizationEndpoint = "Authorisation Endpoint"; + labelTokenEndpoint = "Token Endpoint"; + labelIsPublic = "Is Public"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + name: Joi.string().required().max(450).label(this.labelName), + clientId: Joi.string().required().max(450).label(this.labelClientId), + clientSecret: Joi.string().required().max(450).label(this.labelClientSecret), + validIssuer: Joi.string().required().max(450).label(this.labelValidIssuer), + authorizationEndpoint: Joi.string().required().max(450).label(this.labelAuthorizationEndpoint), + tokenEndpoint: Joi.string().required().max(450).label(this.labelTokenEndpoint), + isPublic: Joi.bool().required().label(this.labelIsPublic), + }; + + doSubmit = async (buttonName : string) => { + try { + const { name, clientId, clientSecret, validIssuer, authorizationEndpoint, tokenEndpoint, isPublic } = this.state.data; + + if (this.isEditMode()) { + const { ssoProviderId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(ssoProviderId); + const response = await ssoManagerService.putSsoProvider(generalIdRef, name, clientId, clientSecret, validIssuer, authorizationEndpoint, tokenEndpoint, isPublic); + if (response) { + toast.info("Sso Provider edited"); + } + } else { + const response = await ssoManagerService.postSsoProvider(name, clientId, clientSecret, validIssuer, authorizationEndpoint, tokenEndpoint, isPublic); + if (response) { + toast.info("New Sso Provider added"); + } + } + + if (buttonName === this.labelSave) + this.setState({ redirect: "/ssoManager" }); + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + componentDidMount = async () => { + const { ssoProviderId } = this.props.router.params; + + if (ssoProviderId !== undefined) { + try { + const loadedData = await ssoManagerService.getSsoProvider(ssoProviderId); + if (loadedData) { + const { data } = this.state; + + data.name = loadedData.name; + data.clientId = loadedData.clientId; + data.clientSecret = loadedData.clientSecret; + data.validIssuer = loadedData.validIssuer; + data.authorizationEndpoint = loadedData.authorizationEndpoint; + data.tokenEndpoint = loadedData.tokenEndpoint; + data.isPublic = loadedData.isPublic + + this.setState({ loaded: true, data }); + } + else { + this.setState({ loaded: false }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + if (!this.isEditMode()) + this.setState({ loaded: true } ); + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + let mode = "Add"; + if (this.isEditMode()) mode = "Edit"; + + let redirectUrl = window.location.href.slice(0,window.location.href.length - this.props.router.location.pathname.length) + "/account/auth/" + this.props.router.params.ssoProviderId; + + return ( + +

    {mode} Sso Provider

    +
    + {this.renderError("_general")} + {this.renderInput("name", this.labelName, InputType.text)} + {this.renderInput("clientId", this.labelClientId, InputType.text)} + {this.renderInput("clientSecret", this.labelClientSecret, InputType.text)} + {this.renderInput("validIssuer", this.labelValidIssuer, InputType.text)} + {this.renderInput("authorizationEndpoint", this.labelAuthorizationEndpoint, InputType.text)} + {this.renderInput("tokenEndpoint", this.labelTokenEndpoint, InputType.text)} +
    {this.renderInput("isPublic", this.labelIsPublic, InputType.checkbox)}
    + + {this.isEditMode() &&
    Redirect URL: {redirectUrl}
    } + + {this.isEditMode() && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const HOCSsoProviderDetails = withRouter(SsoProviderDetails); + +export default HOCSsoProviderDetails; diff --git a/src/modules/manager/ssoManager/components/ssoManagerTable.tsx b/src/modules/manager/ssoManager/components/ssoManagerTable.tsx new file mode 100644 index 0000000..4accb1b --- /dev/null +++ b/src/modules/manager/ssoManager/components/ssoManagerTable.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import authentication from "../../../frame/services/authenticationService"; +import { GetSsoProvider } from "../services/ssoManagerService"; + +class SsoManagerTable extends React.Component> { + columns: Column[] = [{ key: "name", label: "Name", order: "asc" }]; + + raiseSort = (sortColumn: Column) => { + this.setState({ sortColumn }); + if (this.props.onSort !== undefined) this.props.onSort(sortColumn); + }; + + handleAuditParams = (item: any) => { + return { + entityName: "e_suite.Database.Core.Tables.UserManager.SsoProvider", + primaryKey: '{"Id":' + item.id + "}", + }; + }; + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditSsoProvider") ? "edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteSsoProvider") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return ( +
    + ); + } +} + +export default SsoManagerTable; diff --git a/src/modules/manager/ssoManager/services/ssoManagerService.ts b/src/modules/manager/ssoManager/services/ssoManagerService.ts new file mode 100644 index 0000000..df63211 --- /dev/null +++ b/src/modules/manager/ssoManager/services/ssoManagerService.ts @@ -0,0 +1,84 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/SsoManager"; + +export type GetSsoProvider = { + id : bigint; + guid : string; + name : string; + clientId : string; + clientSecret : string; + validIssuer : string; + authorizationEndpoint : string; + tokenEndpoint : string; + isPublic : boolean; + deleted : boolean; +} + +export async function getSsoProviders( page : number, pageSize : number, sortKey : string, sortAscending : boolean, filters? : Map | undefined ): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/ssoProviders", + { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + + return response?.data; +} + + + +export async function getSsoProvider( id? : bigint, guid? : string ): Promise{ + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/ssoProvider?" + params ); + + return response?.data; +} + +export async function postSsoProvider(name : string, clientId : string, clientSecret : string, validIssuer : string, authorizationEndpoint : string, tokenEndpoint : string, isPublic : boolean): Promise{ + return await httpService.post(apiEndpoint + "/ssoProvider", { + name, + clientId, + clientSecret, + validIssuer, + authorizationEndpoint, + tokenEndpoint, + isPublic + }); +} + +export async function putSsoProvider( id : GeneralIdRef, name : string, clientId : string, clientSecret : string, validIssuer : string, authorizationEndpoint : string, tokenEndpoint : string, isPublic : boolean): Promise{ + return await httpService.put(apiEndpoint + "/ssoProvider", { + id, + name, + clientId, + clientSecret, + validIssuer, + authorizationEndpoint, + tokenEndpoint, + isPublic + }); +} + +export async function deleteSsoProvider( id? : bigint, guid?: string): Promise{ + const generalIdRef = MakeGeneralIdRef( id, guid); + + return await httpService.delete(apiEndpoint + "/ssoProvider", { data: generalIdRef }); +} + +const ssoManagerService = { + getSsoProviders, + getSsoProvider, + postSsoProvider, + putSsoProvider, + deleteSsoProvider +}; + +export default ssoManagerService; diff --git a/src/modules/manager/ssoManager/ssoManager.tsx b/src/modules/manager/ssoManager/ssoManager.tsx new file mode 100644 index 0000000..bc301b1 --- /dev/null +++ b/src/modules/manager/ssoManager/ssoManager.tsx @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import SsoManagerTable from './components/ssoManagerTable'; +import ssoManagerService, { GetSsoProvider } from './services/ssoManagerService'; +import Button, { ButtonType } from '../../../components/common/Button'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface SsoManagerState{ + loaded: boolean; + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class SsoManager extends Component< any, any, SsoManagerState> { + state = { + loaded : false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "name", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + + const pagedData = await ssoManagerService.getSsoProviders(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData }); + } + else { + this.setState({ loaded: false }); + } + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await ssoManagerService.getSsoProviders(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn }); + } + else { + this.setState({ loaded: false }); + } + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + filters.set(name, value); + + const pagedData = await ssoManagerService.getSsoProviders(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if (pagedData) { + this.setState({ loaded: true, filters, pagedData }); + } + else { + this.setState({ loaded: false }); + } + }; + + onDelete = async ( keyValue? : GetSsoProvider) => { + const response = await ssoManagerService.deleteSsoProvider( keyValue?.id, keyValue?.guid); + if (response) { + this.componentDidMount(); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +}; + +export default SsoManager; diff --git a/src/modules/manager/users/UserDetails.tsx b/src/modules/manager/users/UserDetails.tsx new file mode 100644 index 0000000..bde8f47 --- /dev/null +++ b/src/modules/manager/users/UserDetails.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import HorizontalTabs from "../../../components/common/HorizionalTabs"; +import Tab from "../../../components/common/Tab"; +import GeneralTab from "./components/GeneralTab"; + +interface UserDetailsProps { + editMode : boolean; +} + +class UserDetails extends React.Component { + isEditMode = () => { + const { editMode } = this.props; + return editMode; + }; + + render() { + const isEditMode = this.isEditMode(); + + let mode = "Add"; + if (isEditMode) mode = "Edit"; + + + let tabs : JSX.Element[] = []; + + tabs.push( + + ); + + return ( +
    +

    {mode} User

    + + {tabs} + +
    + ); + } +} + +export default UserDetails; diff --git a/src/modules/manager/users/components/GeneralTab.tsx b/src/modules/manager/users/components/GeneralTab.tsx new file mode 100644 index 0000000..c691455 --- /dev/null +++ b/src/modules/manager/users/components/GeneralTab.tsx @@ -0,0 +1,144 @@ +import Joi from "joi"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import Form, { FormState } from "../../../../components/common/Form"; +import { InputType } from "../../../../components/common/Input"; +import { GeneralIdRef, MakeGeneralIdRef } from "../../../../utils/GeneralIdRef"; +import withRouter, { RouterProps } from "../../../../utils/withRouter"; +import authentication from "../../../frame/services/authenticationService"; +import userService from "../services/usersService"; +import Loading from "../../../../components/common/Loading"; +import { CustomFieldValue } from "../../glossary/services/glossaryService"; + +interface GeneralTabProps extends RouterProps { + isEditMode: boolean; +} + +interface GeneralTabState extends FormState { + data: { + firstName: string; + lastName: string; + middleNames: string; + email: string; + domain: CustomFieldValue[]; + }; + redirect: string; +} + +class LocGeneralTab extends Form { + state: GeneralTabState = { + loaded: false, + data: { + firstName: "", + lastName: "", + middleNames: "", + email: "", + domain: [], + }, + errors: {}, + redirect: "", + }; + + labelFirstName = "First name"; + labelMiddleNames = "Middle names"; + labelLastName = "Last name"; + labelEmail = "Mail"; + labelDomain = "Domain"; + + labelApply = "Save"; + labelSave = "Save and close"; + + schema = { + firstName: Joi.string().required().max(450).label(this.labelFirstName), + middleNames: Joi.string().allow("").required().label(this.labelMiddleNames), + lastName: Joi.string().required().label(this.labelLastName), + email: Joi.string() + .required() + .email({ tlds: { allow: false } }) + .label(this.labelEmail), + domain: Joi.optional(), + }; + + doSubmit = async (buttonName: string) => { + try { + const { isEditMode } = this.props; + + const { firstName, middleNames, lastName, email, domain } = this.state.data; + + if (isEditMode) { + const { userId } = this.props.router.params; + + var generalIdRef = MakeGeneralIdRef(userId); + const response = await userService.putUser(generalIdRef, firstName, middleNames, lastName, email, domain[0]?.value as GeneralIdRef); + if (response) { + toast.info("User edited"); + } + } else { + const response = await userService.postUser(firstName, middleNames, lastName, email, domain[0]?.value as GeneralIdRef); + if (response) { + toast.info("New User added"); + } + } + + if (buttonName === this.labelSave) this.setState({ redirect: "/users" }); + } catch (ex: any) { + this.handleGeneralError(ex); + } + }; + + componentDidMount = async () => { + const { userId } = this.props.router.params; + + const { data } = this.state; + + if (userId !== undefined) { + try { + const loadedData = await userService.getUser(userId); + if (loadedData) { + data.firstName = loadedData.firstName; + data.lastName = loadedData.lastName; + data.middleNames = loadedData.middleNames; + data.email = loadedData.email; + data.domain = [{ value: loadedData.domain }]; + } + } catch (ex: any) { + this.handleGeneralError(ex); + } + } else { + const user = authentication.getCurrentUser(); + + data.domain = [{ value: MakeGeneralIdRef(user?.domainid) }]; + } + + this.setState({ loaded: true, data }); + }; + + render() { + const { loaded, redirect } = this.state; + if (redirect !== "") return ; + + const { isEditMode } = this.props; + return ( + +
    + {this.renderError("_general")} + {isEditMode && this.renderInput("email", this.labelEmail, InputType.text, true)} + {!isEditMode && this.renderInput("email", this.labelEmail)} + {this.renderInput("firstName", this.labelFirstName)} + {this.renderInput("middleNames", this.labelMiddleNames)} + {this.renderInput("lastName", this.labelLastName)} + + {this.renderDomainPicker(true, "domain", this.labelDomain, 1, 1)} + + {isEditMode && this.renderButton(this.labelApply)} + {this.renderButton(this.labelSave)} + +
    + ); + } +} + +const GeneralTab = withRouter(LocGeneralTab); + +export default GeneralTab; diff --git a/src/modules/manager/users/components/usersTable.tsx b/src/modules/manager/users/components/usersTable.tsx new file mode 100644 index 0000000..e63c19b --- /dev/null +++ b/src/modules/manager/users/components/usersTable.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import Column from "../../../../components/common/columns"; +import Table, { PublishedTableProps } from "../../../../components/common/Table"; +import { GetUser } from "../services/usersService"; +import Button, { ButtonType } from "../../../../components/common/Button"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; +import authentication from "../../../frame/services/authenticationService"; + +export interface UsersTableProps extends PublishedTableProps { + resendConfirmEmail?: (user: GetUser) => void; +} + +class UsersTable extends React.Component { + canResendConfirmMail = authentication.hasAccess("ResendConfirmMail"); + + columns: Column[] = [ + { key: "displayName", label: "Name", order: "asc" }, + { key: "email", label: "Email", order: "asc" }, + { key: "domainName", label: "Domain", order: "asc" }, + { key: "created", label: "Created", order: "asc" }, + { key: "lastUpdated", label: "Last Updated", order: "asc" }, + { + key: "emailConfirmed", + label: "Resend Confirm", + order: "asc", + searchable: false, + content: (item) => { + if (!item!.emailConfirmed && this.canResendConfirmMail) { + return ( + <> + + + ); + } + return <>; + }, + }, + ]; + + resendConfirmEmail = (keyValue: GetUser) => { + const { resendConfirmEmail } = this.props; + + if (resendConfirmEmail != null) resendConfirmEmail(keyValue!); + }; + + raiseSort = (sortColumn: Column) => { + this.setState({ sortColumn }); + if (this.props.onSort !== undefined) this.props.onSort(sortColumn); + }; + + handleAuditParams = (item: any) => { + return { + entityName: "e_suite.Database.Core.Tables.UserManager.User", + primaryKey: '{"Id":' + item.id + "}", + }; + }; + + canDelete = (item: GetUser) => { + const user = authentication.getCurrentUser(); + return item.id != user!.primarysid; + }; + + render() { + const { data, sortColumn, onChangePage, onSearch, onDelete } = this.props; + const editPath = authentication.hasAccess("EditUser") ? "edit/{0}" : undefined; + const doDelete = authentication.hasAccess("DeleteUser") ? onDelete : undefined; + const showAudit = authentication.hasAccess("ViewAuditLog") ? this.handleAuditParams : undefined; + + return ( +
    + ); + } +} + +export default UsersTable; diff --git a/src/modules/manager/users/services/usersService.ts b/src/modules/manager/users/services/usersService.ts new file mode 100644 index 0000000..340274a --- /dev/null +++ b/src/modules/manager/users/services/usersService.ts @@ -0,0 +1,94 @@ +import httpService from "../../../../services/httpService"; +import { Paginated } from "../../../../services/Paginated"; +import { GeneralIdRef, MakeGeneralIdRef, MakeGeneralIdRefParams } from "../../../../utils/GeneralIdRef"; +import MapToJson from "../../../../utils/MapToJson"; + +const apiEndpoint = "/User"; + +export interface IUserBase { + firstName: string; + lastName: string; + middleNames: string; + email: string; + domain: GeneralIdRef; +} + +export interface GetUser extends IUserBase { + id: bigint; + guid: string; + displayName: string; + created: Date; + lastUpdated: Date; + domainName: string; + emailConfirmed: boolean; +} + +export async function getUsers(page: number, pageSize: number, sortKey: string, sortAscending: boolean, filters?: Map | undefined): Promise> { + const filterString = MapToJson(filters); + const response = await httpService.get>(apiEndpoint + "/users", { params: { + page: page, + pageSize: pageSize, + sortKey : sortKey, + sortAscending: sortAscending, + filters : filterString + } } ); + return response?.data; +} + +export async function getUser(id?: bigint, guid?: string) { + const params = MakeGeneralIdRefParams(id, guid); + + const response = await httpService.get(apiEndpoint + "/user?" + params); + + return response?.data; +} + +export async function postUser(firstName: string, middleNames: string, lastName: string, email: string, domain?: GeneralIdRef): Promise { + const domainId = MakeGeneralIdRef(domain?.id, domain?.guid); + + return await httpService.post(apiEndpoint + "/user", { + firstName, + middleNames, + lastName, + email, + domainId + }); +} + +export async function putUser(id: GeneralIdRef, firstName: string, middleNames: string, lastName: string, email: string, domain?: GeneralIdRef): Promise { + return await httpService.put(apiEndpoint + "/user", { + id, + firstName, + middleNames, + lastName, + email, + domain + }); +} + +export async function deleteUserByEmail(email?: string): Promise { + return await httpService.delete(apiEndpoint + "/userByEmail", { data: email }); +} + +export async function deleteUser(id?: bigint, guid?: string): Promise { + const generalIdRef = MakeGeneralIdRef(id, guid); + return await httpService.delete(apiEndpoint + "/user", { data: generalIdRef }); +} + +export async function resendConfirmEmail(id?: bigint, guid?: string): Promise { + const generalIdRef = MakeGeneralIdRef(id, guid); + + return await httpService.post(apiEndpoint + "/resendConfirmEmail", generalIdRef); +} + +const userService = { + getUsers, + getUser, + postUser, + putUser, + deleteUser, + deleteUserByEmail, + resendConfirmEmail +}; + +export default userService; diff --git a/src/modules/manager/users/users.tsx b/src/modules/manager/users/users.tsx new file mode 100644 index 0000000..42f96cf --- /dev/null +++ b/src/modules/manager/users/users.tsx @@ -0,0 +1,103 @@ +import React, { Component } from 'react'; +import Column from '../../../components/common/columns'; +import { Paginated } from '../../../services/Paginated'; +import UsersTable from './components/usersTable'; +import userService, { GetUser } from './services/usersService'; +import Button, { ButtonType } from '../../../components/common/Button'; +import { toast } from 'react-toastify'; +import Loading from '../../../components/common/Loading'; +import Permission from '../../../components/common/Permission'; + +interface UsersState{ + loaded: boolean, + pagedData : Paginated, + sortColumn : Column, + filters: Map; +} + +class Users extends Component< any, any, UsersState> { + state = { + loaded: false, + pagedData : { page: 1, + pageSize : 10, + count: 0, + totalPages: 1, + data: [] }, + sortColumn: { key: "displayName", label: "Name", order: "asc" }, + filters: new Map() + } + + componentDidMount = async () => { + const { page, pageSize } = this.state.pagedData; + + await this.changePage(page, pageSize); + } + + changePage = async(page: number, pageSize : number) =>{ + const { sortColumn, filters } = this.state; + + const pagedData = await userService.getUsers(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + if(pagedData) { + this.setState({ loaded: true, pagedData }); + } else { + this.setState({ loaded: false }); + } + } + + onSort = async(sortColumn : Column) => { + const {page, pageSize } = this.state.pagedData; + const { filters } = this.state; + const pagedData = await userService.getUsers(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + + if (pagedData) { + this.setState({ loaded: true, pagedData, sortColumn}); + } else { + this.setState({ loaded: false }); + } + } + + onSearch = async ( name: string, value: string) => { + const {page, pageSize } = this.state.pagedData; + const {sortColumn, filters } = this.state; + filters.set(name, value); + const pagedData = await userService.getUsers(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); + + if (pagedData) { + this.setState( { loaded: true, filters, pagedData }); + } else { + this.setState({ loaded: false }); + } + }; + + onDelete = async ( item? : GetUser) => { + const response = await userService.deleteUser( item?.id, item?.guid); + if (response) { + this.componentDidMount(); + } + } + + resentConfirmEmail = async (user: GetUser) => { + const response = await userService.resendConfirmEmail( user.id, user.guid); + if (response) { + toast.info("Confirm e-mail resent"); + } + } + + render(): JSX.Element { + const { loaded, pagedData, sortColumn } = this.state; + + return ( + + +
    + +
    +
    +
    + +
    + ); + } +}; + +export default Users; diff --git a/src/modules/profile/Profile.tsx b/src/modules/profile/Profile.tsx new file mode 100644 index 0000000..f9e2260 --- /dev/null +++ b/src/modules/profile/Profile.tsx @@ -0,0 +1,204 @@ +import React from "react"; +import Joi from "joi"; +import { toast } from "react-toastify"; +import Form, { FormState, FormData } from "../../components/common/Form"; +import profileService from "./services/profileService"; +import { InputType } from "../../components/common/Input"; +import { TwoFactorAuthenticationSettings } from "./models/TwoFactorAuthenticationSettings"; +import Loading from "../../components/common/Loading"; + +export interface ProfileStateData extends FormData { + firstName: string; + middleNames: string; + lastName: string; + email: string; + newPassword: string; + confirmPassword: string; + originalUsingTwoFactorAuthentication: boolean; + usingTwoFactorAuthentication: boolean; + tfaCode: string; +} + +export interface ProfileState extends FormState { + data: ProfileStateData; + twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings; +} + +class Profile extends Form { + state = { + loaded: false, + passwordMaxLenght : 255, + data: { + firstName: "", + middleNames: "", + lastName: "", + email: "", + newPassword: "", + confirmPassword: "", + originalUsingTwoFactorAuthentication: false, + usingTwoFactorAuthentication: false, + tfaCode: "" + }, + errors: {}, + twoFactorAuthenticationSettings: { + manualEntrySetupCode: "", + qrCodeImageUrl: "", + }, + }; + + labelFirstName = "First Name"; + labelMiddleNames = "Middle Name(s)"; + labelLastName = "Last Name"; + labelEmail = "E-Mail"; + labelNewPassword = "New Password"; + labelConfirmPassword = "Confirm Password"; + + labelUsingTwoFactorAuthentication = "2 Factor Authentication"; + labelTfaCode = "Authentication code"; + labelApply = "Save"; + + schema = { + firstName: Joi.string().required().label(this.labelFirstName), + middleNames: Joi.string().allow("").required().label(this.labelMiddleNames), + lastName: Joi.string().required().label(this.labelLastName), + email: Joi.string() + .required() + .email({ tlds: { allow: false } }) + .label(this.labelEmail), + newPassword: Joi.string().allow("").min(5).label(this.labelNewPassword), + confirmPassword: Joi.string() + .when("newPassword", { + is: "", + then: Joi.allow("").optional(), + otherwise: Joi.valid(Joi.ref("newPassword")).error(() => { + const e = new Error("Passwords must match"); + e.name = "confirmPassword"; + return e; + }), + }) + .label(this.labelConfirmPassword), + + originalUsingTwoFactorAuthentication: Joi.boolean().required(), + usingTwoFactorAuthentication: Joi.boolean().required().label(this.labelUsingTwoFactorAuthentication), + + tfaCode: Joi.string() + .when("originalUsingTwoFactorAuthentication", { + is: Joi.ref("usingTwoFactorAuthentication"), + then: Joi.allow("").optional(), + otherwise: Joi.when("usingTwoFactorAuthentication", { + is: true, + then: Joi.string() + .length(6) + .required() + .error(() => { + const e = new Error("You must enter the code from the authenicator"); + e.name = "tfaCode"; + return e; + }), + otherwise: Joi.allow("").optional(), + }), + }) + .label(this.labelTfaCode), + }; + + async componentDidMount() { + try { + const profile = await profileService.getMyProfile(); + if (profile) { + const { firstName, middleNames, lastName, email, usingTwoFactorAuthentication, twoFactorAuthenticationSettings } = profile; + + const data = { + firstName, + middleNames, + lastName, + email, + newPassword: "", + confirmPassword: "", + originalUsingTwoFactorAuthentication: usingTwoFactorAuthentication, + usingTwoFactorAuthentication, + tfaCode: "" + }; + this.setState({ loaded: true, data, twoFactorAuthenticationSettings }); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + } + + doSubmit = async (buttonName : string) => { + try { + const { firstName, middleNames, lastName, email, usingTwoFactorAuthentication, tfaCode, newPassword, confirmPassword } = this.state.data; + + let password = ""; + + if (newPassword === confirmPassword) password = newPassword; + + const response = await profileService.putMyProfile(firstName, middleNames, lastName, email, usingTwoFactorAuthentication, tfaCode, password); + if (response) { + await this.componentDidMount(); + toast.info("Your profile settings have been saved"); + } + } + catch(ex: any) { + this.handleGeneralError(ex); + } + }; + + render() { + const { loaded, twoFactorAuthenticationSettings, passwordMaxLenght } = this.state; + + const { usingTwoFactorAuthentication, newPassword } = this.state.data; + + const tfaEnabled = usingTwoFactorAuthentication ? "Enabled" : "Disabled"; + + let tfaImageBlock = null; + if (twoFactorAuthenticationSettings) + tfaImageBlock = ( + + + {twoFactorAuthenticationSettings.manualEntrySetupCode} + + ); + + let tfaSection : JSX.Element; + tfaSection = ( +
    + {this.renderToggle("usingTwoFactorAuthentication", this.labelUsingTwoFactorAuthentication)} + {tfaImageBlock} + {this.renderInput("tfaCode", this.labelTfaCode)} +
    + ); + + let passwordSection = {this.renderInput("newPassword", this.labelNewPassword, InputType.password)}; + + if (newPassword !== "") + passwordSection = ( + + {this.renderInput("newPassword", this.labelNewPassword, InputType.password, false, undefined, "", passwordMaxLenght)} + {this.renderInput("confirmPassword", this.labelConfirmPassword, InputType.password, false, undefined, "", passwordMaxLenght)} + + ); + + return ( + +

    Profile

    +
    + {this.renderError("_general")} + {this.renderInput("email", this.labelEmail, InputType.text, true)} + {this.renderInput("firstName", this.labelFirstName)} + {this.renderInput("middleNames", this.labelMiddleNames)} + {this.renderInput("lastName", this.labelLastName)} + + {passwordSection} + + {this.renderDropSection("turnOnTfa", , tfaSection)} +
    + {this.renderButton(this.labelApply)} + +
    + ); + } +} + +export default Profile; diff --git a/src/modules/profile/models/ProfileDetails.ts b/src/modules/profile/models/ProfileDetails.ts new file mode 100644 index 0000000..8b5241a --- /dev/null +++ b/src/modules/profile/models/ProfileDetails.ts @@ -0,0 +1,11 @@ +import { TwoFactorAuthenticationSettings } from "./TwoFactorAuthenticationSettings"; + +export interface ProfileDetails { + firstName: string; + middleNames: string; + lastName: string; + email: string; + password: string; + usingTwoFactorAuthentication: boolean; + twoFactorAuthenticationSettings: TwoFactorAuthenticationSettings; +} diff --git a/src/modules/profile/models/TwoFactorAuthenticationSettings.ts b/src/modules/profile/models/TwoFactorAuthenticationSettings.ts new file mode 100644 index 0000000..08ee324 --- /dev/null +++ b/src/modules/profile/models/TwoFactorAuthenticationSettings.ts @@ -0,0 +1,4 @@ +export interface TwoFactorAuthenticationSettings { + manualEntrySetupCode: string; + qrCodeImageUrl: string; +} diff --git a/src/modules/profile/services/profileService.ts b/src/modules/profile/services/profileService.ts new file mode 100644 index 0000000..daf69bb --- /dev/null +++ b/src/modules/profile/services/profileService.ts @@ -0,0 +1,47 @@ +import httpService from "../../../services/httpService"; +import { ProfileDetails } from "../models/ProfileDetails"; + +const apiEndpoint = "/Profile"; + +export async function getMyProfile(): Promise { + const { data } = await httpService.get(apiEndpoint + "/myProfile"); + + const result: ProfileDetails = { + firstName: data.firstName, + middleNames: data.middleNames, + lastName: data.lastName, + email: data.email, + password: data.password, + usingTwoFactorAuthentication: data.usingTwoFactorAuthentication, + twoFactorAuthenticationSettings: data.twoFactorAuthenticationSettings, + }; + + return result; +} + +export async function putMyProfile( + firstName: string, + middleNames: string, + lastName: string, + email: string, + usingTwoFactorAuthentication: boolean, + securityCode: string, + password: string +) { + return await httpService.put(apiEndpoint + "/myProfile", { + firstName, + middleNames, + lastName, + email, + password, + usingTwoFactorAuthentication, + securityCode, + }); +} + +const profileService = { + getMyProfile, + putMyProfile, +}; + +export default profileService; diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 0000000..ece12df --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts new file mode 100644 index 0000000..49a2a16 --- /dev/null +++ b/src/reportWebVitals.ts @@ -0,0 +1,15 @@ +import { ReportHandler } from 'web-vitals'; + +const reportWebVitals = (onPerfEntry?: ReportHandler) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/services/Paginated.tsx b/src/services/Paginated.tsx new file mode 100644 index 0000000..acfe78f --- /dev/null +++ b/src/services/Paginated.tsx @@ -0,0 +1,7 @@ +export type Paginated = { + count: number, + pageSize : number, + page: number, + totalPages : number, + data : T[]; +} diff --git a/src/services/httpService.ts b/src/services/httpService.ts new file mode 100644 index 0000000..ac6e21f --- /dev/null +++ b/src/services/httpService.ts @@ -0,0 +1,107 @@ +import axios, { InternalAxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from "axios"; +import { isValid, parseISO } from "date-fns"; +import { toast } from "react-toastify"; + +Object.defineProperty(BigInt.prototype, "toJSON", { + get() { + return () => Number(this); + } +}); + +const onRequest = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { + return config; +}; + +const onRequestError = (error: AxiosError): Promise => { + return Promise.reject(error); +}; + +export function handleDates(body: any) { + if (body === null || body === undefined || typeof body !== "object") + return body; + + for (const key of Object.keys(body)) { + const value = body[key]; + if (value !== undefined && value !== "" && isNaN(value)) { + const parsedValue : Date = parseISO(value); + if (isValid(parsedValue)) body[key] = parsedValue; + else if (typeof value === "object") handleDates(value); + } + } +} + +const onResponse = (response: AxiosResponse): AxiosResponse => { + handleDates(response.data); + return response; +}; + +const onResponseError = (error: AxiosError): Promise => { + return Promise.reject(error); +}; + +export function setupInterceptorsTo(axiosInstance: AxiosInstance): AxiosInstance { + axiosInstance.interceptors.request.use(onRequest, onRequestError); + axiosInstance.interceptors.response.use(onResponse, onResponseError); + return axiosInstance; +} + +axios.defaults.baseURL = window.__RUNTIME_CONFIG__.API_URL; +setupInterceptorsTo(axios); + +export function setJwt(jwt: string | null) { + if (jwt) { + axios.defaults.headers.common["Authorization"] = "Bearer " + jwt; + } else { + delete axios.defaults.headers.common["Authorization"]; + } +} + +export function Get(url: string, config?: any): any { + return axios.get(url, config) + .then((response) => { + return response; + }) + .catch((error) => { + toast.error(error.message); + }); +} + +export function Post(url: string, data?: any, config?: any): any { + return axios.post(url, data, config) + .then((response) => { + return response; + }) + .catch((error) => { + toast.error(error.message); + }); +} + +export function Put(url: string, data?: any, config?: any): any { + return axios.put(url, data, config) + .then((response) => { + return response; + }) + .catch((error) => { + toast.error(error.message); + }); +} + +export function Delete(url: string, config?: any): any { + return axios.delete(url, config) + .then((response) => { + return response; + }) + .catch((error) => { + toast.error(error.message); + }); +} + +const httpService = { + get: Get, + post: Post, + put: Put, + delete: Delete, + setJwt, +}; + +export default httpService; diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..1dd407a --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import "@testing-library/jest-dom"; diff --git a/src/types/globals.ts b/src/types/globals.ts new file mode 100644 index 0000000..fa2c950 --- /dev/null +++ b/src/types/globals.ts @@ -0,0 +1,11 @@ +export {}; + +declare global { + interface Window { + __RUNTIME_CONFIG__: { + NODE_ENV: string; + API_URL: string; + EXTERNAL_LOGIN: boolean; + }; + } +} diff --git a/src/utils/GeneralIdRef.tsx b/src/utils/GeneralIdRef.tsx new file mode 100644 index 0000000..3ad7f0c --- /dev/null +++ b/src/utils/GeneralIdRef.tsx @@ -0,0 +1,36 @@ +export interface GeneralIdRef { + id? : bigint + guid? : string; +} + +export function MakeGeneralIdRef( id? : bigint, guid?: string) : GeneralIdRef +{ + let result : GeneralIdRef = {}; + result.id = id; + result.guid = guid; + return result; +} + +export function MakeGeneralIdRefParams( id? : bigint, guid? : string) +{ + let params = ""; + if (id !== undefined) + params += "id="+ id; + + if (guid !== undefined){ + if (params !== ""){ + params += "&" + } + params += "guid=" + guid; + } + + return params +} + +export function GeneralRefIdsMatch( x : GeneralIdRef, y: GeneralIdRef ) +{ + if ( x.id !== null && x.id === y.id) { return true; } + if ( x.guid !== null &&x.guid === y.guid) { return true; } + + return false; +} \ No newline at end of file diff --git a/src/utils/MapToJson.tsx b/src/utils/MapToJson.tsx new file mode 100644 index 0000000..a346378 --- /dev/null +++ b/src/utils/MapToJson.tsx @@ -0,0 +1,11 @@ + +export default function MapToJson ( map? : Map){ + type keyValue = { [key: string]:string }; + + let jsonObject : keyValue = {}; + map?.forEach((value, key) => { + jsonObject[key] = value; + }); + + return JSON.stringify(jsonObject); +} \ No newline at end of file diff --git a/src/utils/deepfind.tsx b/src/utils/deepfind.tsx new file mode 100644 index 0000000..f9d7336 --- /dev/null +++ b/src/utils/deepfind.tsx @@ -0,0 +1,17 @@ +export default function deepFind(obj : any, path : string) { + if (!path) return null; + + var paths = path.split("."), + current = obj, + i; + + for (i = 0; i < paths.length; ++i) { + if (current[paths[i]] === undefined) { + return undefined; + } else { + current = current[paths[i]]; + } + } + + return current; +} diff --git a/src/utils/theme.tsx b/src/utils/theme.tsx new file mode 100644 index 0000000..bbfc0f3 --- /dev/null +++ b/src/utils/theme.tsx @@ -0,0 +1,21 @@ + +const storedTheme = localStorage.getItem('theme') + +const getPreferredTheme = () => { + if (storedTheme) { + return storedTheme + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' +} + +const setTheme = function (theme: string) { + localStorage.setItem('theme', theme) +} + +const theme = { + getPreferredTheme, + setTheme +} + +export default theme \ No newline at end of file diff --git a/src/utils/withRouter.tsx b/src/utils/withRouter.tsx new file mode 100644 index 0000000..4e15e8b --- /dev/null +++ b/src/utils/withRouter.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; + +const useQuery = () => new URLSearchParams(useLocation().search); + +export interface RouterProps{ + router: { + location : any, + navigate : any, + params : any, + query : any + } +} + +/* This is a higher order component that + * inject a special prop to our component. + */ +export default function withRouter(Component: any) { + function ComponentWithRouter(props: any) { + let location = useLocation(); + let navigate = useNavigate(); + let params = useParams(); + let query = useQuery(); + return ; + } + return ComponentWithRouter; +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ed38e30 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve" + }, + "include": [ + "src", + "src/types" + ] +}