Merge branch 'main' into feature/interstitial_process_instance_show
|
@ -50,12 +50,15 @@ jobs:
|
|||
session: "tests",
|
||||
database: "sqlite",
|
||||
}
|
||||
- {
|
||||
python: "3.10",
|
||||
os: "windows-latest",
|
||||
session: "tests",
|
||||
database: "sqlite",
|
||||
}
|
||||
# FIXME: tests cannot pass on windows and we currently cannot debug
|
||||
# since none of us have a windows box that can run the python app.
|
||||
# so ignore windows tests until we can get it fixed.
|
||||
# - {
|
||||
# python: "3.10",
|
||||
# os: "windows-latest",
|
||||
# session: "tests",
|
||||
# database: "sqlite",
|
||||
# }
|
||||
- {
|
||||
python: "3.11",
|
||||
os: "macos-latest",
|
||||
|
|
|
@ -59,6 +59,8 @@ jobs:
|
|||
uses: docker/metadata-action@v4.4.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
labels: |
|
||||
org.opencontainers.image.description=Frontend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=-latest
|
||||
type=ref,event=branch,suffix=-${{ steps.date.outputs.date }}
|
||||
|
@ -103,6 +105,8 @@ jobs:
|
|||
uses: docker/metadata-action@v4.4.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
labels: |
|
||||
org.opencontainers.image.description=Backend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams
|
||||
tags: |
|
||||
type=ref,event=branch,suffix=-latest
|
||||
type=ref,event=branch,suffix=-${{ steps.date.outputs.date }}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "19"
|
||||
# rust: "1.64"
|
||||
# golang: "1.19"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||
# formats:
|
||||
# - pdf
|
||||
|
||||
# Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
|
@ -49,20 +49,23 @@ pipeline {
|
|||
stages {
|
||||
stage('Prep') {
|
||||
steps { script {
|
||||
def jobMetaJson = new JsonBuilder([
|
||||
git_commit: env.GIT_COMMIT.take(7),
|
||||
git_branch: env.GIT_BRANCH,
|
||||
build_id: env.BUILD_ID,
|
||||
]).toPrettyString()
|
||||
sh "echo '${jobMetaJson}' > version_info.json"
|
||||
dir("spiffworkflow-${params.COMPONENT}") {
|
||||
def jobMetaJson = new JsonBuilder([
|
||||
git_commit: env.GIT_COMMIT.take(7),
|
||||
git_branch: env.GIT_BRANCH,
|
||||
build_id: env.BUILD_ID,
|
||||
]).toPrettyString()
|
||||
sh "echo '${jobMetaJson}' > version_info.json"
|
||||
}
|
||||
} }
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
steps { script {
|
||||
dir("spiffworkflow-${params.COMPONENT}") {
|
||||
/* Tag and Commit is combined to avoid clashes of parallel builds. */
|
||||
image = docker.build(
|
||||
"${params.DOCKER_NAME}:${env.GIT_COMMIT.take(8)}",
|
||||
"${params.DOCKER_NAME}:${params.DOCKER_TAG}-${env.GIT_COMMIT.take(8)}",
|
||||
"--label=commit='${env.GIT_COMMIT.take(8)}' ."
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
_build
|
||||
.venv
|
||||
.vscode
|
|
@ -0,0 +1,20 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -0,0 +1,35 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'SpiffWorkflow'
|
||||
copyright = '2023, Sartography'
|
||||
author = 'Sartography' # Very ok to add your name here.
|
||||
release = '0.1'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = ['myst_parser']
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.venv']
|
||||
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
#html_theme = 'alabaster'
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_static_path = ['static']
|
||||
html_logo = "spiffworkflow_logo.png"
|
||||
html_theme_options = {
|
||||
'logo_only': True,
|
||||
'display_version': False,
|
||||
}
|
||||
html_css_files = ["custom.css"]
|
|
@ -0,0 +1,131 @@
|
|||
# How to Contribute to the Documentation
|
||||
|
||||
This documentation is currently hosted live at [Spiff-Arena's ReadTheDocs](https://spiff-arena.readthedocs.io/en/latest/)
|
||||
|
||||
|
||||
Please set aside a couple of hours to work through this, as getting this setup correctly once is 10,000 times better than having problems every day for the rest of your life.
|
||||
|
||||
## Our Methodology
|
||||
|
||||
The methodology we are following is knowns as ["Docs as Code"](https://www.writethedocs.org/guide/docs-as-code/)
|
||||
|
||||
This means using the same tools and processes that software developers use for writing code to write the documenation for code.
|
||||
In following this methodoloy, you will have to pick up some tools you haven't had to use before (Git, Sphinx).
|
||||
Why would a technical writer need to learn these software engineering tools?
|
||||
I'll never make the case as well as an article by [Tom Johnson](https://idratherbewriting.com/trends/trends-to-follow-or-forget-docs-as-code.html).
|
||||
|
||||
You might notice, when looking at the markdown files, that every sentence starts on a new line.
|
||||
Like this one.
|
||||
Unless there is a blank line between sentences, Markdown will still render this as a paragraph.
|
||||
This is called [Ventilated Code](https://vanemden.wordpress.com/2009/01/01/ventilated-prose/) and can be very helpful when working in Markdown.
|
||||
|
||||
|
||||
## Our Tools
|
||||
|
||||
[Markdown](https://www.markdownguide.org/getting-started/) is a "markup language that you can use to add formatting elements to plain text documents.
|
||||
You won't be writing the documentation in a word processor, but in simple plain text, and using some special syntax that will consistently and professionally format that text.
|
||||
|
||||
|
||||
The basic Markdown syntax is very simple. Here are some [quick examples](https://commonmark.org/help/). And here is a great [10 minute tutorial](https://commonmark.org/help/tutorial/).
|
||||
This will cover a lot of the basics, like bolding text, italics, paragraphs, lists and other common formatting techniques.
|
||||
|
||||
![Markdown screenshot](./images/markdown.png "Markdown example")
|
||||
|
||||
### MyST
|
||||
Markdown doesn't support some really useful things.
|
||||
You can't add footnotes, or create an "aside" comment or build a table.
|
||||
Because of this there are many extensions typically referened to as Markdown "Flavors".
|
||||
The flavor we are using is MyST.
|
||||
There is [excellent documentation on MyST](https://myst-parser.readthedocs.io/en/v0.13.5/using/syntax.html) that you should definitely review, so you know everthing that is available to you.
|
||||
|
||||
|
||||
### Sphinx
|
||||
This is a large documenation effort. Many different Markdown pages will together make up the full website.
|
||||
|
||||
You will mostly use Sphinx in the background - you won't be aware of it.
|
||||
But if you decide that you want to alter the theme (the colors, styles, etc...) of the final website, Sphinx controls this and offers [themes](https://sphinx-themes.org/) and the ability to change styles / colors and formatting through the site.
|
||||
You just need to learn a little CSS to control it.
|
||||
|
||||
|
||||
### GitHub
|
||||
Our project is managed by a version control system called Git.
|
||||
You can use GIT to submit changes to the documenation, in the same we use to submit changes to our code.
|
||||
It is avilable on GitHub as the [spiff-arena project](https://github.com/sartography/spiff-arena). Git also manages versions of the code, and handles running tests, and causing our documenation to be built and deployed.
|
||||
It will take a bit to get comfortable with Git, but when you do, you will come to love it (or maybe hate it, but with a lot of respect)
|
||||
|
||||
## Setup
|
||||
|
||||
So that's a lot of tools, and seemingly a lot to learn.
|
||||
But you will find that most of it just works - and that once you get into a regular flow, it will become second nature.
|
||||
|
||||
|
||||
### Step 1: Pre-Requisites
|
||||
Assure you have been granted write access to our repository.
|
||||
Make sure you have an account on GitHub and then contact dan@sartography.com and ask him to add you as a contributor.
|
||||
|
||||
|
||||
### Step 2: Install VSCode
|
||||
[Download VSCode](https://code.visualstudio.com/) and install it on your computer.
|
||||
|
||||
### Step 3: Install Python
|
||||
We need python in order to build the website locally so we can really see what our content is going to look like once we publish. It's going to be handy for other reasons as well. We'll want python to be properly set up inside of VS Code. Follow [these directions and brief tutorial](https://code.visualstudio.com/docs/python/python-tutorial
|
||||
) to assure this is set up.
|
||||
|
||||
|
||||
|
||||
### Step 3: Connect VSCode to Git
|
||||
VSCode comes with Git built in.
|
||||
So you can use VSCode to "pull" changes from others down to your local computer and "push" changes back up to share with others (and to cause our docs site to rebuild)
|
||||
|
||||
Here are directions for how to [clone Spiff-Arena](https://learn.microsoft.com/en-us/azure/developer/javascript/how-to/with-visual-studio-code/clone-github-repository?tabs=create-repo-command-palette%2Cinitialize-repo-activity-bar%2Ccreate-branch-command-palette%2Ccommit-changes-command-palette%2Cpush-command-palette#clone-repository). **IMPORTANT**: Follow those directions, but be sure to checkout https://github.com/sartography/spiff-arena instead of the project they are using!
|
||||
|
||||
You can save the project to any directory on your computer.
|
||||
We strongly suggest you create a sub-folder called "projects" in your "home" or "Desktop" folder and checkout the code into this directory.
|
||||
|
||||
### Step 4: Open just the Docs Folder
|
||||
|
||||
We've checked out the whole spiff-arena project, but we are only going to be working inside of the docs directory. So let's open just that folder in VSCode.
|
||||
|
||||
* Go to File -> Open Folder
|
||||
* Select the "docs" folder inside of spiff-arena.
|
||||
|
||||
Now clikc on the two pieces of paper at the top corner of your screen, and you should see a project that looks like this:
|
||||
|
||||
![Docs Directory](./images/docs_dir.png "Docs Directory")
|
||||
|
||||
Without all the rest of the code in your way.
|
||||
|
||||
### Step 4: Add some extensions
|
||||
* Inside VSCode, go to File -> Preferences -> Extensions
|
||||
* Search for "myst"
|
||||
* click the "install" button.
|
||||
* Repeat, this time doing it for "python extension for VS Code"
|
||||
|
||||
![Myst Extension](./images/myst.png "Search or MyST in extensions")
|
||||
|
||||
|
||||
### Step 5: Install Python Dependencies
|
||||
This project requires a few Python dependencies to work correctly. We are going to set up a Virtual Evironment for Python to keep us sane later on. You can do that by following these steps:
|
||||
|
||||
1. Open the Command Palette (Ctrl+Shift+P), start typing the **Python: Create Environment** command to search, and then select the command.
|
||||
1. Select **Venv**
|
||||
1. Select Python 3.11 from the list of options if there is nore than one thing to select.
|
||||
1. Be sure the the checkbox next to "requirements.txt" is selected.
|
||||
1. Click OK.
|
||||
|
||||
### Step 6: Fire up the website
|
||||
1. Go to Terminial -> New Terminal
|
||||
1. type: **sphinx-autobuild . _build/html** at the prompt and hit enter.
|
||||
1. Open your browser and go to http://127.0.0.1:8000
|
||||
|
||||
|
||||
### Step 7: Make a chance
|
||||
1. Open up a markdown file, and make a change.
|
||||
|
||||
### Step 8: Commit your changes and push them up for everyone.
|
||||
1. Select the "git" button on the left hand side of the toolbar (cricles with lines between them) ![Git button](./images/git.png "Git button")
|
||||
|
||||
2. Press the blue "Commit" button.
|
||||
|
||||
3. Any changes you pushed up, should be live on our website within 5 to 10 minutes.
|
||||
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 916 B |
After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,18 @@
|
|||
Welcome to SpiffWorkflow's documentation!
|
||||
=======================================
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
:caption: Contents
|
||||
quick_start/quick_start.md
|
||||
documentation/documentation.md
|
||||
```
|
||||
|
||||
This is great!
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* [](genindex)
|
||||
* [](modindex)
|
||||
* [](search)
|
|
@ -0,0 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,306 @@
|
|||
# Quick start guide
|
||||
|
||||
```{admonition} Welcome to the SpiffWorkflow quick start guide!
|
||||
:class: info
|
||||
|
||||
👇 Throughout this step-by-step guide, we will walk you through key components of SpiffWorkflow, ensuring that you have a clear understanding of how to use the platform effectively.
|
||||
```
|
||||
|
||||
|
||||
## 🚀 Getting Started with SpiffWorkflow
|
||||
|
||||
SpiffWorkflow is a platform that facilitates the execution of business processes performed within the Status platform.
|
||||
|
||||
To access SpiffWorkflow, simply sign in using your Keycloak account. Once you have successfully signed in to the Spiff platform, it is crucial to familiarize yourself with the various sections within the SpiffWorkflow. This will enable you to gain a comprehensive understanding of the interface.
|
||||
|
||||
```{image} images/Untitled.png
|
||||
:alt: Login Page
|
||||
:width: 45%
|
||||
```
|
||||
```{image} images/Untitled_1.png
|
||||
:alt: Home Page
|
||||
:width: 45%
|
||||
```
|
||||
|
||||
```{admonition} Signing In
|
||||
:class: warning
|
||||
|
||||
⚠️ In the event that you encounter any difficulties signing in to Spiff, please reach out to Jakub (**@jakubgs**) on Discord for assistance and further guidance.
|
||||
```
|
||||
|
||||
Here, we will provide a generic overview of each section step by step, allowing you to navigate and engage with the platform more effectively.
|
||||
|
||||
### Step 1: Explore the Home section
|
||||
|
||||
Once you are signed in, you can start exploring the home page. The home page has three tab sections: **In Progress**, **Completed** and **Start New**.
|
||||
|
||||
![Untitled](images/Untitled_2.png)
|
||||
|
||||
- The "In Progress" section provides an overview of all ongoing process instances, including those initiated by you, those awaiting your action, or those awaiting action from a team you are a member of (Optional).
|
||||
- The "Completed" section allows you to view all completed process instances, including those initiated by you, those initiated by other SpiffWorkflow users with tasks completed by you and if applicable, those with tasks completed by a group of which you are a member.
|
||||
- The “Start New” section displays the processes you are permitted to start according to your role.
|
||||
|
||||
```{admonition} Signing In
|
||||
:class: info
|
||||
💡 **Process:** A process is a sequence of tasks that must be completed to achieve a specific goal.
|
||||
|
||||
**Instance:** An instance, on the other hand, represents a specific occurrence of a process. Each instance has its own set of data and state that is updated as the instance progresses through the workflow.
|
||||
```
|
||||
|
||||
If you are a member of a team, you may also have one or more Instances with tasks waiting for [team name] lists as well.
|
||||
|
||||
![Untitled](images/Untitled_3.png)
|
||||
|
||||
### Step 2: Explore the Processes section
|
||||
|
||||
|
||||
The process section provides a comprehensive view of the process ecosystem by showcasing process groups and process models.
|
||||
|
||||
```{admonition} Process Groups
|
||||
:class: info
|
||||
💡 A **process group** is a way of grouping a bunch of **process models.** A **process model** contains all the files necessary to execute a specific process.
|
||||
```
|
||||
--
|
||||
![Untitled](images/Untitled_4.png)
|
||||
|
||||
### Step 3: Explore the Process Instances section
|
||||
|
||||
The Process Instance section provides a detailed view of individual process instances, allowing you to track their progress and manage them effectively. This section includes essential information such as the instance ID, process name, the individual who started the process, the end date, and the current status.
|
||||
|
||||
![Untitled](images/Untitled_5.png)
|
||||
|
||||
```{admonition} Desktop Notifications
|
||||
:class: info
|
||||
💡 To receive SpiffWorkflow notifications in StatusApp Desktop, Public key from your Status account should be added to your **Bamboo profile**. This will ensure that workflow-related notifications are sent to you.
|
||||
|
||||
```
|
||||
|
||||
When getting started with SpiffWorkflow, it's essential to take the time to explore and familiarize yourself with the platform's interface and features. Feel free to ask questions about the platform's features or how to get started. The PPG team is always on hand to provide assistance and support when needed.
|
||||
|
||||
---
|
||||
|
||||
## 🌱 How to Start a Process
|
||||
|
||||
With SpiffWorkflow, you can easily initiate a new process instance. Here's a step-by-step guide on how to start a process.
|
||||
|
||||
### Step 1: Sign in and navigate to Home section
|
||||
|
||||
The first thing you need to do is sign in to your account on SpiffWorkflow. Once you're signed in, you'll see three tabs in the Home section: In progress, Completed, and Start New. If you want to start a new process, click the "Start New +" button. This will bring up "Processes I can start" section.
|
||||
|
||||
![Untitled](images/Untitled_6.png)
|
||||
|
||||
```{admonition} The Landing Page
|
||||
:class: info
|
||||
💡 The landing page will be the **Home section** by default, and you can navigate to other sections.
|
||||
```
|
||||
|
||||
### Step 2: Select the process
|
||||
|
||||
Next, you will see a list of available processes that you have permission to start. Choose the process you want to initiate and click “Start”.
|
||||
|
||||
![Untitled](images/Untitled_7.png)
|
||||
|
||||
Congratulations! You have successfully started a new process instance in SpiffWorkflow.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 How to respond to a request
|
||||
|
||||
When using SpiffWorkflow, knowing how to respond to requests is essential to the process. While each request may have unique requirements, the basic steps for responding are similar. The following steps will guide you through the process of responding to requests.
|
||||
|
||||
### Step 1: Navigate to the home page
|
||||
|
||||
Once you are signed in, navigate to the home page of SpiffWorkflow. On the home page, you will see a list of all the requests that are available to you.
|
||||
|
||||
There will be three types of instances shown:
|
||||
|
||||
- **Started by me:** This section shows a list of process instances that were started by you, providing you with an overview of the instances you have initiated.
|
||||
- **Waiting for me:** This section displays a list of process instances with tasks assigned to you and are currently waiting for you to respond to.
|
||||
- **Waiting for [team name]:** If you are a member of SpiffWorkflow**,** this section displays a list of process instances with tasks assigned to a group you are a member of and currently waiting for someone in that group to complete them.
|
||||
|
||||
![Untitled](images/Untitled_8.png)
|
||||
|
||||
In the case of new users who haven't started or been part of any process or been assigned to any team, you won't be able to see any items on the home page.
|
||||
|
||||
![Untitled](images/Untitled_9.png)
|
||||
|
||||
### Step 2: Respond to the request
|
||||
|
||||
Once you have identified the request you need to respond to, simply click on the 'Go' button in the action column to open it. Upon opening the process instance, you can respond to the request based on the requirements of that task.
|
||||
|
||||
Depending on the task requirements, this may involve submitting additional information, reviewing the task or any other action item.
|
||||
|
||||
![Untitled](images/Untitled_10.png)
|
||||
|
||||
That's it! With these simple steps, you can efficiently review tasks in SpiffWorkflow.
|
||||
|
||||
---
|
||||
|
||||
## 📑 How to view process steps for the process you just started
|
||||
|
||||
After starting a process, it's important to stay informed about its progress. Even though you'll receive notifications when your attention is required, it's natural to be curious about what's happening in the background. Therefore, monitoring the process steps regularly is a great way to ensure everything is moving smoothly.
|
||||
|
||||
Here's how you can view the steps of the process you just started.
|
||||
|
||||
### Step 1: Navigate to the “Home” or “Process Instance” section.
|
||||
|
||||
There are 2 ways of finding your process instances.
|
||||
|
||||
Option 1: Once you're signed in, navigate to the home section. Here you will find a list of all the processes instances you've initiated.
|
||||
|
||||
![Untitled](images/Untitled_11.png)
|
||||
|
||||
Option 2: You can also view the processes you have initiated in the **"Process Instances"** section.
|
||||
|
||||
![Untitled](images/Untitled_12.png)
|
||||
|
||||
### Step 2: Select the process instance you want to view
|
||||
|
||||
Click on the process instance you want to view. This will take you to the process instance information. Navigate to the BPMN diagram section. Here you can see the current task highlighted in **yellow**. The grey represents the path which was taken by the current process steps.
|
||||
|
||||
![Untitled](images/Untitled_13.png)
|
||||
|
||||
By following these steps, you can easily view the steps of the process you initiated and keep track of progress.
|
||||
|
||||
---
|
||||
|
||||
## 🏷️How to view the Process-defined metadata for a process instance
|
||||
|
||||
The Process-defined **metadata can provide valuable insights into its history, current status, and other important details that is specifically created and used within a particular process. With the SpiffWorkflow platform, users can easily view the metadata for a process instance.
|
||||
|
||||
To check the metadata of a process instance, follow these steps.
|
||||
|
||||
### Step 1: Navigate to the “Home” or “Process Instance” section.
|
||||
|
||||
Once you're signed in, navigate to the home section. Here you will find a list of all the process instances you've initiated under **“Started by me”**.
|
||||
|
||||
![Untitled](images/Untitled_14.png)
|
||||
|
||||
### Step 2: View metadata for the selected process instance
|
||||
|
||||
Click on the process instance you want to view. Upon clicking this, you will be able to view the information about the given instance. You'll find the metadata under the details option in the process instance.
|
||||
|
||||
![Untitled](images/Untitled_15.png)
|
||||
|
||||
By following these simple steps, you can easily view the metadata for a process instance in SpiffWorkflow.
|
||||
|
||||
---
|
||||
|
||||
## 📂 How to view Process Model files
|
||||
|
||||
The process model files provide great transparency into our internal business rules and processes. You can dig deep into the decision-making process and really understand how the process and organization operate. With these steps, you'll be able to access process models easily and efficiently.
|
||||
|
||||
### Step 1: Head over to the process section
|
||||
|
||||
Once you have successfully signed in, navigate to the process section. This section allows you to access all the process groups and process models you have access to.
|
||||
|
||||
![Untitled](images/Untitled_16.png)
|
||||
|
||||
> **Step 2: Find and click on the process**
|
||||
>
|
||||
|
||||
You can either search for a process model using the search bar or navigate through displayed processes to find the process model.
|
||||
|
||||
![Untitled](images/Untitled_17.png)
|
||||
|
||||
### Step 3: Access the process model files
|
||||
|
||||
Once you have clicked on the process you want to view, a list of the model files that are associated with the process will appear.
|
||||
|
||||
![Untitled](images/Untitled_18.png)
|
||||
|
||||
By following these simple steps, you can easily view process model files in SpiffWorkflow.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 How to view and filter process instances
|
||||
|
||||
As you work on various process instances in SpiffWorkflow, you may want to view and filter some of them. This can help you track the status of various instances and manage them more efficiently.
|
||||
|
||||
Here are the steps to view and filter process instances in SpiffWorkflow.
|
||||
|
||||
### Step 1: Navigate to Process Instances
|
||||
|
||||
Once you are signed in, navigate to the "Process Instances" section. Within the "Process Instances" section, you'll see a list of all the instances for the processes you can access.
|
||||
|
||||
![Untitled](images/Untitled_19.png)
|
||||
|
||||
### Step 2: Click on Filter option
|
||||
|
||||
To filter the list, click on the "Filter" option. This will expand the filter section where you will be able to provide details about the process instance. This allows you to enter various details, including the process model, start date, end date, and time. To refine your search, you can enter multiple filter parameters.
|
||||
|
||||
![Untitled](images/Untitled_20.png)
|
||||
|
||||
### Step 3: Apply Filters:
|
||||
|
||||
Once you have entered all the relevant filter details, click on the "**Apply**" button to apply the filters. The system will then display all the process instances matching the input details.
|
||||
|
||||
![Untitled](images/Untitled_21.png)
|
||||
|
||||
To filter process instances by **process-defined metadata**, follow these steps:
|
||||
|
||||
- Search for the specific **process** you want to filter and click on the column option to select metadata options.
|
||||
|
||||
![Untitled](images/Untitled_22.png)
|
||||
|
||||
- The metadata fields will be displayed in dropdown. Select the field you want to display and Click on "**Save**" to apply the changes.
|
||||
|
||||
![Untitled](images/Untitled_23.png)
|
||||
|
||||
- After saving the details, the newly created column will be displayed. Finally click on “**Apply“** button to reflect the changes.
|
||||
|
||||
![Untitled](images/Untitled_24.png)
|
||||
|
||||
### (Optional) Step 4: Save Perspectives
|
||||
|
||||
If you wish to save the perspectives, click on the "**Save**" button.
|
||||
|
||||
![Untitled](images/Untitled_25.png)
|
||||
|
||||
A prompt will appear, allowing you to provide a name for the identifier associated with the saved filter. Enter a descriptive name for the filter identifier and “**Save”** changes. Now you can search for specific processes using Process Instance Perspectives.
|
||||
|
||||
![Untitled](images/Untitled_26.png)
|
||||
|
||||
![Untitled](images/Untitled_27.png)
|
||||
|
||||
### (Optional) Step 5: Filter by ID
|
||||
|
||||
![Untitled](images/Untitled_28.png)
|
||||
|
||||
If you want to filter by ID, go to the "Find by Id" section of the page. Enter the ID and click "Submit". The system will show you the process instance with the corresponding ID.
|
||||
|
||||
You can now view the process instances that you filtered for and take appropriate action based on their status. This can help you manage your workflows more efficiently and keep track of the progress of various process instances.
|
||||
|
||||
---
|
||||
|
||||
## 🗳️ How to request additional permissions
|
||||
|
||||
As a user, you may be required to access certain process groups or start process models in order to perform desired actions. However, you may not have the necessary access or permissions to do so. In this case, you will need to request access or additional permissions from the admins - PPG team.
|
||||
|
||||
By following these steps, you can submit a request and seek the necessary permissions to perform the desired actions.
|
||||
|
||||
### Step 1: Navigate & Search
|
||||
|
||||
Once you are signed in, navigate to the "**Process**" section. Use the search bar or browse through the available process models until you find "**Request Access**”. Click on the process model to open it.
|
||||
|
||||
![Untitled](images/Untitled_29.png)
|
||||
|
||||
If you want to access the request access process from **Home** section and click on the "**Start New +**" button. This will open the "Processes I can start" section where you can find the “Request Access” process.
|
||||
|
||||
![Untitled](images/Untitled_30.png)
|
||||
|
||||
### Step 2: Start the Process
|
||||
|
||||
Once the "**Process Request**" model is open, initiate the process by clicking on the "Start" button.
|
||||
|
||||
![Untitled](images/Untitled_31.png)
|
||||
|
||||
### Step 3: Provide Request Details & Submit
|
||||
|
||||
A task will be presented to capture the necessary information and details for special permissions request. Find the “**Description”** text field and enter the relevant information and details about your request.
|
||||
|
||||
Ensure that all required details have been included such as Process name, Process group name, and type of permissions you need. Click on the "**Submit**" button or similar action to submit your access or special permissions request.
|
||||
|
||||
![Untitled](images/Untitled_32.png)
|
||||
|
||||
By following these steps, you can request the special permissions needed to carry out your tasks effectively.
|
|
@ -0,0 +1,46 @@
|
|||
alabaster==0.7.13
|
||||
appdirs==1.4.4
|
||||
astroid==2.15.5
|
||||
attrs==23.1.0
|
||||
Babel==2.12.1
|
||||
cattrs==22.2.0
|
||||
certifi==2023.5.7
|
||||
charset-normalizer==3.1.0
|
||||
colorama==0.4.6
|
||||
docutils==0.18.1
|
||||
esbonio==0.16.1
|
||||
idna==3.4
|
||||
imagesize==1.4.1
|
||||
Jinja2==3.1.2
|
||||
lazy-object-proxy==1.9.0
|
||||
livereload==2.6.3
|
||||
lsprotocol==2023.0.0a1
|
||||
markdown-it-py==2.2.0
|
||||
MarkupSafe==2.1.2
|
||||
mdit-py-plugins==0.3.5
|
||||
mdurl==0.1.2
|
||||
myst-parser==1.0.0
|
||||
packaging==23.1
|
||||
pygls==1.0.2
|
||||
Pygments==2.15.1
|
||||
pyspellchecker==0.7.2
|
||||
PyYAML==6.0
|
||||
requests==2.30.0
|
||||
six==1.16.0
|
||||
snowballstemmer==2.2.0
|
||||
Sphinx==6.2.1
|
||||
sphinx-autoapi==2.1.0
|
||||
sphinx-autobuild==2021.3.14
|
||||
sphinx-rtd-theme==1.2.0
|
||||
sphinxcontrib-applehelp==1.0.4
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
sphinxcontrib-htmlhelp==2.0.1
|
||||
sphinxcontrib-jquery==4.1
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
tornado==6.3.2
|
||||
typeguard==3.0.2
|
||||
Unidecode==1.3.6
|
||||
urllib3==2.0.2
|
||||
wrapt==1.15.0
|
After Width: | Height: | Size: 14 KiB |
|
@ -42,7 +42,7 @@ RUN poetry install --without dev
|
|||
FROM deployment AS final
|
||||
|
||||
LABEL source="https://github.com/sartography/spiff-arena"
|
||||
LABEL description="Software development platform for building, running, and monitoring executable diagrams"
|
||||
LABEL description="Backend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams"
|
||||
|
||||
COPY --from=setup /app /app
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ Spiffworkflow Backend
|
|||
:alt: Black
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
|
|
|
@ -40,15 +40,16 @@ fi
|
|||
|
||||
additional_args=""
|
||||
|
||||
if [[ "${SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT:-}" != "/" ]]; then
|
||||
additional_args="${additional_args} -e SCRIPT_NAME=${SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT}"
|
||||
app_root="${SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT:-}"
|
||||
if [[ -n "$app_root" ]] && [[ "${app_root}" != "/" ]]; then
|
||||
additional_args="${additional_args} -e SCRIPT_NAME=${app_root}"
|
||||
fi
|
||||
|
||||
# HACK: if loading fixtures for acceptance tests when we do not need multiple workers
|
||||
# it causes issues with attempting to add duplicate data to the db
|
||||
workers=3
|
||||
worker_count=4
|
||||
if [[ "${SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA:-}" == "true" ]]; then
|
||||
workers=1
|
||||
worker_count=1
|
||||
fi
|
||||
|
||||
if [[ "${SPIFFWORKFLOW_BACKEND_RUN_DATA_SETUP:-}" != "false" ]]; then
|
||||
|
@ -67,11 +68,35 @@ fi
|
|||
git init "${SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR}"
|
||||
git config --global --add safe.directory "${SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR}"
|
||||
|
||||
if [[ -z "${SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER:-}" ]]; then
|
||||
# default to 3 * 2 = 6 threads per worker
|
||||
# you may want to configure threads_to_use_per_core based on whether your workload is more cpu intensive or more I/O intensive:
|
||||
# cpu heavy, make it smaller
|
||||
# I/O heavy, make it larger
|
||||
threads_to_use_per_core=3
|
||||
|
||||
# https://stackoverflow.com/a/55423170/6090676
|
||||
# if we had access to python (i'm not sure i want to run another python script here),
|
||||
# we could do something like this (on linux) to get the number of cores available to this process and a better estimate of a
|
||||
# reasonable num_cores_multiple_for_threads
|
||||
# if hasattr(os, 'sched_getaffinity')
|
||||
# number_of_available_cores = os.sched_getaffinity(0)
|
||||
# BUT the python solution isn't even as portable as this one, which is mostly posix compliant and works on linux/mac/freebsd.
|
||||
num_cores_multiple_for_threads=$(getconf _NPROCESSORS_ONLN 2>/dev/null || getconf NPROCESSORS_ONLN 2>/dev/null || echo 1)
|
||||
|
||||
SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER=$((threads_to_use_per_core * num_cores_multiple_for_threads))
|
||||
export SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER
|
||||
fi
|
||||
|
||||
# --worker-class is not strictly necessary, since setting threads will automatically set the worker class to gthread, but meh
|
||||
export IS_GUNICORN="true"
|
||||
# THIS MUST BE THE LAST COMMAND!
|
||||
exec poetry run gunicorn ${additional_args} \
|
||||
--bind "0.0.0.0:$port" \
|
||||
--workers="$workers" \
|
||||
--preload \
|
||||
--worker-class "gthread" \
|
||||
--workers="$worker_count" \
|
||||
--threads "$SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER" \
|
||||
--limit-request-line 8192 \
|
||||
--timeout "$GUNICORN_TIMEOUT_SECONDS" \
|
||||
--capture-output \
|
||||
|
|
|
@ -3089,7 +3089,7 @@ lxml = "*"
|
|||
type = "git"
|
||||
url = "https://github.com/sartography/SpiffWorkflow"
|
||||
reference = "main"
|
||||
resolved_reference = "a68dec77ebb0960dd8097341df7575a34e435501"
|
||||
resolved_reference = "340e9983b5afd2e6e71df883e74f7dc20d4474dd"
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
|
|
|
@ -185,13 +185,19 @@ def create_app() -> flask.app.Flask:
|
|||
return app # type: ignore
|
||||
|
||||
|
||||
def get_version_info_data() -> dict[str, Any]:
|
||||
version_info_data_dict = {}
|
||||
if os.path.isfile("version_info.json"):
|
||||
with open("version_info.json") as f:
|
||||
version_info_data_dict = json.load(f)
|
||||
return version_info_data_dict
|
||||
|
||||
|
||||
def _setup_prometheus_metrics(app: flask.app.Flask, connexion_app: connexion.apps.flask_app.FlaskApp) -> None:
|
||||
metrics = ConnexionPrometheusMetrics(connexion_app)
|
||||
app.config["PROMETHEUS_METRICS"] = metrics
|
||||
if os.path.isfile("version_info.json"):
|
||||
version_info_data = {}
|
||||
with open("version_info.json") as f:
|
||||
version_info_data = json.load(f)
|
||||
version_info_data = get_version_info_data()
|
||||
if len(version_info_data) > 0:
|
||||
# prometheus does not allow periods in key names
|
||||
version_info_data_normalized = {k.replace(".", "_"): v for k, v in version_info_data.items()}
|
||||
metrics.info("version_info", "Application Version Info", **version_info_data_normalized)
|
||||
|
|
|
@ -162,6 +162,19 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/debug/version-info:
|
||||
get:
|
||||
operationId: spiffworkflow_backend.routes.debug_controller.version_info
|
||||
summary: Returns information about the version of the application
|
||||
tags:
|
||||
- Status
|
||||
responses:
|
||||
"200":
|
||||
description: Returns version info if it exists.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/OkTrue"
|
||||
|
||||
/active-users/updates/{last_visited_identifier}:
|
||||
parameters:
|
||||
|
|
|
@ -13,18 +13,17 @@ class ConfigurationError(Exception):
|
|||
"""ConfigurationError."""
|
||||
|
||||
|
||||
def setup_database_uri(app: Flask) -> None:
|
||||
"""Setup_database_uri."""
|
||||
def setup_database_configs(app: Flask) -> None:
|
||||
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None:
|
||||
database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}"
|
||||
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite":
|
||||
app.config[
|
||||
"SQLALCHEMY_DATABASE_URI"
|
||||
] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = (
|
||||
f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
|
||||
)
|
||||
elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres":
|
||||
app.config[
|
||||
"SQLALCHEMY_DATABASE_URI"
|
||||
] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = (
|
||||
f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
|
||||
)
|
||||
else:
|
||||
# use pswd to trick flake8 with hardcoded passwords
|
||||
db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD")
|
||||
|
@ -34,6 +33,27 @@ def setup_database_uri(app: Flask) -> None:
|
|||
else:
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI")
|
||||
|
||||
# if pool size came in from the environment, it's a string, but we need an int
|
||||
# if it didn't come in from the environment, base it on the number of threads
|
||||
# note that max_overflow defaults to 10, so that will give extra buffer.
|
||||
pool_size = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_POOL_SIZE")
|
||||
if pool_size is not None:
|
||||
pool_size = int(pool_size)
|
||||
else:
|
||||
# this one doesn't come from app config and isn't documented in default.py
|
||||
# because we don't want to give people the impression
|
||||
# that setting it in flask python configs will work. on the contrary, it's used by a bash
|
||||
# script that starts the backend, so it can only be set in the environment.
|
||||
threads_per_worker_config = os.environ.get("SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER")
|
||||
if threads_per_worker_config is not None:
|
||||
pool_size = int(threads_per_worker_config)
|
||||
else:
|
||||
# this is a sqlalchemy default, if we don't have any better ideas
|
||||
pool_size = 5
|
||||
|
||||
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {}
|
||||
app.config["SQLALCHEMY_ENGINE_OPTIONS"]["pool_size"] = pool_size
|
||||
|
||||
|
||||
def load_config_file(app: Flask, env_config_module: str) -> None:
|
||||
"""Load_config_file."""
|
||||
|
@ -115,7 +135,7 @@ def setup_config(app: Flask) -> None:
|
|||
|
||||
app.config["PROCESS_UUID"] = uuid.uuid4()
|
||||
|
||||
setup_database_uri(app)
|
||||
setup_database_configs(app)
|
||||
setup_logger(app)
|
||||
|
||||
if app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"] == "":
|
||||
|
|
|
@ -33,6 +33,10 @@ SPIFFWORKFLOW_BACKEND_BACKGROUND_SCHEDULER_USER_INPUT_REQUIRED_POLLING_INTERVAL_
|
|||
default="120",
|
||||
)
|
||||
)
|
||||
|
||||
# we only use this in one place, and it checks to see if it is None.
|
||||
SPIFFWORKFLOW_BACKEND_DATABASE_POOL_SIZE = environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_POOL_SIZE")
|
||||
|
||||
SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND = environ.get(
|
||||
"SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND", default="http://localhost:7001"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
default_group: everybody
|
||||
|
||||
groups:
|
||||
admin:
|
||||
users: [admin@spiffworkflow.org]
|
||||
|
||||
permissions:
|
||||
admin:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [read]
|
||||
uri: /*
|
||||
|
||||
tasks-crud:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, update, delete]
|
||||
uri: /tasks/*
|
||||
|
||||
process-instances-crud:
|
||||
groups: [ admin ]
|
||||
users: [ ]
|
||||
allowed_permissions: [create, update, delete]
|
||||
uri: /process-instances/*
|
||||
|
||||
suspend:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/process-instance-suspend
|
||||
|
||||
terminate:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/process-instance-terminate
|
||||
|
||||
resume:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/process-instance-resume
|
||||
|
||||
reset:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/process-instance-reset
|
||||
|
||||
users-exist:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/users/exists/by-username
|
||||
|
||||
send-event:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/send-event/*
|
||||
|
||||
task-complete:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/task-complete/*
|
||||
|
||||
messages:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create]
|
||||
uri: /v1.0/messages/*
|
||||
|
||||
secrets:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [create, update, delete]
|
||||
uri: /v1.0/secrets/*
|
||||
|
||||
task-data:
|
||||
groups: [admin]
|
||||
users: []
|
||||
allowed_permissions: [update]
|
||||
uri: /v1.0/task-data/*
|
|
@ -129,9 +129,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||
def serialized_with_metadata(self) -> dict[str, Any]:
|
||||
process_instance_attributes = self.serialized
|
||||
process_instance_attributes["process_metadata"] = self.process_metadata
|
||||
process_instance_attributes[
|
||||
"process_model_with_diagram_identifier"
|
||||
] = self.process_model_with_diagram_identifier
|
||||
process_instance_attributes["process_model_with_diagram_identifier"] = (
|
||||
self.process_model_with_diagram_identifier
|
||||
)
|
||||
return process_instance_attributes
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
"""APIs for dealing with process groups, process models, and process instances."""
|
||||
from flask import make_response
|
||||
from flask.wrappers import Response
|
||||
|
||||
from spiffworkflow_backend import get_version_info_data
|
||||
|
||||
|
||||
def test_raise_error() -> Response:
|
||||
raise Exception("This exception was generated by /debug/test-raise-error for testing purposes. Please ignore.")
|
||||
|
||||
|
||||
def version_info() -> Response:
|
||||
return make_response(get_version_info_data(), 200)
|
||||
|
|
|
@ -9,7 +9,3 @@ def status() -> Response:
|
|||
"""Status."""
|
||||
ProcessInstanceModel.query.filter().first()
|
||||
return make_response({"ok": True}, 200)
|
||||
|
||||
|
||||
def test_raise_error() -> Response:
|
||||
raise Exception("This exception was generated by /status/test-raise-error for testing purposes. Please ignore.")
|
||||
|
|
|
@ -438,14 +438,6 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st
|
|||
)
|
||||
yield render_data("error", api_error)
|
||||
return
|
||||
except Exception as e:
|
||||
api_error = ApiError(
|
||||
error_code="engine_steps_error",
|
||||
message=f"Failed to complete an automated task. Error was: {str(e)}",
|
||||
status_code=400,
|
||||
)
|
||||
yield render_data("error", api_error)
|
||||
return
|
||||
processor.refresh_waiting_tasks()
|
||||
ready_engine_task_count = get_ready_engine_step_count(processor.bpmn_process_instance)
|
||||
tasks = get_reportable_tasks()
|
||||
|
|
|
@ -577,13 +577,14 @@ class AuthorizationService:
|
|||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="read", target_uri="/process-instances/report-metadata")
|
||||
)
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/active-users/*"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/debug/version-info"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-groups"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-models"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes/callers"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/user-groups/for-current-user"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/active-users/*"))
|
||||
permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/users/exists/by-username"))
|
||||
permissions_to_assign.append(
|
||||
PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*")
|
||||
|
|
|
@ -415,9 +415,9 @@ class ProcessInstanceProcessor:
|
|||
tld.process_instance_id = process_instance_model.id
|
||||
|
||||
# we want this to be the fully qualified path to the process model including all group subcomponents
|
||||
current_app.config[
|
||||
"THREAD_LOCAL_DATA"
|
||||
].process_model_identifier = f"{process_instance_model.process_model_identifier}"
|
||||
current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
|
||||
f"{process_instance_model.process_model_identifier}"
|
||||
)
|
||||
|
||||
self.process_instance_model = process_instance_model
|
||||
self.process_model_service = ProcessModelService()
|
||||
|
@ -577,9 +577,9 @@ class ProcessInstanceProcessor:
|
|||
bpmn_subprocess_definition.bpmn_identifier
|
||||
] = bpmn_process_definition_dict
|
||||
spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {}
|
||||
bpmn_subprocess_definition_bpmn_identifiers[
|
||||
bpmn_subprocess_definition.id
|
||||
] = bpmn_subprocess_definition.bpmn_identifier
|
||||
bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = (
|
||||
bpmn_subprocess_definition.bpmn_identifier
|
||||
)
|
||||
|
||||
task_definitions = TaskDefinitionModel.query.filter(
|
||||
TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore
|
||||
|
|
|
@ -432,7 +432,8 @@ class TaskService:
|
|||
for task_id, task_properties in tasks.items():
|
||||
# The Root task is added to the spec by Spiff when the bpmn process is instantiated
|
||||
# within Spiff. We do not actually need it and it's missing from our initial
|
||||
# bpmn process defintion so let's avoid using it.
|
||||
# bpmn process defintion so let's avoid using it. This causes issues with the hashing
|
||||
# since it is not there when we initially take the hash and save the definition.
|
||||
if task_properties["task_spec"] == "Root":
|
||||
continue
|
||||
|
||||
|
|
|
@ -289,6 +289,7 @@ class TestAuthorizationService(BaseTest):
|
|||
) -> None:
|
||||
expected_permissions = [
|
||||
("/active-users/*", "read"),
|
||||
("/debug/version-info", "read"),
|
||||
("/process-groups", "read"),
|
||||
("/process-instances/find-by-id/*", "read"),
|
||||
("/process-instances/for-me", "create"),
|
||||
|
|
|
@ -23,7 +23,7 @@ module.exports = {
|
|||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['react', 'sonarjs', '@typescript-eslint'],
|
||||
plugins: ['react', 'sonarjs', '@typescript-eslint', 'unused-imports'],
|
||||
rules: {
|
||||
// according to https://github.com/typescript-eslint/typescript-eslint/issues/2621, You should turn off the eslint core rule and turn on the typescript-eslint rule
|
||||
// but not sure which of the above "extends" statements is maybe bringing in eslint core
|
||||
|
@ -43,6 +43,16 @@ module.exports = {
|
|||
'react/require-default-props': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
|
|
|
@ -29,8 +29,7 @@ RUN ./bin/build
|
|||
# Final image without setup dependencies.
|
||||
FROM base AS final
|
||||
|
||||
LABEL source="https://github.com/sartography/spiff-arena"
|
||||
LABEL description="Software development platform for building, running, and monitoring executable diagrams"
|
||||
LABEL description="Frontend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams"
|
||||
|
||||
# WARNING: On localhost frontend assumes backend is one port lowe.
|
||||
ENV PORT0=7001
|
||||
|
|
|
@ -81,7 +81,7 @@ describe('tasks', () => {
|
|||
cy.navigateToHome();
|
||||
|
||||
// look for somethig to make sure the homepage has loaded
|
||||
cy.contains('Instances with tasks waiting for me').should('exist');
|
||||
cy.contains('Waiting for me').should('exist');
|
||||
|
||||
// FIXME: this will probably need a better way to link to the proper form that we want
|
||||
cy.contains('Go').click();
|
||||
|
|
|
@ -76,24 +76,41 @@ const submitWithUser = (
|
|||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
if (expectAdditionalApprovalInfoPage) {
|
||||
cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
// if (expectAdditionalApprovalInfoPage) {
|
||||
// cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Continue$/)
|
||||
.click();
|
||||
}
|
||||
// cy.get('button')
|
||||
// .contains(/^Continue$/)
|
||||
// .click();
|
||||
// }
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.location({ timeout: 60000 }).should((loc) => {
|
||||
expect(loc.pathname).to.eq('/');
|
||||
});
|
||||
cy.wait(2000);
|
||||
cy.get('button').contains('Return to Home', { timeout: 60000 });
|
||||
cy.logout();
|
||||
cy.wait(2000);
|
||||
};
|
||||
|
||||
//Check if the process instance is completed successfully
|
||||
const checkProcessInstanceCompleted = (
|
||||
username,
|
||||
password,
|
||||
processInstanceId
|
||||
) => {
|
||||
cy.wait(2000);
|
||||
cy.log('========Login with : ', username);
|
||||
cy.log('========processInstanceId: ', processInstanceId);
|
||||
cy.login(username, password);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.visit('/admin/process-instances/find-by-id');
|
||||
cy.get('#process-instance-id-input').type(processInstanceId);
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
cy.wait(2000);
|
||||
cy.get('#tag-1 > span').contains('complete');
|
||||
}
|
||||
|
||||
// Consulting Fees Path - Without Files
|
||||
describe.only('Consulting Fees Path - Without Files', () => {
|
||||
Cypress._.times(1, () => {
|
||||
|
@ -237,6 +254,8 @@ describe.only('Consulting Fees Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -378,6 +397,8 @@ describe.only('Consulting Fees Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -539,6 +560,8 @@ describe.only('Consulting Fees Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -734,6 +757,8 @@ describe('Consulting Fees Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -925,6 +950,8 @@ describe('Consulting Fees Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1131,6 +1158,8 @@ describe('Consulting Fees Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -82,24 +82,41 @@ const submitWithUser = (
|
|||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
if (expectAdditionalApprovalInfoPage) {
|
||||
cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
// if (expectAdditionalApprovalInfoPage) {
|
||||
// cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Continue$/)
|
||||
.click();
|
||||
}
|
||||
// cy.get('button')
|
||||
// .contains(/^Continue$/)
|
||||
// .click();
|
||||
// }
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.location({ timeout: 60000 }).should((loc) => {
|
||||
expect(loc.pathname).to.eq('/');
|
||||
});
|
||||
cy.wait(2000);
|
||||
cy.get('button').contains('Return to Home', { timeout: 60000 });
|
||||
cy.logout();
|
||||
cy.wait(2000);
|
||||
};
|
||||
|
||||
//Check if the process instance is completed successfully
|
||||
const checkProcessInstanceCompleted = (
|
||||
username,
|
||||
password,
|
||||
processInstanceId
|
||||
) => {
|
||||
cy.wait(2000);
|
||||
cy.log('========Login with : ', username);
|
||||
cy.log('========processInstanceId: ', processInstanceId);
|
||||
cy.login(username, password);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.visit('/admin/process-instances/find-by-id');
|
||||
cy.get('#process-instance-id-input').type(processInstanceId);
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
cy.wait(2000);
|
||||
cy.get('#tag-1 > span').contains('complete');
|
||||
}
|
||||
|
||||
// Equipment Path - Without Files
|
||||
describe.only('Equipment Path - Without Files', () => {
|
||||
Cypress._.times(1, () => {
|
||||
|
@ -270,6 +287,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -397,6 +415,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -550,6 +569,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -663,6 +683,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -776,6 +797,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -907,6 +929,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
// Within Policy. People Ops Partner Group and Budget owner approves the request
|
||||
|
@ -1032,6 +1055,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1158,6 +1182,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1302,6 +1327,7 @@ describe.only('Equipment Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1526,6 +1552,7 @@ describe('Equipment Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1702,6 +1729,7 @@ describe('Equipment Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1904,6 +1932,7 @@ describe('Equipment Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2067,6 +2096,7 @@ describe('Equipment Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2230,6 +2260,7 @@ describe('Equipment Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2410,6 +2441,7 @@ describe('Equipment Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
// Within Policy. People Ops Partner Group and Budget owner approves the request
|
||||
|
@ -2584,6 +2616,7 @@ describe('Equipment Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2759,6 +2792,7 @@ describe('Equipment Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2962,6 +2996,7 @@ describe('Equipment Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,24 +76,42 @@ const submitWithUser = (
|
|||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
if (expectAdditionalApprovalInfoPage) {
|
||||
cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
// if (expectAdditionalApprovalInfoPage) {
|
||||
// cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Continue$/)
|
||||
.click();
|
||||
}
|
||||
// cy.get('button')
|
||||
// .contains(/^Continue$/)
|
||||
// .click();
|
||||
// }
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.location({ timeout: 60000 }).should((loc) => {
|
||||
expect(loc.pathname).to.eq('/');
|
||||
});
|
||||
cy.wait(2000);
|
||||
cy.get('button').contains('Return to Home', { timeout: 60000 });
|
||||
cy.logout();
|
||||
cy.wait(2000);
|
||||
|
||||
};
|
||||
|
||||
//Check if the process instance is completed successfully
|
||||
const checkProcessInstanceCompleted = (
|
||||
username,
|
||||
password,
|
||||
processInstanceId
|
||||
) => {
|
||||
cy.wait(2000);
|
||||
cy.log('========Login with : ', username);
|
||||
cy.log('========processInstanceId: ', processInstanceId);
|
||||
cy.login(username, password);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.visit('/admin/process-instances/find-by-id');
|
||||
cy.get('#process-instance-id-input').type(processInstanceId);
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
cy.wait(2000);
|
||||
cy.get('#tag-1 > span').contains('complete');
|
||||
}
|
||||
|
||||
// Learning and Development Path - Without Files
|
||||
describe.only('Learning and Development Path - Without Files', () => {
|
||||
Cypress._.times(1, () => {
|
||||
|
@ -205,6 +223,7 @@ describe.only('Learning and Development Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -318,6 +337,7 @@ describe.only('Learning and Development Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -449,6 +469,7 @@ describe.only('Learning and Development Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -604,6 +625,7 @@ describe.only('Learning and Development Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -717,6 +739,7 @@ describe.only('Learning and Development Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -848,6 +871,20 @@ describe.only('Learning and Development Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
const peopleOpsUsername = Cypress.env('peopleopssme_username');
|
||||
const peopleOpsPassword = Cypress.env('peopleopssme_password');
|
||||
cy.log(`=====peopleOpsUsername : ${peopleOpsUsername}`);
|
||||
cy.log(`=====peopleOpsPassword : ${peopleOpsPassword}`);
|
||||
|
||||
submitWithUser(
|
||||
peopleOpsUsername,
|
||||
peopleOpsPassword,
|
||||
processInstanceId,
|
||||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1017,6 +1054,7 @@ describe('Learning and Development Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1181,6 +1219,7 @@ describe('Learning and Development Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1362,6 +1401,7 @@ describe('Learning and Development Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1568,6 +1608,7 @@ describe('Learning and Development Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1732,6 +1773,7 @@ describe('Learning and Development Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1927,6 +1969,7 @@ describe('Learning and Development Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,24 +76,41 @@ const submitWithUser = (
|
|||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
if (expectAdditionalApprovalInfoPage) {
|
||||
cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
// if (expectAdditionalApprovalInfoPage) {
|
||||
// cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Continue$/)
|
||||
.click();
|
||||
}
|
||||
// cy.get('button')
|
||||
// .contains(/^Continue$/)
|
||||
// .click();
|
||||
// }
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.location({ timeout: 60000 }).should((loc) => {
|
||||
expect(loc.pathname).to.eq('/');
|
||||
});
|
||||
cy.wait(2000);
|
||||
cy.get('button').contains('Return to Home', { timeout: 60000 });
|
||||
cy.logout();
|
||||
cy.wait(2000);
|
||||
};
|
||||
|
||||
//Check if the process instance is completed successfully
|
||||
const checkProcessInstanceCompleted = (
|
||||
username,
|
||||
password,
|
||||
processInstanceId
|
||||
) => {
|
||||
cy.wait(2000);
|
||||
cy.log('========Login with : ', username);
|
||||
cy.log('========processInstanceId: ', processInstanceId);
|
||||
cy.login(username, password);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.visit('/admin/process-instances/find-by-id');
|
||||
cy.get('#process-instance-id-input').type(processInstanceId);
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
cy.wait(2000);
|
||||
cy.get('#tag-1 > span').contains('complete');
|
||||
}
|
||||
|
||||
describe.only('Other Fees Path - Without Files', () => {
|
||||
Cypress._.times(1, () => {
|
||||
// Budget owner approves the request
|
||||
|
@ -219,6 +236,8 @@ describe.only('Other Fees Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -332,6 +351,8 @@ describe.only('Other Fees Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -463,6 +484,8 @@ describe.only('Other Fees Path - Without Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -644,6 +667,8 @@ describe('Other Fees Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -808,6 +833,8 @@ describe('Other Fees Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -990,6 +1017,8 @@ describe('Other Fees Path - With Files', () => {
|
|||
'Task: Reminder: Check Existing Budget',
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,19 +76,42 @@ const submitWithUser = (
|
|||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
// if (expectAdditionalApprovalInfoPage) {
|
||||
// cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
//
|
||||
// cy.get('button')
|
||||
// .contains(/^Continue$/)
|
||||
// .click();
|
||||
// }
|
||||
if (expectAdditionalApprovalInfoPage === 'Task: Update Application Landscape') {
|
||||
cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 });
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Continue$/)
|
||||
.click();
|
||||
}
|
||||
|
||||
// cy.getBySel('return-to-home-button', { timeout: 60000 });
|
||||
cy.get('button').contains('Return to Home', { timeout: 60000 });
|
||||
cy.logout();
|
||||
};
|
||||
|
||||
//Check if the process instance is completed successfully
|
||||
const checkProcessInstanceCompleted = (
|
||||
username,
|
||||
password,
|
||||
processInstanceId
|
||||
) => {
|
||||
cy.wait(2000);
|
||||
cy.log('========Login with : ', username);
|
||||
cy.log('========processInstanceId: ', processInstanceId);
|
||||
cy.login(username, password);
|
||||
|
||||
cy.wait(1000);
|
||||
cy.visit('/admin/process-instances/find-by-id');
|
||||
cy.get('#process-instance-id-input').type(processInstanceId);
|
||||
|
||||
cy.get('button')
|
||||
.contains(/^Submit$/)
|
||||
.click();
|
||||
|
||||
cy.wait(2000);
|
||||
cy.get('#tag-1 > span').contains('complete');
|
||||
}
|
||||
|
||||
// Software and Licenses Path - Without Files
|
||||
describe.only('Software and Licenses Path - Without Files', () => {
|
||||
Cypress._.times(1, () => {
|
||||
|
@ -226,6 +249,8 @@ describe.only('Software and Licenses Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -390,11 +415,13 @@ describe.only('Software and Licenses Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
// Budget owner rejects the request
|
||||
it('Budget owner rejects', () => {
|
||||
it.only('Budget owner rejects', () => {
|
||||
const username = Cypress.env('requestor_username');
|
||||
const password = Cypress.env('requestor_password');
|
||||
cy.log(`=====username : ${username}`);
|
||||
|
@ -498,6 +525,8 @@ describe.only('Software and Licenses Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -645,6 +674,8 @@ describe.only('Software and Licenses Path - Without Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -786,6 +817,8 @@ describe.only('Software and Licenses Path - Without Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -912,6 +945,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1049,6 +1084,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1145,6 +1182,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1280,6 +1319,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1409,6 +1450,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1614,6 +1657,8 @@ describe('Software and Licenses Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1817,6 +1862,8 @@ describe('Software and Licenses Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1979,6 +2026,8 @@ describe('Software and Licenses Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2181,6 +2230,8 @@ describe('Software and Licenses Path - With Files', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2378,6 +2429,8 @@ describe('Software and Licenses Path - With Files', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2602,6 +2655,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -2836,6 +2891,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3030,6 +3087,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3261,6 +3320,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => {
|
|||
null,
|
||||
'approve'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3481,6 +3542,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => {
|
|||
null,
|
||||
'reject'
|
||||
);
|
||||
|
||||
checkProcessInstanceCompleted(username, password, processInstanceId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
"eslint-plugin-react": "^7.31.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sonarjs": "^0.15.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"safe-regex": "^2.1.1",
|
||||
"ts-migrate": "^0.1.30"
|
||||
|
@ -13286,6 +13287,36 @@
|
|||
"eslint": "^7.5.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unused-imports": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz",
|
||||
"integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-rule-composer": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-rule-composer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz",
|
||||
"integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
|
@ -42460,6 +42491,21 @@
|
|||
"@typescript-eslint/utils": "^5.58.0"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-unused-imports": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz",
|
||||
"integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-rule-composer": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"eslint-rule-composer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz",
|
||||
"integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
|
|
|
@ -113,6 +113,7 @@
|
|||
"eslint-plugin-react": "^7.31.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sonarjs": "^0.15.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"safe-regex": "^2.1.1",
|
||||
"ts-migrate": "^0.1.30"
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.5 KiB |
|
@ -3,9 +3,11 @@ import { Content } from '@carbon/react';
|
|||
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { defineAbility } from '@casl/ability';
|
||||
import React from 'react';
|
||||
import NavigationBar from './components/NavigationBar';
|
||||
|
||||
import HomePageRoutes from './routes/HomePageRoutes';
|
||||
import About from './routes/About';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import AdminRoutes from './routes/AdminRoutes';
|
||||
import ProcessRoutes from './routes/ProcessRoutes';
|
||||
|
@ -14,6 +16,7 @@ import { AbilityContext } from './contexts/Can';
|
|||
import UserService from './services/UserService';
|
||||
import ErrorDisplay from './components/ErrorDisplay';
|
||||
import APIErrorProvider from './contexts/APIErrorContext';
|
||||
import ScrollToTop from './components/ScrollToTop';
|
||||
|
||||
export default function App() {
|
||||
if (!UserService.isLoggedIn()) {
|
||||
|
@ -31,10 +34,12 @@ export default function App() {
|
|||
<BrowserRouter>
|
||||
<NavigationBar />
|
||||
<Content>
|
||||
<ScrollToTop />
|
||||
<ErrorDisplay />
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/*" element={<HomePageRoutes />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/tasks/*" element={<HomePageRoutes />} />
|
||||
<Route path="/process/*" element={<ProcessRoutes />} />
|
||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||
|
|
|
@ -109,7 +109,6 @@ export default function ErrorDisplay() {
|
|||
|
||||
if (errorObject) {
|
||||
const title = 'Error:';
|
||||
window.scrollTo(0, 0); // Scroll back to the top of the page
|
||||
|
||||
errorTag = (
|
||||
<Notification title={title} onClose={() => removeError()} type="error">
|
||||
|
|
|
@ -30,6 +30,7 @@ import { PermissionsToCheck } from '../interfaces';
|
|||
import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||
import { UnauthenticatedError } from '../services/HttpService';
|
||||
import { SPIFF_ENVIRONMENT } from '../config';
|
||||
import appVersionInfo from '../helpers/appVersionInfo';
|
||||
|
||||
// for ref: https://react-bootstrap.github.io/components/navbar/
|
||||
export default function NavigationBar() {
|
||||
|
@ -57,6 +58,15 @@ export default function NavigationBar() {
|
|||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
|
||||
// default to readthedocs and let someone specify an environment variable to override:
|
||||
//
|
||||
let documentationUrl = 'https://spiffworkflow.readthedocs.io';
|
||||
if ('DOCUMENTATION_URL' in window.spiffworkflowFrontendJsenv) {
|
||||
documentationUrl = window.spiffworkflowFrontendJsenv.DOCUMENTATION_URL;
|
||||
}
|
||||
|
||||
const versionInfo = appVersionInfo();
|
||||
|
||||
useEffect(() => {
|
||||
let newActiveKey = '/admin/process-groups';
|
||||
if (location.pathname.match(/^\/admin\/messages\b/)) {
|
||||
|
@ -81,6 +91,15 @@ export default function NavigationBar() {
|
|||
return activeKey === menuItemPath;
|
||||
};
|
||||
|
||||
let aboutLinkElement = null;
|
||||
|
||||
if (Object.keys(versionInfo).length) {
|
||||
aboutLinkElement = <a href="/about">About</a>;
|
||||
}
|
||||
|
||||
const userEmail = UserService.getUserEmail();
|
||||
const username = UserService.getPreferredUsername();
|
||||
|
||||
const profileToggletip = (
|
||||
<div style={{ display: 'flex' }} id="user-profile-toggletip">
|
||||
<Toggletip isTabTip align="bottom-right">
|
||||
|
@ -89,15 +108,18 @@ export default function NavigationBar() {
|
|||
className="user-profile-toggletip-button"
|
||||
type="button"
|
||||
>
|
||||
<div className="user-circle">
|
||||
{UserService.getPreferredUsername()[0].toUpperCase()}
|
||||
</div>
|
||||
<div className="user-circle">{username[0].toUpperCase()}</div>
|
||||
</ToggletipButton>
|
||||
<ToggletipContent className="user-profile-toggletip-content">
|
||||
<p>
|
||||
<strong>{UserService.getPreferredUsername()}</strong>
|
||||
<strong>{username}</strong>
|
||||
</p>
|
||||
<p>{UserService.getUserEmail()}</p>
|
||||
{username !== userEmail && <p>{userEmail}</p>}
|
||||
<hr />
|
||||
{aboutLinkElement}
|
||||
<a target="_blank" href={documentationUrl} rel="noreferrer">
|
||||
Documentation
|
||||
</a>
|
||||
<hr />
|
||||
<Button
|
||||
data-qa="logout-button"
|
||||
|
|
|
@ -162,9 +162,6 @@ export default function ProcessInstanceListTable({
|
|||
const [requiresRefilter, setRequiresRefilter] = useState<boolean>(false);
|
||||
const [lastColumnFilter, setLastColumnFilter] = useState<string>('');
|
||||
|
||||
const [listHasBeenFiltered, setListHasBeenFiltered] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const preferredUsername = UserService.getPreferredUsername();
|
||||
const userEmail = UserService.getUserEmail();
|
||||
|
||||
|
@ -223,6 +220,9 @@ export default function ProcessInstanceListTable({
|
|||
];
|
||||
}, []);
|
||||
|
||||
// this is used from pages like the home page that have multiple tables
|
||||
// and cannot store the report hash in the query params.
|
||||
// it can be used to create a link to the process instances list page to reconstruct the report.
|
||||
const [reportHash, setReportHash] = useState<string | null>(null);
|
||||
|
||||
const [
|
||||
|
@ -321,14 +321,35 @@ export default function ProcessInstanceListTable({
|
|||
// so use a variable instead
|
||||
const processModelSelectionItemsForUseEffect = useRef<ProcessModel[]>([]);
|
||||
|
||||
const clearFilters = useCallback((updateRequiresRefilter: boolean = true) => {
|
||||
setProcessModelSelection(null);
|
||||
setProcessStatusSelection([]);
|
||||
setStartFromDate('');
|
||||
setStartFromTime('');
|
||||
setStartToDate('');
|
||||
setStartToTime('');
|
||||
setEndFromDate('');
|
||||
setEndFromTime('');
|
||||
setEndToDate('');
|
||||
setEndToTime('');
|
||||
setProcessInitiatorSelection(null);
|
||||
setWithOldestOpenTask(false);
|
||||
setSystemReport(null);
|
||||
setSelectedUserGroup(null);
|
||||
if (updateRequiresRefilter) {
|
||||
setRequiresRefilter(true);
|
||||
}
|
||||
if (reportMetadata) {
|
||||
reportMetadata.filter_by = [];
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const getProcessInstances = useCallback(
|
||||
(
|
||||
processInstanceReport: ProcessInstanceReport | null = null
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
) => {
|
||||
if (listHasBeenFiltered) {
|
||||
return;
|
||||
}
|
||||
let reportMetadataBodyToUse: ReportMetadata = {
|
||||
columns: [],
|
||||
filter_by: [],
|
||||
|
@ -341,6 +362,11 @@ export default function ProcessInstanceListTable({
|
|||
}
|
||||
}
|
||||
|
||||
// a bit hacky, clear out all filters before setting them from report metadata
|
||||
// to ensure old filters are cleared out.
|
||||
// this is really for going between the 'For Me' and 'All' tabs.
|
||||
clearFilters(false);
|
||||
|
||||
// this is the code to re-populate the widgets on the page
|
||||
// with values from the report metadata, which is derived
|
||||
// from the searchParams (often report_hash)
|
||||
|
@ -433,7 +459,6 @@ export default function ProcessInstanceListTable({
|
|||
additionalReportFilters,
|
||||
dateParametersToAlwaysFilterBy,
|
||||
filtersEnabled,
|
||||
listHasBeenFiltered,
|
||||
paginationQueryParamPrefix,
|
||||
perPageOptions,
|
||||
processInstanceApiSearchPath,
|
||||
|
@ -441,6 +466,7 @@ export default function ProcessInstanceListTable({
|
|||
setProcessInstancesFromResult,
|
||||
stopRefreshing,
|
||||
systemReportOptions,
|
||||
clearFilters,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -448,11 +474,7 @@ export default function ProcessInstanceListTable({
|
|||
if (!permissionsLoaded) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getReportMetadataWithReportHash() {
|
||||
if (listHasBeenFiltered) {
|
||||
return;
|
||||
}
|
||||
const queryParams: string[] = [];
|
||||
['report_hash', 'report_id'].forEach((paramName: string) => {
|
||||
if (searchParams.get(paramName)) {
|
||||
|
@ -517,7 +539,6 @@ export default function ProcessInstanceListTable({
|
|||
autoReload,
|
||||
filtersEnabled,
|
||||
getProcessInstances,
|
||||
listHasBeenFiltered,
|
||||
permissionsLoaded,
|
||||
reportIdentifier,
|
||||
searchParams,
|
||||
|
@ -632,17 +653,34 @@ export default function ProcessInstanceListTable({
|
|||
reportMetadataToUse.filter_by = filtersToKeep;
|
||||
};
|
||||
|
||||
const getFilterByFromReportMetadata = (reportColumnAccessor: string) => {
|
||||
if (reportMetadata) {
|
||||
return reportMetadata.filter_by.find((reportFilter: ReportFilter) => {
|
||||
return reportColumnAccessor === reportFilter.field_name;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const insertOrUpdateFieldInReportMetadata = (
|
||||
reportMetadataToUse: ReportMetadata,
|
||||
fieldName: string,
|
||||
fieldValue: any
|
||||
) => {
|
||||
removeFieldFromReportMetadata(reportMetadataToUse, fieldName);
|
||||
if (fieldValue) {
|
||||
reportMetadataToUse.filter_by.push({
|
||||
field_name: fieldName,
|
||||
field_value: fieldValue,
|
||||
});
|
||||
let existingReportFilter = getFilterByFromReportMetadata(fieldName);
|
||||
if (existingReportFilter) {
|
||||
existingReportFilter.field_value = fieldValue;
|
||||
} else {
|
||||
existingReportFilter = {
|
||||
field_name: fieldName,
|
||||
field_value: fieldValue,
|
||||
operator: 'equals',
|
||||
};
|
||||
reportMetadataToUse.filter_by.push(existingReportFilter);
|
||||
}
|
||||
} else {
|
||||
removeFieldFromReportMetadata(reportMetadataToUse, fieldName);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -747,11 +785,7 @@ export default function ProcessInstanceListTable({
|
|||
page = 1;
|
||||
|
||||
const newReportMetadata = getNewReportMetadataBasedOnPageWidgets();
|
||||
setListHasBeenFiltered(true);
|
||||
setReportMetadata(newReportMetadata);
|
||||
searchParams.set('per_page', perPage.toString());
|
||||
searchParams.set('page', page.toString());
|
||||
setSearchParams(searchParams);
|
||||
|
||||
const queryParamString = `per_page=${perPage}&page=${page}`;
|
||||
HttpService.makeCallToBackend({
|
||||
|
@ -820,6 +854,7 @@ export default function ProcessInstanceListTable({
|
|||
const formatProcessInstanceStatus = (_row: any, value: any) => {
|
||||
return titleizeString((value || '').replaceAll('_', ' '));
|
||||
};
|
||||
|
||||
const processStatusSearch = () => {
|
||||
return (
|
||||
<MultiSelect
|
||||
|
@ -841,34 +876,12 @@ export default function ProcessInstanceListTable({
|
|||
);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setProcessModelSelection(null);
|
||||
setProcessStatusSelection([]);
|
||||
setStartFromDate('');
|
||||
setStartFromTime('');
|
||||
setStartToDate('');
|
||||
setStartToTime('');
|
||||
setEndFromDate('');
|
||||
setEndFromTime('');
|
||||
setEndToDate('');
|
||||
setEndToTime('');
|
||||
setProcessInitiatorSelection(null);
|
||||
setWithOldestOpenTask(false);
|
||||
setSystemReport(null);
|
||||
setSelectedUserGroup(null);
|
||||
setRequiresRefilter(true);
|
||||
if (reportMetadata) {
|
||||
reportMetadata.filter_by = [];
|
||||
}
|
||||
};
|
||||
|
||||
const processInstanceReportDidChange = (selection: any, mode?: string) => {
|
||||
clearFilters();
|
||||
const selectedReport = selection.selectedItem;
|
||||
setProcessInstanceReportSelection(selectedReport);
|
||||
removeError();
|
||||
setProcessInstanceReportJustSaved(mode || null);
|
||||
setListHasBeenFiltered(false);
|
||||
|
||||
let queryParamString = '';
|
||||
if (selectedReport) {
|
||||
|
@ -939,15 +952,6 @@ export default function ProcessInstanceListTable({
|
|||
setReportColumnToOperateOn(null);
|
||||
};
|
||||
|
||||
const getFilterByFromReportMetadata = (reportColumnAccessor: string) => {
|
||||
if (reportMetadata) {
|
||||
return reportMetadata.filter_by.find((reportFilter: ReportFilter) => {
|
||||
return reportColumnAccessor === reportFilter.field_name;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getNewFiltersFromReportForEditing = (
|
||||
reportColumnForEditing: ReportColumnForEditing
|
||||
) => {
|
||||
|
@ -1597,14 +1601,17 @@ export default function ProcessInstanceListTable({
|
|||
) {
|
||||
hasAccessToCompleteTask = true;
|
||||
}
|
||||
|
||||
let buttonText = 'View';
|
||||
if (hasAccessToCompleteTask && processInstance.task_id) {
|
||||
buttonText = 'Go';
|
||||
}
|
||||
|
||||
buttonElement = (
|
||||
<Button kind="secondary" href={interstitialUrl}>
|
||||
<Button
|
||||
kind="secondary"
|
||||
href={interstitialUrl}
|
||||
style={{ width: '60px' }}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
// @ts-ignore
|
||||
import { Loading, Grid, Column, Button } from '@carbon/react';
|
||||
import { Loading, Button } from '@carbon/react';
|
||||
import { BACKEND_BASE_URL } from '../config';
|
||||
import { getBasicHeaders } from '../services/HttpService';
|
||||
|
||||
|
@ -95,26 +95,17 @@ export default function ProcessInterstitial({
|
|||
return state;
|
||||
};
|
||||
|
||||
const getStatusImage = () => {
|
||||
switch (getStatus()) {
|
||||
case 'RUNNING':
|
||||
return (
|
||||
<Loading description="Active loading indicator" withOverlay={false} />
|
||||
);
|
||||
case 'LOCKED':
|
||||
return <img src="/interstitial/locked.png" alt="Locked" />;
|
||||
case 'READY':
|
||||
case 'REDIRECTING':
|
||||
return <img src="/interstitial/redirect.png" alt="Redirecting ...." />;
|
||||
case 'WAITING':
|
||||
return <img src="/interstitial/waiting.png" alt="Waiting ...." />;
|
||||
case 'COMPLETED':
|
||||
return <img src="/interstitial/completed.png" alt="Completed" />;
|
||||
case 'ERROR':
|
||||
return <img src="/interstitial/errored.png" alt="Errored" />;
|
||||
default:
|
||||
return getStatus();
|
||||
const getLoadingIcon = () => {
|
||||
if (getStatus() === 'RUNNING') {
|
||||
return (
|
||||
<Loading
|
||||
description="Active loading indicator"
|
||||
withOverlay={false}
|
||||
style={{ margin: 'auto' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getReturnHomeButton = (index: number) => {
|
||||
|
@ -129,6 +120,7 @@ export default function ProcessInterstitial({
|
|||
kind="secondary"
|
||||
data-qa="return-to-home-button"
|
||||
onClick={() => navigate(`/tasks`)}
|
||||
style={{ marginBottom: 30 }}
|
||||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
|
@ -138,35 +130,14 @@ export default function ProcessInterstitial({
|
|||
return '';
|
||||
};
|
||||
|
||||
const getHr = (index: number) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<div style={{ padding: '10px 0 50px 0' }}>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
function capitalize(str: string): string {
|
||||
if (str && str.length > 0) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const userMessage = (myTask: ProcessInstanceTask) => {
|
||||
if (!processInstance || processInstance.status === 'completed') {
|
||||
if (!myTask.can_complete && userTasks.includes(myTask.type)) {
|
||||
return (
|
||||
<>
|
||||
<h4 className="heading-compact-01">Waiting on Someone Else</h4>
|
||||
<p>
|
||||
This next task is assigned to a different person or team. There is
|
||||
no action for you to take at this time.
|
||||
</p>
|
||||
</>
|
||||
<p>
|
||||
This next task is assigned to a different person or team. There is
|
||||
no action for you to take at this time.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (shouldRedirect(myTask)) {
|
||||
|
@ -198,6 +169,7 @@ export default function ProcessInterstitial({
|
|||
if (state === 'CLOSED' && lastTask === null) {
|
||||
navigate(`/tasks`);
|
||||
}
|
||||
|
||||
if (lastTask) {
|
||||
return (
|
||||
<>
|
||||
|
@ -215,21 +187,10 @@ export default function ProcessInterstitial({
|
|||
],
|
||||
]}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
{getStatusImage()}
|
||||
<div>
|
||||
<h1 style={{ marginBottom: '0em' }}>
|
||||
{lastTask.process_model_display_name}:{' '}
|
||||
{lastTask.process_instance_id}
|
||||
</h1>
|
||||
<div>Status: {capitalize(getStatus())}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
{data.map((d, index) => (
|
||||
<Grid fullWidth style={{ marginBottom: '1em' }}>
|
||||
<Column md={6} lg={8} sm={4}>
|
||||
{getLoadingIcon()}
|
||||
<div style={{ maxWidth: 800, margin: 'auto', padding: 50 }}>
|
||||
{data.map((d, index) => (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
index < 4
|
||||
|
@ -240,10 +201,9 @@ export default function ProcessInterstitial({
|
|||
{userMessage(d)}
|
||||
</div>
|
||||
{getReturnHomeButton(index)}
|
||||
{getHr(index)}
|
||||
</Column>
|
||||
</Grid>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export default function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { ObjectWithStringKeysAndValues } from '../interfaces';
|
||||
|
||||
const appVersionInfo = () => {
|
||||
const versionInfoFromHtmlMetaTag = document.querySelector(
|
||||
'meta[name="version-info"]'
|
||||
);
|
||||
let versionInfo: ObjectWithStringKeysAndValues = {};
|
||||
if (versionInfoFromHtmlMetaTag) {
|
||||
const versionInfoContentString =
|
||||
versionInfoFromHtmlMetaTag.getAttribute('content');
|
||||
if (
|
||||
versionInfoContentString &&
|
||||
versionInfoContentString !== '%REACT_APP_VERSION_INFO%'
|
||||
) {
|
||||
versionInfo = JSON.parse(versionInfoContentString);
|
||||
}
|
||||
}
|
||||
|
||||
return versionInfo;
|
||||
};
|
||||
|
||||
export default appVersionInfo;
|
|
@ -0,0 +1,44 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// https://stackoverflow.com/a/59843241/6090676
|
||||
// useful for determining what state has changed that caused useEffect to trigger.
|
||||
// just change the useEffect in question to useEffectDebugger
|
||||
const usePrevious = (value: any, initialValue: any) => {
|
||||
const ref = useRef(initialValue);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
};
|
||||
export default function useEffectDebugger(
|
||||
effectHook: any,
|
||||
dependencies: any,
|
||||
dependencyNames: any = []
|
||||
) {
|
||||
const previousDeps = usePrevious(dependencies, []);
|
||||
|
||||
const changedDeps = dependencies.reduce(
|
||||
(accum: any, dependency: any, index: any) => {
|
||||
if (dependency !== previousDeps[index]) {
|
||||
const keyName = dependencyNames[index] || index;
|
||||
return {
|
||||
...accum,
|
||||
[keyName]: {
|
||||
before: previousDeps[index],
|
||||
after: dependency,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return accum;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (Object.keys(changedDeps).length) {
|
||||
console.log('[use-effect-debugger] ', changedDeps);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(effectHook, dependencies);
|
||||
}
|
|
@ -478,22 +478,28 @@ svg.notification-icon {
|
|||
|
||||
.user_instructions_0 {
|
||||
filter: opacity(1);
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.user_instructions_1 {
|
||||
filter: opacity(60%);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.user_instructions_2 {
|
||||
filter: opacity(40%);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.user_instructions_3 {
|
||||
filter: opacity(20%);
|
||||
font-size: 9em;
|
||||
}
|
||||
|
||||
.user_instructions_4 {
|
||||
filter: opacity(10%);
|
||||
font-size: 8em;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
|
@ -562,3 +568,7 @@ svg.notification-icon {
|
|||
.user-circle:nth-child(n+11) {
|
||||
background-color: #8e8e8e;
|
||||
}
|
||||
|
||||
.version-info-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
|
|
@ -112,6 +112,8 @@ export interface ProcessReference {
|
|||
is_primary: boolean;
|
||||
}
|
||||
|
||||
export type ObjectWithStringKeysAndValues = { [key: string]: string };
|
||||
|
||||
export interface ProcessFile {
|
||||
content_type: string;
|
||||
last_modified: string;
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// @ts-ignore
|
||||
import { Table } from '@carbon/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import appVersionInfo from '../helpers/appVersionInfo';
|
||||
import { ObjectWithStringKeysAndValues } from '../interfaces';
|
||||
import HttpService from '../services/HttpService';
|
||||
|
||||
export default function About() {
|
||||
const frontendVersionInfo = appVersionInfo();
|
||||
const [backendVersionInfo, setBackendVersionInfo] =
|
||||
useState<ObjectWithStringKeysAndValues | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVersionInfoResponse = (
|
||||
response: ObjectWithStringKeysAndValues
|
||||
) => {
|
||||
setBackendVersionInfo(response);
|
||||
};
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/debug/version-info`,
|
||||
successCallback: handleVersionInfoResponse,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const versionInfoFromDict = (
|
||||
title: string,
|
||||
versionInfoDict: ObjectWithStringKeysAndValues | null
|
||||
) => {
|
||||
if (versionInfoDict !== null && Object.keys(versionInfoDict).length) {
|
||||
const tableRows = Object.keys(versionInfoDict)
|
||||
.sort()
|
||||
.map((key) => {
|
||||
const value = versionInfoDict[key];
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className="version-info-column">
|
||||
<strong>{key}</strong>
|
||||
</td>
|
||||
<td className="version-info-column">{value}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<h2 title="This information is configurable by specifying values in version_info.json in the app at build time">
|
||||
{title}
|
||||
</h2>
|
||||
<Table striped bordered>
|
||||
<tbody>{tableRows}</tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>About</h1>
|
||||
{versionInfoFromDict('Frontend version information', frontendVersionInfo)}
|
||||
{versionInfoFromDict('Backend version information', backendVersionInfo)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -22,7 +22,7 @@ export default function InProgressInstances() {
|
|||
const titleText = `This is a list of instances with tasks that are waiting for the ${userGroup} group.`;
|
||||
const headerElement = (
|
||||
<h2 title={titleText} className="process-instance-table-header">
|
||||
Instances with tasks waiting for <strong>{userGroup}</strong>
|
||||
Waiting for <strong>{userGroup}</strong>
|
||||
</h2>
|
||||
);
|
||||
return (
|
||||
|
@ -61,7 +61,7 @@ export default function InProgressInstances() {
|
|||
'This is a list of instances that have tasks that you can complete.';
|
||||
const waitingForMeHeaderElement = (
|
||||
<h2 title={waitingForMeTitleText} className="process-instance-table-header">
|
||||
Instances with tasks waiting for me
|
||||
Waiting for me
|
||||
</h2>
|
||||
);
|
||||
|
||||
|
|
|
@ -88,19 +88,15 @@ function TypeaheadWidget({
|
|||
);
|
||||
}
|
||||
|
||||
enum FormSubmitType {
|
||||
Default,
|
||||
Draft,
|
||||
}
|
||||
|
||||
export default function TaskShow() {
|
||||
const [task, setTask] = useState<Task | null>(null);
|
||||
const [userTasks] = useState(null);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
// save current form data so that we can avoid validations in certain situations
|
||||
const [currentFormObject, setCurrentFormObject] = useState<any>({});
|
||||
const [noValidate, setNoValidate] = useState<boolean>(false);
|
||||
|
||||
const [taskData, setTaskData] = useState<any>(null);
|
||||
|
||||
const { addError, removeError } = useAPIError();
|
||||
|
||||
|
@ -115,11 +111,11 @@ export default function TaskShow() {
|
|||
useEffect(() => {
|
||||
const processResult = (result: Task) => {
|
||||
setTask(result);
|
||||
setTaskData(result.data);
|
||||
setDisabled(false);
|
||||
if (!result.can_complete) {
|
||||
navigateToInterstitial(result);
|
||||
}
|
||||
window.scrollTo(0, 0); // Scroll back to the top of the page
|
||||
|
||||
/* Disable call to load previous tasks -- do not display menu.
|
||||
const url = `/v1.0/process-instances/for-me/${modifyProcessIdentifierForPathParam(
|
||||
|
@ -167,26 +163,30 @@ export default function TaskShow() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = (
|
||||
formObject: any,
|
||||
_event: any,
|
||||
submitType: FormSubmitType = FormSubmitType.Default
|
||||
) => {
|
||||
const handleFormSubmit = (formObject: any, _event: any) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataToSubmit = formObject?.formData;
|
||||
if (!dataToSubmit) {
|
||||
navigate(`/tasks`);
|
||||
return;
|
||||
}
|
||||
let queryParams = '';
|
||||
if (submitType === FormSubmitType.Draft) {
|
||||
|
||||
// if validations are turned off then save as draft
|
||||
if (noValidate) {
|
||||
queryParams = '?save_as_draft=true';
|
||||
}
|
||||
setDisabled(true);
|
||||
removeError();
|
||||
delete dataToSubmit.isManualTask;
|
||||
|
||||
// NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values
|
||||
// so there is no way to clear out a field that previously had a value.
|
||||
// To resolve this, we could potentially go through the object that we are posting (either in here or in
|
||||
// HttpService) and translate all undefined values to null.
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/tasks/${params.process_instance_id}/${params.task_id}${queryParams}`,
|
||||
successCallback: processSubmitResult,
|
||||
|
@ -301,9 +301,16 @@ export default function TaskShow() {
|
|||
return errors;
|
||||
};
|
||||
|
||||
const updateFormData = (formObject: any) => {
|
||||
currentFormObject.formData = formObject.formData;
|
||||
setCurrentFormObject(currentFormObject);
|
||||
// This turns off validations and then dispatches the click event after
|
||||
// waiting a second to give the state time to update.
|
||||
// This is to allow saving the form without validations causing issues.
|
||||
const handleSaveAndCloseButton = () => {
|
||||
setNoValidate(true);
|
||||
setTimeout(() => {
|
||||
(document.getElementById('our-very-own-form') as any).dispatchEvent(
|
||||
new Event('submit', { cancelable: true, bubbles: true })
|
||||
);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const formElement = () => {
|
||||
|
@ -312,11 +319,9 @@ export default function TaskShow() {
|
|||
}
|
||||
|
||||
let formUiSchema;
|
||||
let taskData = task.data;
|
||||
let jsonSchema = task.form_schema;
|
||||
let reactFragmentToHideSubmitButton = null;
|
||||
if (task.typename === 'ManualTask') {
|
||||
taskData = {};
|
||||
jsonSchema = {
|
||||
type: 'object',
|
||||
required: [],
|
||||
|
@ -357,14 +362,12 @@ export default function TaskShow() {
|
|||
closeButton = (
|
||||
<Button
|
||||
id="close-button"
|
||||
onClick={handleSaveAndCloseButton}
|
||||
disabled={disabled}
|
||||
kind="secondary"
|
||||
title="Save changes without submitting."
|
||||
onClick={() =>
|
||||
handleFormSubmit(currentFormObject, null, FormSubmitType.Draft)
|
||||
}
|
||||
>
|
||||
Close
|
||||
Save and Close
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -399,17 +402,18 @@ export default function TaskShow() {
|
|||
<Grid fullWidth condensed>
|
||||
<Column sm={4} md={5} lg={8}>
|
||||
<Form
|
||||
id="our-very-own-form"
|
||||
disabled={disabled}
|
||||
formData={taskData}
|
||||
onChange={(obj: any) => setTaskData(obj.formData)}
|
||||
onSubmit={handleFormSubmit}
|
||||
schema={jsonSchema}
|
||||
uiSchema={formUiSchema}
|
||||
widgets={widgets}
|
||||
validator={validator}
|
||||
onChange={updateFormData}
|
||||
customValidate={customValidate}
|
||||
noValidate={noValidate}
|
||||
omitExtraData
|
||||
liveOmit
|
||||
>
|
||||
{reactFragmentToHideSubmitButton}
|
||||
</Form>
|
||||
|
|
|
@ -56,6 +56,7 @@ backendCallProps) => {
|
|||
Object.assign(httpArgs, { body: postBody });
|
||||
} else if (typeof postBody === 'object') {
|
||||
if (!objectIsEmpty(postBody)) {
|
||||
// NOTE: stringify strips out keys with value undefined
|
||||
Object.assign(httpArgs, { body: JSON.stringify(postBody) });
|
||||
Object.assign(headers, { 'Content-Type': 'application/json' });
|
||||
}
|
||||
|
|
|
@ -100,12 +100,15 @@ export default function BaseInputTemplate<
|
|||
if (type === 'date') {
|
||||
// display the date in a date input box as the config wants.
|
||||
// it should in be y-m-d when it gets here.
|
||||
let dateValue: string | null = '';
|
||||
let dateValue: string | null = value;
|
||||
if (value || value === 0) {
|
||||
if (value.length < 10) {
|
||||
dateValue = value;
|
||||
} else {
|
||||
dateValue = ymdDateStringToConfiguredFormat(value);
|
||||
try {
|
||||
dateValue = ymdDateStringToConfiguredFormat(value);
|
||||
// let the date component and form validators handle bad dates and do not blow up
|
||||
} catch (RangeError) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|