Initial commit

This commit is contained in:
Colin Dawson 2026-01-20 21:48:51 +00:00
parent f6c90c76d0
commit 6e3ec1c243
203 changed files with 14818 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.git
.vscode
node_modules
build
Dockerfile

3
.env Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=development
API_URL=http://localhost:3001/api/
EXTERNAL_LOGIN=true

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=development
API_URL=http://localhost:3001/api/
EXTERNAL_LOGIN=true

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* -crlf

226
.gitignore vendored Normal file
View File

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

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
@fortawesome:registry=https://npm.fontawesome.com/
//npm.fontawesome.com/:_authToken=FDC47A4D-82AB-4EDC-887C-4853D3D0AEEA

22
.vscode/launch.json vendored Normal file
View File

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

32
Dockerfile Normal file
View File

@ -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;\""]

70
README.md Normal file
View File

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

54
azure-pipelines.yml Normal file
View File

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

77
config-overrides.js Normal file
View File

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

50
nginx/nginx.conf Normal file
View File

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

84
package.json Normal file
View File

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

2
proxy.cmd Normal file
View File

@ -0,0 +1,2 @@
cd ../e-suite.Proxy/
docker compose up

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

48
public/index.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- Runtime environment variables -->
<script src="%PUBLIC_URL%/runtime-env.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="stylesheet" href="/styles.css">
<title>e-suite</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

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

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

1
public/runtime-env.js Normal file
View File

@ -0,0 +1 @@
window.__RUNTIME_CONFIG__ = {"NODE_ENV":"development","API_URL":"http://localhost:3001/api/","EXTERNAL_LOGIN":"true"};

170
src/App.tsx Normal file
View File

@ -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 ? <Route path="/profile" element={<Redirect to="/account/profile"/>}/>
: <Route path="/profile" element={<Mainframe><Profile /></Mainframe>}/>;
return (
<>
<Route path="/audit/:auditId" element={<Mainframe title="Audit logs"><HOCAudit /></Mainframe>}/>
<Route path="/audit" element={<Mainframe title="Audit logs"><HOCAudit /></Mainframe>} />
<Route path="/blockedIPs" element={<Mainframe title="Blocked IP addresses"><BlockedIPs /></Mainframe>} />
<Route path="/exceptionlogs" element={<Mainframe title="Exception Logs"><ErrorLogs /></Mainframe>} />
<Route path="/specifications/:organisationId/:siteId/add" element={<Mainframe title="Specification Manager"><SpecificationsDetails editMode={false}/></Mainframe>}/>
<Route path="/specifications/:organisationId/:siteId/:specificationId" element={<Mainframe title="Specification Manager"><SpecificationsDetails editMode={true}/></Mainframe>}/>
<Route path="/specifications/:organisationId/:siteId" element={<Mainframe title="Specification Manager"><Specifications /></Mainframe>}/>
<Route path="/site/:organisationId/add" element={<Mainframe title="Site Manager"><SiteDetails editMode={false} /></Mainframe>}/>
<Route path="/site/:organisationId/:siteId" element={<Mainframe title="Site Manager"><SiteDetails editMode={true} /></Mainframe>}/>
<Route path="/site/:organisationId" element={<Mainframe title="Site Manager"><Sites/></Mainframe>}/>
<Route path="/site/" element={<Navigate replace to="/404" />} />
<Route path="/organisations" element={<Mainframe title="e-print"><Organisations /></Mainframe>}/>
<Route path="/organisations/add" element={<Mainframe title="e-print"><HOCOrganisationsDetails editMode={false} /></Mainframe>}/>
<Route path="/organisations/:organisationId" element={<Mainframe title="e-print"><HOCOrganisationsDetails editMode={true}/></Mainframe>}/>
<Route path="/glossaries/add/" element={<Navigate replace to="/404" />} />
<Route path="/glossaries/add/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossariesDetails editMode={false}/></Mainframe>}/>
<Route path="/glossaries/edit/" element={<Navigate replace to="/404" />}/>
<Route path="/glossaries/edit/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossariesDetails editMode={true}/></Mainframe>}/>
<Route path="/glossaries" element={<Mainframe title="Glossary Manager"><HOCGlossaries /></Mainframe>}/>
<Route path="/glossaries/:glossaryId" element={<Mainframe title="Glossary Manager"><HOCGlossaries /></Mainframe>}/>
<Route path="/forms/add" element={<Mainframe title="Form Template Manager"><HOCFormsDetails editMode={false}/></Mainframe>}/>
<Route path="/forms/edit/:formId" element={<Mainframe title="Form Template Manager"><HOCFormsDetails editMode={true} /></Mainframe>}/>
<Route path="/forms" element={<Mainframe title="Form Template Manager"><Forms /></Mainframe>}/>
<Route path="/customfields/add" element={<Mainframe title="Custom Field Manager"><HOCCustomFieldDetails editMode={false}/></Mainframe>}/>
<Route path="/customfields/edit/:customFieldId" element={<Mainframe title="Custom Field Manager"><HOCCustomFieldDetails editMode={true}/></Mainframe>}/>
<Route path="/customfields" element={<Mainframe title="Custom Field Manager"><CustomFields /></Mainframe>}/>
<Route path="/sequence/add" element={<Mainframe title="Sequence Manager"><HOCSequenceDetails editMode={false}/></Mainframe>}/>
<Route path="/sequence/edit/:sequenceId" element={<Mainframe title="Sequence Manager"><HOCSequenceDetails editMode={true}/></Mainframe>}/>
<Route path="/sequence" element={<Mainframe title="Sequence Manager"><Sequence /></Mainframe>}/>
<Route path="/domains/add" element={<Mainframe title="Client Domain Manager"><DomainsDetails editMode={false}/></Mainframe>}/>
<Route path="/domains/edit/:domainId/addRole" element={<Mainframe title="Client Domain Manager"><RolesDetails editMode={false} /></Mainframe>}/>
<Route path="/domains/edit/:domainId/editRole/:roleId" element={<Mainframe title="Client Domain Manager"><RolesDetails editMode={true} /></Mainframe>}/>
<Route path="/domains/edit/:domainId/editRole/:roleId/addUserToRole" element={<Mainframe title="Client Domain Manager"><AddUserToRole editMode={false} /></Mainframe>}/>
<Route path="/domains/edit/:domainId" element={<Mainframe title="Client Domain Manager"><DomainsDetails editMode={true} /></Mainframe>}/>
<Route path="/domains" element={<Mainframe title="Client Domain Manager"><Domains /></Mainframe>}/>
<Route path="/users/add" element={<Mainframe title="User Manager"><UserDetails editMode={false}/></Mainframe>}/>
<Route path="/users/edit/:userId" element={<Mainframe title="User Manager"><UserDetails editMode={true} /></Mainframe>}/>
<Route path="/users" element={<Mainframe title="User Manager"><Users /></Mainframe>}/>
<Route path="/ssoManager" element={<Mainframe title="Sso Manager"><SsoManager /></Mainframe>}/>
<Route path="/ssoManager/add" element={<Mainframe title="Sso Manager"><SsoProviderDetails editMode={false}/></Mainframe>}/>
<Route path="/ssoManager/edit/:ssoProviderId" element={<Mainframe title="Sso Manager"><SsoProviderDetails editMode={true} /></Mainframe>}/>
{profileRoute}
<Route path="/logout" element={<Mainframe><Logout /></Mainframe>}/>
<Route path="/" element={<Mainframe title="e-suite"><HomePage /></Mainframe>}/>
</>
);
}
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() : <Route path="/" element={<Navigate to="/login" />} />;
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 ? <Route path="/login" element={<Redirect to="/account/login"/>}/>
: <Route path="/login" element={<LoginFrame><LoginForm /></LoginFrame>} />;
return (
<HelmetProvider>
<Helmet htmlAttributes={htmlAttributes}>
<title>{config.applicationName}</title>
</Helmet>
<main>
<Routes>
<Route path="/env" element={<EnvPage />} />
{loginRoute}
<Route path="/forgot-password" element={<LoginFrame><ForgotPassword /></LoginFrame>} />
<Route path="/404" element={<LoginFrame><NotFound /></LoginFrame>} />
<Route path="/emailuseraction/:token" element={<LoginFrame><EmailUserAction /></LoginFrame>} />
{secureRoutes}
<Route path="*" element={<Navigate replace to="/404"/>} />
</Routes>
<ToastContainer />
</main>
</HelmetProvider>
);
}
export default App;

