Initial commit
This commit is contained in:
parent
f6c90c76d0
commit
6e3ec1c243
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
.git
|
||||
.vscode
|
||||
node_modules
|
||||
build
|
||||
Dockerfile
|
||||
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
||||
NODE_ENV=development
|
||||
API_URL=http://localhost:3001/api/
|
||||
EXTERNAL_LOGIN=true
|
||||
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
NODE_ENV=development
|
||||
API_URL=http://localhost:3001/api/
|
||||
EXTERNAL_LOGIN=true
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* -crlf
|
||||
226
.gitignore
vendored
Normal file
226
.gitignore
vendored
Normal 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
2
.npmrc
Normal 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
22
.vscode/launch.json
vendored
Normal 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
32
Dockerfile
Normal 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
70
README.md
Normal 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
54
azure-pipelines.yml
Normal 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
77
config-overrides.js
Normal 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
50
nginx/nginx.conf
Normal 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
84
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
48
public/index.html
Normal file
48
public/index.html
Normal 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
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal 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
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
1
public/runtime-env.js
Normal file
1
public/runtime-env.js
Normal 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
170
src/App.tsx
Normal 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
118
src/Sass/_ckEditor.scss
Normal 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
19
src/Sass/_domains.scss
Normal 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;
|
||||
}
|
||||
32
src/Sass/_esuiteVariables.scss
Normal file
32
src/Sass/_esuiteVariables.scss
Normal 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
83
src/Sass/_forms.scss
Normal 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
22
src/Sass/_frame.scss
Normal 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
99
src/Sass/_leftMenu.scss
Normal 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
16
src/Sass/_nav.scss
Normal 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;
|
||||
}
|
||||
|
||||
70
src/Sass/autoComplete.scss
Normal file
70
src/Sass/autoComplete.scss
Normal 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
7
src/Sass/general.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.loading {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
padding:0;
|
||||
}
|
||||
33
src/Sass/global.scss
Normal file
33
src/Sass/global.scss
Normal 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;
|
||||
}
|
||||
19
src/Sass/horizionalTabs.scss
Normal file
19
src/Sass/horizionalTabs.scss
Normal 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
137
src/Sass/login.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
6
src/Sass/multiSelect.scss
Normal file
6
src/Sass/multiSelect.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.multiSelect {
|
||||
.multiSelectContainer {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
60
src/Sass/old/_colors.scss
Normal file
60
src/Sass/old/_colors.scss
Normal 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;
|
||||
27
src/Sass/old/_container.scss
Normal file
27
src/Sass/old/_container.scss
Normal 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
15
src/Sass/old/_fonts.scss
Normal 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;
|
||||
45
src/Sass/old/_formfields.scss
Normal file
45
src/Sass/old/_formfields.scss
Normal 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
91
src/Sass/old/_nav.scss
Normal 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
363
src/Sass/old/_reset.scss
Normal 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
50
src/Sass/old/_sizes.scss
Normal 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
36
src/Sass/old/_table.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
29
src/Sass/old/loginredundant.scss
Normal file
29
src/Sass/old/loginredundant.scss
Normal 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
38
src/Sass/pill.scss
Normal 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
10
src/Sass/vars.scss
Normal 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;
|
||||
118
src/components/common/AutoComplete.tsx
Normal file
118
src/components/common/AutoComplete.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
93
src/components/common/Button.tsx
Normal file
93
src/components/common/Button.tsx
Normal 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;
|
||||
57
src/components/common/ConfirmButton.tsx
Normal file
57
src/components/common/ConfirmButton.tsx
Normal 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;
|
||||
99
src/components/common/CustomFieldsEditor.tsx
Normal file
99
src/components/common/CustomFieldsEditor.tsx
Normal 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;
|
||||
9
src/components/common/DateView.tsx
Normal file
9
src/components/common/DateView.tsx
Normal 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)}</>
|
||||
}
|
||||
13
src/components/common/ErrorBlock.tsx
Normal file
13
src/components/common/ErrorBlock.tsx
Normal 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;
|
||||
|
||||
957
src/components/common/Form.tsx
Normal file
957
src/components/common/Form.tsx
Normal 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;
|
||||
74
src/components/common/HorizionalTabs.tsx
Normal file
74
src/components/common/HorizionalTabs.tsx
Normal 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;
|
||||
129
src/components/common/Input.tsx
Normal file
129
src/components/common/Input.tsx
Normal 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;
|
||||
27
src/components/common/Loading.tsx
Normal file
27
src/components/common/Loading.tsx
Normal 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;
|
||||
10
src/components/common/LoadingPanel.tsx
Normal file
10
src/components/common/LoadingPanel.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
interface LoadingProps {
|
||||
}
|
||||
|
||||
const LoadingPanel: FunctionComponent<LoadingProps> = () => {
|
||||
return ( <div>Loading</div> );
|
||||
}
|
||||
|
||||
export default LoadingPanel;
|
||||
69
src/components/common/MultiSelect.tsx
Normal file
69
src/components/common/MultiSelect.tsx
Normal 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;
|
||||
109
src/components/common/Pagination.tsx
Normal file
109
src/components/common/Pagination.tsx
Normal 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;
|
||||
22
src/components/common/Permission.tsx
Normal file
22
src/components/common/Permission.tsx
Normal 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;
|
||||
52
src/components/common/Pill.tsx
Normal file
52
src/components/common/Pill.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
src/components/common/Redirect.tsx
Normal file
21
src/components/common/Redirect.tsx
Normal 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;
|
||||
55
src/components/common/Select.tsx
Normal file
55
src/components/common/Select.tsx
Normal 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;
|
||||
18
src/components/common/Tab.tsx
Normal file
18
src/components/common/Tab.tsx
Normal 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;
|
||||
36
src/components/common/TabHeader.tsx
Normal file
36
src/components/common/TabHeader.tsx
Normal 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;
|
||||
70
src/components/common/Table.tsx
Normal file
70
src/components/common/Table.tsx
Normal 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;
|
||||
138
src/components/common/TableBody.tsx
Normal file
138
src/components/common/TableBody.tsx
Normal 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;
|
||||
40
src/components/common/TableFooter.tsx
Normal file
40
src/components/common/TableFooter.tsx
Normal 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;
|
||||
103
src/components/common/TableHeader.tsx
Normal file
103
src/components/common/TableHeader.tsx
Normal 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;
|
||||
66
src/components/common/TemplateEditor.tsx
Normal file
66
src/components/common/TemplateEditor.tsx
Normal 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;
|
||||
184
src/components/common/TemplateFiller.tsx
Normal file
184
src/components/common/TemplateFiller.tsx
Normal 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;
|
||||
26
src/components/common/ToggleSlider.tsx
Normal file
26
src/components/common/ToggleSlider.tsx
Normal 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;
|
||||
356
src/components/common/ckeditor/TextEditor.jsx
Normal file
356
src/components/common/ckeditor/TextEditor.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 ];
|
||||
}
|
||||
}
|
||||
@ -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 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
15
src/components/common/ckeditor/plugins/abbreviation/utils.js
Normal file
15
src/components/common/ckeditor/plugins/abbreviation/utils.js
Normal 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;
|
||||
}, '' );
|
||||
}
|
||||
9
src/components/common/ckeditor/plugins/field/field.ts
Normal file
9
src/components/common/ckeditor/plugins/field/field.ts
Normal 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 ];
|
||||
}
|
||||
}
|
||||
38
src/components/common/ckeditor/plugins/field/fieldcommand.js
Normal file
38
src/components/common/ckeditor/plugins/field/fieldcommand.js
Normal 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;
|
||||
}
|
||||
}
|
||||
119
src/components/common/ckeditor/plugins/field/fieldediting.js
Normal file
119
src/components/common/ckeditor/plugins/field/fieldediting.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/components/common/ckeditor/plugins/field/fieldui.js
Normal file
67
src/components/common/ckeditor/plugins/field/fieldui.js
Normal 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;
|
||||
}
|
||||
12
src/components/common/ckeditor/plugins/field/styles.css
Normal file
12
src/components/common/ckeditor/plugins/field/styles.css
Normal 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;
|
||||
}
|
||||
9
src/components/common/columns.tsx
Normal file
9
src/components/common/columns.tsx
Normal 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
|
||||
}
|
||||
48
src/components/common/expando.tsx
Normal file
48
src/components/common/expando.tsx
Normal 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;
|
||||
6
src/components/common/option.tsx
Normal file
6
src/components/common/option.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { GeneralIdRef } from "../../utils/GeneralIdRef";
|
||||
|
||||
export default interface Option {
|
||||
_id: string | number;
|
||||
name: string;
|
||||
}
|
||||
3
src/components/common/table.css
Normal file
3
src/components/common/table.css
Normal file
@ -0,0 +1,3 @@
|
||||
tr.selected {
|
||||
background-color: lightblue;
|
||||
}
|
||||
94
src/components/pickers/CustomFieldPicker.tsx
Normal file
94
src/components/pickers/CustomFieldPicker.tsx
Normal 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;
|
||||
144
src/components/pickers/DomainPicker.tsx
Normal file
144
src/components/pickers/DomainPicker.tsx
Normal 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;
|
||||
64
src/components/pickers/FormTemplatePicker.tsx
Normal file
64
src/components/pickers/FormTemplatePicker.tsx
Normal 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;
|
||||
136
src/components/pickers/GlossaryPicker.tsx
Normal file
136
src/components/pickers/GlossaryPicker.tsx
Normal 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;
|
||||
58
src/components/pickers/SequencePicker.tsx
Normal file
58
src/components/pickers/SequencePicker.tsx
Normal 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;
|
||||
58
src/components/pickers/SsoProviderPicker.tsx
Normal file
58
src/components/pickers/SsoProviderPicker.tsx
Normal 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;
|
||||
58
src/components/pickers/UserPicker.tsx
Normal file
58
src/components/pickers/UserPicker.tsx
Normal 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
3
src/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"applicationName":"e-suite"
|
||||
}
|
||||
1
src/img/E-SUITE_logo.svg
Normal file
1
src/img/E-SUITE_logo.svg
Normal 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
15
src/img/logo.tsx
Normal 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;
|
||||
1
src/img/logo_esuite-white.svg
Normal file
1
src/img/logo_esuite-white.svg
Normal 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
Loading…
Reference in New Issue
Block a user