118
src/Sass/_ckEditor.scss Normal file
View File

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

19
src/Sass/_domains.scss Normal file
View File

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

View File

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

83
src/Sass/_forms.scss Normal file
View File

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

22
src/Sass/_frame.scss Normal file
View File

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

99
src/Sass/_leftMenu.scss Normal file
View File

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

16
src/Sass/_nav.scss Normal file
View File

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

View File

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

7
src/Sass/general.scss Normal file
View File

@ -0,0 +1,7 @@
.loading {
cursor: wait;
}
.pagination {
padding:0;
}

33
src/Sass/global.scss Normal file
View File

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

View File

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

137
src/Sass/login.scss Normal file
View File

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

View File

@ -0,0 +1,6 @@
.multiSelect {
.multiSelectContainer {
display: inline-flex;
width: 100%;
}
}

60
src/Sass/old/_colors.scss Normal file
View File

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

View File

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

15
src/Sass/old/_fonts.scss Normal file
View File

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

View File

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

91
src/Sass/old/_nav.scss Normal file
View File

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

363
src/Sass/old/_reset.scss Normal file
View File

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

50
src/Sass/old/_sizes.scss Normal file
View File

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

36
src/Sass/old/_table.scss Normal file
View File

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

View File

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

38
src/Sass/pill.scss Normal file
View File

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

10
src/Sass/vars.scss Normal file
View File

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

View File

@ -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<AutocompleteProps, AutocompleteState> {
private inputRef;
constructor(props: AutocompleteProps) {
super(props);
this.state = { filteredOptions: [] }
this.inputRef = React.createRef<HTMLDivElement>();
}
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 (
<div className="autocomplete"
ref={this.inputRef}
onBlur={this.handleBlur}
tabIndex={-1}
>
<input
className="autocomplete-text-input"
type="text"
onChange={(e) => { this.filterOptions(e.target.value) }}
onFocus={(e) => { this.showOptions() }}
placeholder={placeholder}
/>
{filteredOptions.length > 0 && (
<ul className="autocomplete-options">
{filteredOptions.map((x, i) =>
<li
key={x._id}
value={x._id}
tabIndex={0}
>
<button
className="autocomplete-option text-left"
onClick={(e) => {
onSelect(x);
this.setState({ filteredOptions: [] })
}}
>
{x.name}
</button>
</li>
)}
</ul>
)}
</div>
)
}
}

View File

@ -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<T>{
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<T> extends react.Component<ButtonProps<T>> {
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 <Link data-testid={testid} id={id} className={classNames} to={to} >{children}</Link>
}
return <button data-testid={testid} id={id} className={classNames} name={name} disabled={disabled} onClick={this.Click}>{children}</button>;
}
}
export default Button;

View File

@ -0,0 +1,57 @@
import react from 'react';
import Button, { ButtonType } from './Button';
export interface ConfirmButtonProps<T>{
delayMS? : number;
buttonType : ButtonType;
keyValue: T;
children: React.ReactNode;
confirmMessage?: React.ReactNode;
onClick?: ( keyValue? : T ) => void;
}
export interface ConfirmButtonState{
firstClick : boolean
}
class ConfirmButton<T> extends react.Component<ConfirmButtonProps<T>, 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 && <Button buttonType={buttonType} onClick={this.FirstClick}>{children}</Button>}
{firstClick && <Button buttonType={ButtonType.danger} onClick={this.SecondClick}>{confirmMessage!==undefined?confirmMessage:"Are you sure?"}</Button>}
</>
);
}
}
export default ConfirmButton;

View File

@ -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<CustomFieldsEditorProps, CustomFieldsEditorState> {
columns : Column<CustomField>[] = [
{ 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<CustomField> = {
count: 0,
page: 1,
pageSize: 10,
totalPages: 0,
data: value
}
return <div>
{label}
<Table data={ paginated } keyName="id" columns={this.columns} onDelete={onDelete}/>
<div>
<CustomFieldPicker name="customField" label={"Add"} exclude={exclude} value={undefined} onChange={this.handleChange}/>
<Button buttonType={ButtonType.primary} onClick={this.handleAdd} disabled={this.state.id === undefined}>
<FontAwesomeIcon icon={faAdd}/>
</Button>
</div>
</div>;
}
}
export default CustomFieldsEditor;

View File

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

View File

@ -0,0 +1,13 @@
interface ErrorProps {
error?: string
}
function ErrorBlock(props : ErrorProps)
{
const { error } = props;
return ( <>{error && <div className="alert alert-danger">{error}</div>}</> );
}
export default ErrorBlock;

View File

@ -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<P> {
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<P> {
location: LocationProps;
match: Match<P>;
staticContext?: any;
}
class Form<P, FP extends FormProps<P>, FS extends FormState> extends React.Component<FP, FS> {
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<HTMLFormElement>) => {
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<HTMLInputElement>) => {
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<HTMLTextAreaElement>) => {
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<HTMLInputElement>) => {
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<HTMLSelectElement>) => {
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<HTMLInputElement>) => {
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 (
<Button testid={testid} disabled={disabled} name={name ?? label} buttonType={buttonType} onClick={onClick}>
{label}
</Button>
);
}
renderError(name: string) {
const { errors } = this.state;
return <ErrorBlock error={errors[name]} />;
}
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 (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
readOnly
placeHolder={placeHolder}
hidden={!visible}
autoComplete={autoComplete}
/>
);
} else {
return (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
onChange={this.handleChange}
placeHolder={placeHolder}
hidden={!visible}
autoComplete={autoComplete}
/>
);
}
}
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 (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
readOnly
placeHolder={placeHolder}
/>
);
} else {
return (
<Input
includeLabel={true}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
maxLength={maxLength}
onChange={handleChangeEvent}
placeHolder={placeHolder}
/>
);
}
}
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 <Input includeLabel={true} type={InputType.number} name={name} label={label} value={cleanValue} error={errors[name]} readOnly />;
} else {
return (
<Input
includeLabel={true}
type={InputType.number}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
min={min}
max={max}
step={step}
onChange={this.handleChange}
/>
);
}
}
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 (
<Input
includeLabel={includeLabel}
type={InputType.textarea}
key={name}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
readOnly
/>
);
} else {
return (
<Input
includeLabel={includeLabel}
type={InputType.textarea}
key={name}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
onTextAreaChange={this.handleTextAreaChange}
/>
);
}
}
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 <Input includeLabel={includeLabel} type={type} name={name} label={label} value={cleanValue} error={errors[name]} readOnly />;
} else {
return (
<Input
includeLabel={includeLabel}
type={type}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
onChange={this.handleCustomFieldChange}
/>
);
}
}
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 <Input includeLabel={includeLabel} type={InputType.number} name={name} label={label} value={cleanValue} error={errors[name]} readOnly />;
} else {
return (
<Input
includeLabel={includeLabel}
type={InputType.number}
name={name}
label={label}
value={cleanValue}
error={errors[name]}
min={min}
max={max}
step={step}
onChange={this.handleCustomFieldChange}
/>
);
}
}
renderTemplateEditor(className: string, name: string, label: string, allowCustomFields: boolean) {
const { data } = this.state;
let value = data[name] as string;
return (
<div>
<label htmlFor={name}>{label}</label>
<TemplateEditor className={className} name={name} data={value} onChange={this.handleTemplateEditorChange} showFields={allowCustomFields} />
</div>
);
}
renderSelect(name: string, label: string, options: Option[]) {
const { data, errors } = this.state;
return <Select name={name} label={label} value={data[name]} options={options} error={errors[name]} onChange={this.handleSelectChange} />;
}
renderToggle(name: string, label: string) {
const { data, errors } = this.state;
return <ToggleSlider name={name} label={label} defaultChecked={Boolean(data[name])} error={errors[name]} onChange={this.handleToggleChange} />;
}
renderSequencePicker(includeLabel: boolean, name: string, label: string) {
const { data, errors } = this.state;
return (
<SequencePicker includeLabel={includeLabel} name={name} label={label} value={data[name]} error={errors[name]} onChange={this.handlePickerChange} />
);
}
renderGlossaryPicker(includeLabel: boolean, name: string, label: string, maxEntries?: number, refElementId?: GeneralIdRef) {
const { data, errors } = this.state;
const glossaryValues: CustomFieldValue[] | undefined = data[name] as any as CustomFieldValue[];
return (
<GlossaryPicker
includeLabel={includeLabel}
name={name}
label={label}
maxEntries={maxEntries}
values={glossaryValues}
error={errors[name]}
rootItem={refElementId}
onChange={this.handleGlossaryPickerChange}
/>
);
}
renderDomainPicker(includeLabel: boolean, name: string, label: string, minEntries: number, maxEntries?: number) {
const { data, errors } = this.state;
const domainValues: CustomFieldValue[] | undefined = data[name] as any as CustomFieldValue[];
return (
<DomainPicker
includeLabel={includeLabel}
name={name}
label={label}
minEntries={minEntries}
maxEntries={maxEntries}
values={domainValues}
error={errors[name]}
onChange={this.handleDomainPickerChange}
/>
);
}
renderTemplatePicker(includeLabel: boolean, name: string, label: string) {
const { data, errors } = this.state;
const templateValue: GeneralIdRef = data[name] as any as GeneralIdRef;
return (
<FormTemplatePicker
includeLabel={includeLabel}
name={name}
label={label}
value={templateValue}
error={errors[name]}
onChange={this.handleTemplateFormPickerChange}
/>
);
}
renderUserPicker(name: string, label: string) {
const { data, errors } = this.state;
const glossaryValue: GeneralIdRef | undefined = data[name] as any as GeneralIdRef;
return <UserPicker name={name} label={label} value={glossaryValue} error={errors[name]} onChange={this.handleUserPickerChange} />;
}
renderSsoProviderPicker(name: string, label: string) {
const { data, errors } = this.state;
const glossaryValue: GeneralIdRef | undefined = data[name] as any as GeneralIdRef;
return <SsoProviderPicker name={name} label={label} value={glossaryValue} error={errors[name]} onChange={this.handleSsoProviderPickerChange} />;
}
renderCustomFieldsEditor(name: string, label: string, selected: CustomField[], onAdd: CustomFieldEditorAdd, onDelete: CustomFieldEditorDelete) {
const { data, errors } = this.state;
return (
<CustomFieldsEditor name={name} label={label} value={data[name] as []} error={errors[name]} exclude={selected} onAdd={onAdd} onDelete={onDelete} />
);
}
renderCustomFields(customFields: CustomField[] | undefined) {
if (customFields === undefined) return <></>;
let customFieldsBlock: JSX.Element[] = [];
for (const customField of customFields) customFieldsBlock.push(this.renderCustomField(customField, true));
return <>{customFieldsBlock.map((x) => x)}</>;
}
renderCustomField(customField: CustomField, includeLabel: boolean) {
switch (customField.fieldType.toLowerCase()) {
case "text":
const textParameters: textParams = JSON.parse(customField.parameters!);
if (textParameters.multiLine) {
return this.renderInputTextarea(includeLabel, "customfield_" + customField.id, customField.name, false, customField.defaultValue);
} else {
return this.renderCustomFieldInput(
includeLabel,
"customfield_" + customField.id,
customField.name,
InputType.text,
false,
customField.defaultValue
);
}
case "sequence":
return this.renderCustomFieldInput(includeLabel, "customfield_" + customField.id, customField.name, InputType.text, true);
case "formtemplate":
return this.renderTemplatePicker(includeLabel, "customfield_" + customField.id, customField.name);
case "glossary":
return this.renderGlossaryPicker(
includeLabel,
"customfield_" + customField.id,
customField.name,
customField.maxEntries,
customField.refElementId
);
case "number":
const numberParameters: numberParams = JSON.parse(customField.parameters!);
return this.renderCustomFieldNumber(
includeLabel,
"customfield_" + customField.id,
customField.name,
false,
customField.defaultValue,
numberParameters.minValue ? Number(numberParameters.minValue) : undefined,
numberParameters.maxValue ? Number(numberParameters.maxValue) : undefined,
numberParameters.step ? Number(numberParameters.step) : undefined
);
case "domain":
return this.renderDomainPicker(includeLabel, "customfield_" + customField.id, customField.name, customField.minEntries, customField.maxEntries);
default:
return <>{customField.name + " " + customField.fieldType}</>;
}
}
renderDropSection(name: string, title: JSX.Element, content: JSX.Element) {
const { errors } = this.state;
return (
<Expando name={name} title={title} error={errors[name]}>
{content}
</Expando>
);
}
}
export default Form;

View File

@ -0,0 +1,74 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import TabHeader from './TabHeader';
interface HorizontalTabsProps{
children : JSX.Element[];
}
interface HorizontalTabsState{
activeTab : string;
redirect: string;
}
class HorizontalTabs extends React.Component<HorizontalTabsProps, HorizontalTabsState> {
componentDidMount(): void {
this.onClickTabItem(this.props.children[0].props.label);
}
onClickTabItem = (tab : string) => {
let { activeTab } = this.state;
if (activeTab !== tab) {
activeTab = tab;
}
this.setState({ activeTab } );
};
state : HorizontalTabsState= {
activeTab : "",
redirect : ""
}
render() {
const { children } = this.props;
const { activeTab, redirect } = this.state;
if (redirect !== "") return <Navigate to={redirect} />;
const filteredTabs = children.filter( child => child.props.label === activeTab );
const activeTabChildren = (filteredTabs?.length > 0) ? filteredTabs[0].props.children : <></>
if (children?.length === 1) {
return (<>{activeTabChildren}</>)
}
return (
<div className="horizionalTabs">
<div>
<ul className="tab-list">
{children.map((child) => {
const { label } = child.props;
return (
<TabHeader
isActive={label === activeTab}
key={label}
label={label}
onClick={this.onClickTabItem}
/>
);
})}
</ul>
</div>
<div>
{activeTabChildren}
</div>
</div>
);
}
}
export default HorizontalTabs;

View File

@ -0,0 +1,129 @@
import React, { useState } from "react";
import '../../Sass/_forms.scss';
import ErrorBlock from "./ErrorBlock";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
export enum InputType {
button = "button",
checkbox = "checkbox",
color = "color",
date = "date",
datetimelocal = "datetime-local",
email = "email",
file = "file",
hidden = "hidden",
image = "image",
month = "month",
number = "number",
password = "password",
radio = "radio",
range = "range",
reset = "reset",
search = "search",
submit = "submit",
tel = "tel",
text = "text",
textarea = "textarea",
time = "time",
url = "url",
week = "week",
}
export interface InputProps {
includeLabel?: boolean;
name: string;
label: string;
error: string;
placeHolder?: string;
readOnly?: boolean;
type: InputType;
value?: string | number | readonly string[] | undefined;
defaultValue?: string | number | readonly string[] | undefined;
min?: number;
max?: number;
step?: number;
hidden? : boolean;
autoComplete? : string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onTextAreaChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
maxLength?: number;
}
function Input(props: InputProps) {
const { includeLabel, name, label, error, placeHolder, readOnly, type, value, defaultValue, maxLength, hidden, autoComplete, onChange, onTextAreaChange, ...rest } = props;
let showValue = value;
let checked: boolean = false;
let divClassName = "form-group"
let labelClassName = "label";
let className = "form-control";
let flexClassName = "";
const [showPasswordIcon, setShowPasswordIcon] = useState(faEyeSlash);
if (type === InputType.checkbox) {
checked = (value === String(true));
showValue = undefined;
divClassName = "form-check";
className = "form-check-input";
labelClassName += " form-check-label";
flexClassName += "checkbox";
}
if (type === InputType.checkbox) {
divClassName += ' allignedCheckBox';
}
const renderType = (type === InputType.password && showPasswordIcon === faEye) ? InputType.text : type;
const divEyeIconClassName = (readOnly) ? "fullHeight disabledIcon" : "fullHeight";
if (type === InputType.password) {
flexClassName += "flex";
}
return (
<div className={divClassName} hidden={hidden}>
{(includeLabel === true || includeLabel === undefined) && <label className={labelClassName} htmlFor={name} hidden={hidden}>
{label}
</label>}
<div className={flexClassName}>
{type === InputType.textarea &&
<textarea
id={name}
className={className}
name={name}
onChange={onTextAreaChange}
disabled={readOnly}
value={showValue || defaultValue}
autoComplete={autoComplete}>
</textarea>}
{type !== InputType.textarea &&
<input
{...rest}
id={name}
type={renderType}
className={className}
placeholder={placeHolder}
name={name}
onChange={onChange}
disabled={readOnly}
value={showValue}
checked={checked}
defaultValue={defaultValue}
maxLength={maxLength! > 0 ? maxLength: undefined}
autoComplete={autoComplete}
/>}
{type === InputType.password && <div className={divEyeIconClassName} ><FontAwesomeIcon className="passwordIcon" icon={showPasswordIcon} onClick={() => {
const newIcon = showPasswordIcon === faEye ? faEyeSlash : faEye;
setShowPasswordIcon(newIcon)
}
} />
</div>}
</div>
<ErrorBlock error={error}></ErrorBlock>
</div>
);
}
export default Input;

View File

@ -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<Loading2Props, Loading2State> {
state = { loaded : false }
render() {
const { loaded, children } = this.props;
if (!loaded) {
return (<LoadingPanel/>)
}
else {
return (<>{children}</>);
}
}
}
export default Loading;

View File

@ -0,0 +1,10 @@
import { FunctionComponent } from "react";
interface LoadingProps {
}
const LoadingPanel: FunctionComponent<LoadingProps> = () => {
return ( <div>Loading</div> );
}
export default LoadingPanel;

View File

@ -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<HTMLSelectElement>) => void;
onAdd: (item: Option) => void;
onDelete: (item: Option) => void;
}
interface MultiSelectState {
}
class MultiSelect extends React.Component<MultiSelectProps, MultiSelectState> {
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 =>
<Pill key={x._id} pillKey={x._id} displayText={x.name} readOnly={false} onClick={this.handleDelete} />
)
}
</>
}
return (
<div className="form-group multiSelect multiSelectContainer">
{(includeLabel===undefined || includeLabel===true) && <label htmlFor={name}>{label}</label>}
<div className="form-control">
<Autocomplete options={options} selectedOptions={selectedOptions} onSelect={onAdd}/>
{selectedBlock}
</div>
{error && <div className="alert alert-danger">{error}</div>}
</div>
);
}
}
export default MultiSelect;

View File

@ -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<T> {
data : Paginated<T>
onChangePage : (page: number, pageSize: number) => void;
onUnselect?: () => void;
}
interface PaginationState {
}
class Pagination<T> extends React.Component<PaginationProps<T>, 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<HTMLSelectElement>) => {
const input = e.currentTarget;
const { page } = this.props.data;
let newPageSize : number = +input.value;
this.changePage(page, newPageSize);
}
handlePageSelect = ( e : React.ChangeEvent<HTMLInputElement>) =>
{
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 ( <div className="d-flex py-2 bg-body-tertiary pagination">
<span className="px-2">
<select value={data.pageSize} className="form-select" onChange={this.PageSizeChange}>
{pageSizeOptions.map(({ _id, name }) => (
<option key={_id} value={_id}>
{name}
</option>
))}
</select>
</span>
<span>
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickFirst} disabled={data.page < 2}><FontAwesomeIcon icon={faAngleDoubleLeft}/></Button>
<Button className="me-1"buttonType={ButtonType.primary} onClick={this.clickPrevious} disabled={data.page < 2}><FontAwesomeIcon icon={faAngleLeft}/></Button>
<span className="me-1"><input type={InputType.number} value={data.page} min={1} max={data.totalPages} onChange={this.handlePageSelect}/> of {data.totalPages}</span>
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickNext} disabled={data.page >= data.totalPages}><FontAwesomeIcon icon={faAngleRight}/></Button>
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickLast} disabled={data.page >= data.totalPages}><FontAwesomeIcon icon={faAngleDoubleRight}/></Button>
<span className="me-1">{data.count} Items</span>
</span>
</div>);
}
}
export default Pagination;

View File

@ -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<PermissionProps> {
render() {
const { privilegeKey, children } = this.props;
const hasAccess = authenticationService.hasAccess( privilegeKey );
if (hasAccess === false)
return ( <></> );
else
return ( <>{children}</> );
}
}
export default Permission;

View File

@ -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 (
<div className={classNames}>
{displayText}
{!readOnly && (
<button
type="button"
className={`close ${!enabled ? 'd-none': ''}`}
data-dismiss="alert"
aria-label="Close"
onClick={handleOnClick}
>
<FontAwesomeIcon icon={faXmark} />
</button>
)}
</div>
);
}

View File

@ -0,0 +1,21 @@
import React from "react";
interface RedirectProps {
to : string
}
interface RedirectState {
}
class Redirect extends React.Component<RedirectProps, RedirectState> {
render() {
const {to} = this.props;
window.location.replace(to);
return null;
}
}
export default Redirect;

View File

@ -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<HTMLSelectElement>) => 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<SelectProps> {
render() {
const { includeLabel, name, label, error, value, options, includeBlankFirstEntry, onChange, ...rest } = this.props;
const actualValue = GenerateValue( value);
return (
<div className="form-group">
{(includeLabel===undefined || includeLabel===true) && <label htmlFor={name}>{label}</label>}
{!options && <select multiple {...rest} id={name} value={actualValue} className="form-control loading" name={name} onChange={onChange}>
<option value="loading..." />
</select>}
{options &&
<select {...rest} id={name} value={actualValue} className="form-control" name={name} onChange={onChange}>
{includeBlankFirstEntry && <option value=""/>}
{options?.map(({ _id, name }) => (
<option key={_id} value={_id}>
{name}
</option>
))}
</select>
}
{error && <div className="alert alert-danger">{error}</div>}
</div>
);
}
};
export default Select;

View File

@ -0,0 +1,18 @@
import React from 'react';
interface TabProps{
label: string;
children : JSX.Element | JSX.Element[];
}
class Tab extends React.Component<TabProps> {
render() {
return (
<div>
</div>
);
}
}
export default Tab;

View File

@ -0,0 +1,36 @@
import React from 'react';
interface TabHeaderProps{
isActive : boolean,
label : string
onClick : any;
}
class TabHeader extends React.Component<TabHeaderProps> {
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 (
<li className={className} onClick={onClick}>
{label}
</li>
);
}
}
export default TabHeader;

View File

@ -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<T> {
data: Paginated<T>,
sortColumn? : Column<T>,
selectedRow? : T;
onChangePage? : (page: number, pageSize : number) => {};
onSort? : (sortColumn : Column<T>) => 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<T> extends PublishedTableProps<T> {
keyName : string;
columns : Column<T>[];
editPath? : string;
onAuditParams?: ( item : T ) => AuditParams;
secondaryAudit? : boolean;
}
interface TableState{
debouncedOnSearch?: any
}
class Table<T> extends Component<TableProps<T>, 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 (
<>
<table className='table table-sm table-striped table-bordered'>
<TableHeader columns={columns} sortColumn={sortColumn} showDelete={showDelete} showEdit={showEdit} showAudit={showAudit} showSecondaryAudit={showSecondaryAudit} onSort={onSort} onSearch={debouncedOnSearch} />
<TableBody data={data.data} keyName={keyName} columns={columns} selectedRow={selectedRow} canEdit={canEdit} canDelete={canDelete} onDelete={onDelete} editPath={editPath} onAuditParams={onAuditParams} onSelectRow={onSelectRow} showSecondaryAudit={showSecondaryAudit}/>
<TableFooter data={data} columns={columns} showDelete={showDelete} showEdit={showEdit} showAudit={showAudit} showSecondaryAudit={showSecondaryAudit} onChangePage={onChangePage} onUnselectRow={onUnselectRow} />
</table>
</>
);
}
}
export default Table;

View File

@ -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<T>{
data : T[] | undefined;
keyName : string;
columns : Column<T>[];
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<T> extends Component<TableBodyProps<T>> {
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<T>) => {
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 = <DateView value={foundItem}/>
}
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 = <Link to={resolvedlinkPath}>{columnContent}</Link>;
}
return <>
{columnContent}
</>;
};
clickRow = ( value : T ) =>
{
const { onSelectRow } = this.props;
if (onSelectRow !== undefined)
onSelectRow( value );
}
createKey = (item : T, column : Column<T>) => {
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 (
<tbody>
{data?.map((item) => {
let classNames = "";
if (selectedRow === item)
{
classNames+="table-primary";
}
return (<tr className={classNames} key={(item as any)[keyName]}>
{columns.map((column) => (
<td key={this.createKey(item, column)} onClick={ () => this.clickRow(item)}>{this.renderCell(item, column)}</td>
))}
{showEdit && <td className="align-middle">{(canEdit === undefined || canEdit(item)) && <Button buttonType={ButtonType.primary} to={this.resolvePath( editPath!, [ (item as any)[keyName] ] )}><FontAwesomeIcon icon={faEdit}/></Button>}</td>}
{showDelete && <td className="align-middle">{(canDelete === undefined || canDelete(item)) && <ConfirmButton buttonType={ButtonType.primary} keyValue={item} onClick={onDelete} confirmMessage={"Press again to delete"} ><FontAwesomeIcon icon={faTrash}/></ConfirmButton>}</td>}
{showAudit && <td className="align-middle"><Link to={this.handleAuditParams(item, true)}><Button buttonType={ButtonType.primary}><FontAwesomeIcon icon={faBook}/></Button></Link></td>}
{showAudit && showSecondaryAudit && <td className="align-middle"><Link to={this.handleAuditParams(item, false)}><Button buttonType={ButtonType.secondary}><FontAwesomeIcon icon={faBookJournalWhills}/></Button></Link></td>}
</tr>)
})}
</tbody>
);
}
}
export default TableBody;

View File

@ -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<T>{
data : Paginated<T>;
columns : Column<T>[];
showEdit : boolean;
showDelete : boolean;
showAudit : boolean;
showSecondaryAudit : boolean;
onChangePage?: (page: number, pageSize: number) => void;
onUnselectRow?: () => void;
}
class TableFooter<T> extends Component<TableFooterProps<T>> {
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 : <Pagination data={data} onChangePage={onChangePage} onUnselect={onUnselectRow} />;
if (pagination)
return <tfoot>
<tr>
<td colSpan={columns.length+staticColumnCount}>{pagination}</td>
</tr>
</tfoot>
return <></>
}
}
export default TableFooter;

View File

@ -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<T>{
sortColumn? : Column<T>;
columns : Column<T>[];
showDelete? : boolean;
showEdit? : boolean;
showAudit? : boolean;
showSecondaryAudit? : boolean;
onSort? : (sortColumn : Column<T>) => void;
onSearch?: ( name: string, value: string) => void;
}
class TableHeader<T> extends Component<TableHeaderProps<T>> {
columnsMatch = ( left? : Column<T>, right? : Column<T>) =>
{
if (left?.key !== right?.key) return false;
return true;
}
raiseSort = (column : Column<T>) => {
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<T>) => {
const { sortColumn } = this.props;
if (!sortColumn) return null;
if (!this.columnsMatch(column, sortColumn)) return null;
if (sortColumn?.order === "asc") return <FontAwesomeIcon icon={faSortAsc}/>
return <FontAwesomeIcon icon={faSortDesc}/>
};
changeSearch = (e: ChangeEvent<HTMLInputElement>) =>
{
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 = <tr>
{columns.map((column) =>
<th key={column.path || column.key}>
{
(column.searchable === undefined || column.searchable === true ) &&
<Input name={column.path || column.key} label={""} error={""} type={InputType.text} onChange={this.changeSearch} />
}
</th>
)}
{showEdit && <th></th>}
{showDelete && <th></th>}
{showAudit && <th></th>}
{showAudit && showSecondaryAudit && <th></th>}
</tr>;
return (
<thead>
<tr>
{columns.map((column) =>
<th className="text-nowrap" key={column.path || column.key} scope="col" onClick={() => this.raiseSort(column)}>
{column.label} {this.renderSortIcon(column)}
</th>
)}
{showEdit && <th scope="col"></th>}
{showDelete && <th scope="col"></th>}
{showAudit && <th scope="col"></th>}
{showAudit && showSecondaryAudit && <th scope="col"></th>}
</tr>
{searchRow}
</thead>
);
}
}
export default TableHeader;

View File

@ -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<customfieldType>;
ready : boolean;
}
class TemplateEditor extends React.Component<TemplateEditorProps, TemplateEditorState> {
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 <TextEditor className={className} name={name} data={data} onChange={onChange} customFields={customfields} />;
}
}
export default TemplateEditor;

View File

@ -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<TemplateFillerProps, any, TemplateFillerState> {
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<TemplateFillerProps>, prevState: Readonly<TemplateFillerState>, 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 <div className="p">{domToReact(domNodeAsAny.children, options)}</div>
}
}
}
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 (
<Loading loaded={loaded}>
<div className="ck-content form-editor">
{parsedDefinition}
</div>
</Loading>
);
}
}
export default TemplateFiller;

View File

@ -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<ToggleSliderProps> {
render() {
const { name, label, error, readOnly, defaultChecked, ...rest } = this.props;
return (
<div className="form-group">
<label htmlFor={name}>{label}</label>
<Toggle {...rest} id={name} name={name} defaultChecked={defaultChecked} />
{error && <div className="alert alert-danger">{error}</div>}
</div>
);
}
}
export default ToggleSlider;

View File

@ -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 (
<div>
<div className="ck-main-container">
<div className="editor-container editor-container_document-editor" ref={editorContainerRef}>
<div className="editor-container__menu-bar" ref={editorMenuBarRef}></div>
<div className="editor-container__toolbar" ref={editorToolbarRef}></div>
<div className="editor-container__editor-wrapper">
<div className={editorClasses}>
<div ref={editorRef}>
{isLayoutReady && (
<CKEditor
onReady={editor => {
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 } );
} }
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
export default interface Column<T>{
key : string,
path? : string,
label : string,
link? : string,
order? : string,
searchable? : boolean,
content? : (item : T) => JSX.Element
}

View File

@ -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<ExpandoProps, ExpandoState> {
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 ( <div>
<Button buttonType={ButtonType.secondary} onClick={this.DropDownClick}>{title} <FontAwesomeIcon icon={faCaretDown}/></Button>
</div> );
}
else {
return ( <div>
<Button buttonType={ButtonType.secondary} onClick={this.CloseUpClick}>{title} <FontAwesomeIcon icon={faCaretUp}/></Button>
{children}
</div> );
}
}
}
export default Expando;

View File

@ -0,0 +1,6 @@
import { GeneralIdRef } from "../../utils/GeneralIdRef";
export default interface Option {
_id: string | number;
name: string;
}

View File

@ -0,0 +1,3 @@
tr.selected {
background-color: lightblue;
}

View File

@ -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<CustomFieldPickerProps, CustomFieldPickerState> {
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<HTMLSelectElement>) => {
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 (
<Select name={name} label={label} error={error} value={value?.id} options={filteredOptions} includeBlankFirstEntry={true} onChange={this.handleChange} />
);
}
}
export default CustomFieldPicker;

View File

@ -0,0 +1,144 @@
import React from "react";
import Select from "../common/Select";
import Option from "../common/option";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
import domainsService from "../../modules/manager/domains/serrvices/domainsService";
import { CustomFieldValue } from "../../modules/manager/glossary/services/glossaryService";
import MultiSelect from "../common/MultiSelect";
interface DomainPickerProps {
includeLabel?: boolean;
name: string;
label: string;
error?: string;
values: CustomFieldValue[];
minEntries?: number;
maxEntries?: number;
onChange?: (name: string, value: CustomFieldValue[]) => void;
}
interface DomainPickerState {
options?: Option[];
selectedOptions: Option[];
}
class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState> {
state = { options: [] as Option[], selectedOptions: [] as Option[] };
async componentDidMount() {
const { values } = this.props;
const pagedData = await domainsService.getDomains(0, 10, "name", true);
if (pagedData) {
const options: Option[] | undefined = pagedData.data.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<HTMLSelectElement>) => {
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, values, maxEntries } = this.props;
const { options, selectedOptions } = this.state;
// return (
// <Select
// name={name}
// label={label}
// error={error}
// value={String(value?.id)}
// options={options}
// includeBlankFirstEntry={false}
// onChange={this.handleChange}
// />
// );
if (maxEntries == 1) {
let value = selectedOptions[0]?._id;
return (
<Select
includeLabel={includeLabel}
name={name}
label={label}
error={error}
value={value}
options={options}
includeBlankFirstEntry={true}
onChange={this.handleChange}
/>
);
} else {
return (
<MultiSelect
includeLabel={includeLabel}
name={name}
label={label}
error={error}
options={options}
selectedOptions={selectedOptions}
onAdd={this.handleAdd}
onDelete={this.handleDelete}
></MultiSelect>
);
}
}
}
export default DomainPicker;

View File

@ -0,0 +1,64 @@
import React from "react";
import Select from "../common/Select";
import Option from "../common/option";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
import formsService from "../../modules/manager/forms/services/formsService";
interface FormTemplatePickerProps {
includeLabel? : boolean;
name: string;
label: string;
error?: string;
value?: GeneralIdRef;
onChange?: (name: string, value: GeneralIdRef) => void;
}
interface FormTemplatePickerState {
options?: Option[];
}
class FormTemplatePicker extends React.Component<FormTemplatePickerProps, FormTemplatePickerState> {
state = { options: [] as Option[] };
async componentDidMount() {
const formTemplates = await formsService.getForms(0,10,"name",true);
if (formTemplates) {
const options: Option[] | undefined = (formTemplates.data as any[]).map((x) => {
return {
_id: x.id,
name: x.name,
};
});
this.setState({ options });
}
}
doOnChange = (name: string, value: bigint) => {
const { onChange } = this.props;
const generalIdRef = MakeGeneralIdRef(value);
if (onChange) onChange(name, generalIdRef);
};
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget;
this.doOnChange(input.name, BigInt(input.value));
};
render() {
const { includeLabel, name, label, error, value } = this.props;
const { options } = this.state;
let id = "";
if (!((value === undefined || Number.isNaN(value.id) ))) {
id = String(value.id);
}
return (
<Select includeLabel={includeLabel} name={name} label={label} error={error} value={id} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
);
}
}
export default FormTemplatePicker;

View File

@ -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<GlossaryPickerProps, GlossaryPickerState> {
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<HTMLSelectElement>) => {
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 (
<Select
includeLabel={includeLabel}
name={name}
label={label}
error={error}
value={value}
options={options}
includeBlankFirstEntry={true}
onChange={this.handleChange}
/>
);
} else {
return (
<MultiSelect
includeLabel={includeLabel}
name={name}
label={label}
error={error}
options={options}
selectedOptions={selectedOptions}
onAdd={this.handleAdd}
onDelete={this.handleDelete}
></MultiSelect>
);
}
}
}
export default GlossaryPicker;

View File

@ -0,0 +1,58 @@
import React from "react";
import sequenceService from "../../modules/manager/sequence/services/sequenceService";
import Select from "./../common/Select";
import Option from "../common/option";
import { GeneralIdRef } from "./../../utils/GeneralIdRef";
interface SequencePickerProps {
includeLabel? : boolean;
name: string;
label: string;
error?: string;
value: any;
onChange?: (name: string, value: GeneralIdRef) => void;
}
interface SequencePickerState {
options?: Option[];
}
class SequencePicker extends React.Component<SequencePickerProps, SequencePickerState> {
state = { options: undefined };
async componentDidMount() {
const pagedData = await sequenceService.getSequences(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 });
}
}
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { onChange } = this.props;
const input = e.currentTarget;
const generalIdRef: GeneralIdRef = {
id: BigInt(input.value),
};
if (onChange) onChange(input.name, generalIdRef);
};
render() {
const { includeLabel, name, label, error, value } = this.props;
const { options } = this.state;
return (
<Select includeLabel={includeLabel} name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
);
}
}
export default SequencePicker;

View File

@ -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<SsoProviderPickerProps, SsoProviderPickerState> {
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<HTMLSelectElement>) => {
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 (
<Select name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
);
}
}
export default SsoProviderPicker;

View File

@ -0,0 +1,58 @@
import React from "react";
import Select from "./../common/Select";
import Option from "../common/option";
import { GeneralIdRef } from "./../../utils/GeneralIdRef";
import userService from "../../modules/manager/users/services/usersService";
interface UserPickerProps {
name: string;
label: string;
error?: string;
value: any;
domain? : GeneralIdRef;
onChange?: (name: string, value: GeneralIdRef) => void;
}
interface UserPickerState {
options?: Option[];
}
class UserPicker extends React.Component<UserPickerProps, UserPickerState> {
state = { options: undefined };
async componentDidMount() {
const pagedData = await userService.getUsers(0, 10, "name", true);
if (pagedData) {
const options: Option[] = (pagedData.data as any[]).map(x => {
return {
_id: x.id,
name: x.displayName,
};
});
this.setState({ options });
}
}
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<Select name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} />
);
}
}
export default UserPicker;

3
src/config.json Normal file
View File

@ -0,0 +1,3 @@
{
"applicationName":"e-suite"
}

1
src/img/E-SUITE_logo.svg Normal file
View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 460.03 445"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:url(#linear-gradient-3);}.cls-4{fill:url(#linear-gradient-4);}</style><linearGradient id="linear-gradient" x1="310.38" y1="222.5" x2="460.03" y2="222.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0d7164"/><stop offset="1" stop-color="#29b884"/></linearGradient><linearGradient id="linear-gradient-2" x1="68.62" y1="420.94" x2="498.46" y2="420.94" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="76.45" y1="280.41" x2="498.48" y2="280.41" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#29b884"/><stop offset="1" stop-color="#27a07a"/></linearGradient><linearGradient id="linear-gradient-4" x1="157.22" y1="510.74" x2="492.54" y2="510.74" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#29b884"/><stop offset="1" stop-color="#29b884"/></linearGradient></defs><rect class="cls-1" x="310.38" y="196" width="149.65" height="53"/><path class="cls-2" d="M291.36,555.59a134.32,134.32,0,1,1,107-215.52H498.46c-32.36-82.89-113-141.63-207.34-141.63-122.88,0-222.5,99.62-222.5,222.5s99.62,222.5,222.5,222.5c94.31,0,174.9-58.67,207.29-141.49H398.76A134.12,134.12,0,0,1,291.36,555.59Z" transform="translate(-68.62 -198.44)"/><path class="cls-3" d="M272.63,247c56.88,8.74,103.3,44.45,129.63,93.11h96.22c-32.36-82.89-113-141.63-207.34-141.63-102.6,0-189,69.47-214.69,163.93C109.37,283.65,189,234.09,272.63,247Z" transform="translate(-68.62 -198.44)"/><path class="cls-4" d="M492.54,502.14h-93.6a134.32,134.32,0,0,1-241.71-80.2c0,.16,0,.32,0,.48a177.14,177.14,0,0,0,335.32,79.72Z" transform="translate(-68.62 -198.44)"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

15
src/img/logo.tsx Normal file
View File

@ -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<LogoProps> = (props:LogoProps) => {
return ( <img className={props.className} height={props.height} width={props.width} alt={props.alt} src={logo}/>);
}
export default Logo;

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460.03 445"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="310.38" y="196" width="149.65" height="53"/><path class="cls-1" d="M291.36,555.59a134.32,134.32,0,1,1,107-215.52H498.46c-32.36-82.89-113-141.63-207.34-141.63-122.88,0-222.5,99.62-222.5,222.5s99.62,222.5,222.5,222.5c94.31,0,174.9-58.67,207.29-141.49H398.76A134.12,134.12,0,0,1,291.36,555.59Z" transform="translate(-68.62 -198.44)"/></svg>

After

Width:  |  Height:  |  Size: 504 B

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