You are on page 1of 36

<style>

/*###########################
NAME: DWT CSS Styles
AUTHOR: Luciano Veronese
DATE: November 2022
VERSION: 2.2
############################*/
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
@import url("https://use.fontawesome.com/releases/v5.10.1/css/all.css");

/* CONFIGURATION PARAMETERS */
.StickOnTop {
/* MOST COMMONLY UPDATED */
--chevron-statusvl-alias: WFStatus;
--chevron-current-color-scheme: cs-default;
--chevron-display-for-new-records: no;
--chevron-app-alias: ;
--chevron-style: standard;
--chevron-tail-style: standard;
--chevron-tail-coloring: inherited;
--chevron-displaylayervl-alias: ;
--chevron-display-layers: ;
--chevron-divcontainer: stuck-on-top;
/* RARELY UPDATED */
--chevron-phasedone-icon: done_outline;
--chevron-checkmark_if_done: yes;
--chevron-display-status-persistent: yes;
--chevron-background: white;
--chevron-height: 3rem;
--chevron-phase-maxwidth: auto;
--chevron-phase-minwidth: auto;
--chevron-stretched-phases: 100%;
--chevron-animation-delay: 0;
--chevron-margin-top: 0px;
--chevron-margin-bottom: 5px;
--currsubphase-title-color: #000000;
--currsubphase-text-maxsize: 0.7rem;
--currsubphase-backgroundcolor: #DDDDDD;
--currsubphase-bottom-border-color: #a80520;
--popover-background-color: #f2f2f2;
--popover-title-color: #031f5a;
--popover-title-size: 1.1rem;
--popover-subtitle-color: #031f5a;
--popover-subtitle-size: 0.8rem;
--popover-subphase-color: #a80520;
--popover-subphase-size: 0.8rem;
--chevron-phasedone-color: #FFFFFF;
--phase-color-default: lightgray;
--phase-icon-size: 2.0rem;
--phase-title-maxsize: 1.1rem;
--phase-icon-color: #020202;
--phase-subphasebar-past-color: #3a3a3a;
--phase-subphasebar-current-color: #3a3a3a;
--phase-subphasebar-future-color: #3a3a3a;
}

/* COLOR THEMES */
.cs-default {
--phase-color-past: #00A357;
--phase-color-current:#176DC2;
--phase-color-future: #9E9E9E;
--phase-title-past-color: #FFFFFF;
--phase-title-current-color: #FFFFFF;
--phase-title-future-color: #212121;
--phase-subphasebar-past-color: #185c04;
--phase-subphasebar-current-color: #0c4379;
--phase-subphasebar-future-color: #333634;
--chevron-phasedone-color: #FFFFFF;
}

.cs-icystone {
--phase-color-past: #6B799E;
--phase-color-current:#EBC57C;
--phase-color-future: #a6c2ce;
--phase-title-past-color: #CCC;
--phase-title-current-color: #333;
--phase-title-future-color: #0a1136;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #FFFFFF;
}

.cs-USA {
--phase-color-past: #1F1A4F;
--phase-color-current: #C82024;
--phase-color-future: #EEEEEE;
--phase-title-past-color: #EEE;
--phase-title-current-color: #EEE;
--phase-title-future-color: #0a1136;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #EEE;
}

.cs-wild {
--phase-color-past: #68c077;
--phase-color-current: #FFD55A;
--phase-color-future: #47547e;
--phase-title-past-color: #EEE;
--phase-title-current-color: #770000;
--phase-title-future-color: #eee;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #EEE;
}

.cs-corona {
--phase-color-past: #005A9C;
--phase-color-current: #FFCB05;
--phase-color-future: #EEE;
--phase-title-past-color: #EEE;
--phase-title-current-color: #333;
--phase-title-future-color: #333;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #FFCB05;
}

.cs-greece {
--phase-color-past: #EB8F90;
--phase-color-current: #FFB471;
--phase-color-future: #ADBED2;
--phase-title-past-color: #333;
--phase-title-current-color: #770000;
--phase-title-future-color: #333;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #333;
}

.cs-macarons {
--phase-color-past: #B7DDE0;
--phase-color-current: #FFD0D6;
--phase-color-future: #FEE19F;
--phase-title-past-color: #333;
--phase-title-current-color: #770000;
--phase-title-future-color: #333;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #333;
}

.cs-jeweldark {
--phase-color-past: #2B628B;
--phase-color-current: #92363B;
--phase-color-future: #A87932;
--phase-title-past-color: #DDDDDD;
--phase-title-current-color: #DDDDDD;
--phase-title-future-color: #DDDDDD;
--phase-subphasebar-past-color: #a31800;
--phase-subphasebar-current-color: #a31800;
--phase-subphasebar-future-color: #a31800;
--chevron-phasedone-color: #DDDDDD;
}
/*### END OF CSS USER DEFINED CONFIG ###*/

.DWTContainer {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center; /*center, flex-start, flex-end*/
min-width: fit-content;
margin-right: 0; /* CHANGE calc(var(--chevron-height) / 2); Zero if flat tail
*/
margin-top: var(--chevron-margin-top);
margin-bottom: var(--chevron-margin-bottom);
}

.DWTPhaseContainer {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: var(--chevron-stretched-phases);
}

.DWTPhase {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: var(--chevron-stretched-phases);
max-width: var(--chevron-phase-maxwidth);
min-width: var(--chevron-phase-minwidth);
height: var(--chevron-height);
background-color: var(--phase-color-default);
color: black;
cursor: help;
}

.DWTPhase::before {
position: absolute;
content: "";
width: 0px;
height: 0px;
border-left: calc(var(--chevron-height) / 2) solid var(--chevron-background);
border-right: 0px solid transparent;
border-bottom: calc(var(--chevron-height) / 2) solid transparent;
border-top: calc(var(--chevron-height) / 2) solid transparent;
left: 0%;
}

.DWTPhase[isChevronFlat='true']::before {
position: absolute;
content: "";
width: 0px;
height: 0px;
border: none;
left: 0%;
}

.DWTPhase[isTailFlat='false']::after {
position: absolute;
content: "";
width: 0px;
height: 0px;
left: 100%;
border-left: calc(var(--chevron-height) / 2) solid var(--phase-color-default);
border-right: 0px solid transparent;
border-bottom: calc(var(--chevron-height) / 2) solid transparent;
border-top: calc(var(--chevron-height) / 2) solid transparent;
z-index: 1;
}

.DWTPhase[isChevronFlat='true']::after {
position: absolute;
content: "";
width: 0px;
height: 0px;
left: 100%;
border: none!important;
z-index: 1;
}

.DWTPhaseLabel {
display: flex;
margin-left: calc(var(--chevron-height) / 2);
}

.DWTPhaseLabelIcon {
display: flex;
align-items: center;
justify-content: center;
max-width: 1rem;
}

.DWTPhaseLabelIcon .material-icons {
font-size: clamp(0.3rem, 0.6vw + 1rem, var(--phase-icon-size));
color: var(--phase-icon-color);
/*padding: 0.3em;*/
}

.DWTPhaseLabelText {
display: flex;
align-items: center;
justify-content: center;
padding-left: 0.5rem;
color: var(--phase-title-color);
font-size: clamp(0.3rem, 0.5vw + 0.6rem, var(--phase-title-maxsize));
}

/* Gap between chevrons */


.DWTPhaseContainer:not(:first-child) {
margin-left: 2px;
}

.DWTPhaseContainer:not(:first-child) .DWTPhaseLabel {
}

.DWTPhaseContainer:nth-child(1) .DWTPhase[Status='P']::before,
.DWTPhaseContainer:nth-child(1) .DWTPhase[Status='C']::before {
display: none;
border-right: 0;
border-left: 0;
border-top: 0;
}

.DWTPhase[Status='P'] {
background-color: var( --phase-color-past, lightgray);
}
.DWTPhase[Status='P']::after {
border-left: calc(var(--chevron-height) / 2) solid var( --phase-color-past,
lightgray);
}

.DWTPhase[Status='C'] {
background-color: var( --phase-color-current, lightgray);
}
.DWTPhase[Status='C']::after {
border-left: calc(var(--chevron-height) / 2) solid var( --phase-color-current,
lightgray);
}
.DWTPhase[Status='F'] {
background-color: var( --phase-color-future, lightgray);
}
.DWTPhase[Status='F']::after {
border-left: calc(var(--chevron-height) / 2) solid var( --phase-color-future,
lightgray);
}
/* Icon and label colors should be the same. If specific colors are not define,
fall back to a default */
.DWTPhase[Status='P'] .DWTPhaseLabel .DWTPhaseLabelText, .DWTPhase[Status='P'] .DWT
PhaseLabel .DWTPhaseLabelIcon .material-icons {
color: var( --phase-title-past-color, var(--phase-icon-color));
margin-left: 0.8rem;
}
.DWTPhase[Status='C'] .DWTPhaseLabel .DWTPhaseLabelText, .DWTPhase[Status='C'] .DWT
PhaseLabel .DWTPhaseLabelIcon .material-icons {
color: var( --phase-title-current-color, var(--phase-icon-color));
}
.DWTPhase[Status='F'] .DWTPhaseLabel .DWTPhaseLabelText, .DWTPhase[Status='F'] .DWT
PhaseLabel .DWTPhaseLabelIcon .material-icons {
color: var( --phase-title-future-color, var(--phase-icon-color));
}

.DWTSubPhaseContainer {
position: relative;
display: flex;
flex-wrap: nowrap;
justify-content: center; /* Chevron alignment when not stretched: center, flex-
start, flex-end*/
align-items: center;
flex-direction: column;
width: 100%;
}

.DWTSubphaseBar {
display: flex;
width: 99.0%;
min-height: 0.2rem;
margin-top: -0.2rem;

border: 0;
border-top-left-radius: 2rem;
border-top-right-radius: 2rem;
}

.DWTSubphaseBar[Status='P'] {
background-color: var(--phase-subphasebar-past-color);;
}

.DWTSubphaseBar[Status='C'] {
background-color: var(--phase-subphasebar-current-color);;
}

.DWTSubphaseBar[Status='F'] {
background-color: var(--phase-subphasebar-future-color);;
}

.DWTCurrentSubphase {
color: var(--currsubphase-title-color);
font-size: clamp(0.3rem, 0.4vw + 0.4rem, var(--currsubphase-text-maxsize));
margin-top: 0px;
z-index: 1;
min-height: .8em;
width: auto;
background-color: var(--currsubphase-backgroundcolor);
padding: 0 4px 0 4px;
border-bottom: 3px solid;
border-bottom-color: var(--currsubphase-bottom-border-color);
pointer-events: none;
}

summary {
cursor: pointer;
outline: none;
font-size: 1.7em;
line-height: 0.3;
vertical-align: bottom;
position: absolute;
z-index:1;
}
summary:hover {
color: #AA0000;
}
summary::marker {
display: none;
}
summary::after {
content: "+";
line-height: 1.2;
color: #AAA;
float: left;
font-size: 1.2em;
font-weight: bold;
margin: -10px 10px 0 0;
padding: 0;
text-align: center;
width: 5px;
}

details[open] summary {
margin-top: 1.5rem; /* Margin from top of the controller */
}

details[open] summary::after {
content: "=";
}

details[open] summary::maker {
margin-top: 2rem;
}

details[open] summary ~ * {
animation: sweep 0.5s ease-in-out;
}

@keyframes sweep {
0% {opacity: 0;}
100% {opacity: 1;}
}

#DWTMessage {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
color: white;
font-size: 1.3rem;
padding: 0.2rem;
}

.tippy-box[data-theme~='custom'] {
background-color: var(--popover-background-color);
color: black;
font-family: Raleway, sans-serif;
font-size: 18px;
border-top: 1px solid #888;
border-bottom: 1px solid #888;
z-index: 2;
}

/*** Tooltip/Popover styles ***/

.tooltip-title {
color: var(--popover-title-color);
font-size: var(--popover-title-size);
text-align: center;
display: block;
padding-top: 5px;
}

.tooltip-subtitle {
color: var(--popover-subtitle-color);
font-size: var(--popover-subtitle-size);
text-align: left;
display: block;
padding-top: 5px;
}

.tooltip-subphases {
color: var(--popover-subphase-color);
font-size: var(--popover-subphase-size);
margin-top:0;
text-align: left;
display: block;
padding-top: 2px;
}

/*** Tooltip/Popover styles ***/


.DWTspinner {
-webkit-animation: rotate 2s linear infinite;
animation: rotate 2s linear infinite;
z-index: 1;
}
.DWTspinner .path {
stroke: #176DC2;
stroke-linecap: round;
-webkit-animation: dash 1.5s ease-in-out infinite;
animation: dash 1.5s ease-in-out infinite;
}

@-webkit-keyframes rotate {
100% {
transform: rotate(360deg);
}
}

@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@-webkit-keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
/* Display Layer icon overlayed on top the chevrons */
.DWTDisplayLayerContainer {
width: 1.3rem;
height: 1.3rem;
background-color: rgb(211, 194, 6);
margin-bottom: 0.2rem;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
border-radius: 50%;
position:absolute;
z-index: 2;
top: 0.2rem;
right: 0rem;
display: none;
}

.DWTDisplayLayerTag {
width: 1.1rem;
height: 1.1rem;
background-color: rgb(211, 194, 6);
border-radius: 50%;
color: darkred;
font-weight: bold;
font-size: 1rem;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
</style>
<script src="https://unpkg.com/@popperjs/core@2"></script>
<script src="https://unpkg.com/tippy.js@6"></script>
<script>
/
*##################################################################################
###
NAME: DWT - Dynamic Workflow Tracker
AUTHOR: Luciano Veronese
DATE: November 2022
VERSION: 2.2
DESCRIPTION: display a visual representation of the main phases of a workflow
whose
phases are described by a values list. The values list can be hierarchical (2
levels only)
The second level is aimed to describe sub-phases of a given phase. This is useful
to
describe the source phase when landing into a given phase (the direction we are
coming from)
The custom object is fully customizable through CSS properites and supports
popovers
Popovers are configured through a JSON content dropped into the Description field
of the
values list items. Please, refer to the documentation for the supported
properties.
Material Design Icons are available here: https://fonts.google.com/icons

DEPENDENCIES: popovers depend on a library stored in a CDN.


If internet is not available, popovers are not available, but the tracker works

CHANGELOG:
1.0 - Initial release, restricted user testing
1.1 - Bug fix: Action dropdown hidden by DWT
1.2 - Bug fix: collapse did not minimize
1.3 - Bug Fix: toolbar dropdown menus hidden by DWT. Now DWT can coexist with the
ARCO COs
2.0 - Changed the API communication layer (faster)
- The DWT can now be displayed for new record
- Improved error checking and display
- Fixed bug that duplicated the DWT when tabs in form and switched tab
- The collapse/expand status can be configured to be persistent (cached)
- The color of the subphase bar can be configured
- Flat tail: the last phase chevron can now be flat instead of arrow-spaed
- Display Layers: phases can now be associated to Layers (numbers) and
selectectively displayed in a static or dynamic way
- The active phase for new records can be set through the default item set in
the VL
2.1 - Introduced the management of Layers for sub-phases and theme-specific
subphase colors
2.2 - Name changed from CWT to DWT (Dynamic Workflow Tracker)
- Fixed a visualization bug (checkmark of last chevron)
- Added new option to replace chevrons (triangles) with vertical lines
(separators)
- Added the possibility to instanciate multiple trackers in the same layout

###################################################################################
##*/

var DWT = (function () {

// The Custom Object code is wrapped by the module design pattern to bound the
namespace
// Only the DisplayDWT function is made availble outside the module
(DWT.DisplayDWT())

// Constants
const DWT_STYLE_STANDARD="standard"
const DWT_STYLE_FLAT="flat"
const DWT_CACHE_BASE = "DWTCACHE-v1-" // Name of the cached values in the web
broser local storage
const STS_Past = "P"
const STS_Current = "C"
const STS_Future = "F"
const DEFAULT_MESSAGE_BKGCOLOR = '#990000'
const ARCHER_TOP_DIV = "toolbar-app-buttons" // Append below toolbar
const DEFAULT_VALUES_LIST = "WFStatus";
const DIV_ID_STICKONTOP = "StickOnTop"

let DWTcache = null

/*#################################################################################
####
The following functions are wrappers of the REST APIs aimed to allow reading
the
values list with ths chevron status.

###################################################################################
##*/

const FetchRequestTemplate = {
mode: 'cors',
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache',
// 'accept':
'application/json,text/html,application/xhtml+xml,application/xmlq=0.9,*/*;q=0.8',
'accept':
'application/json,text/html,application/xhtml+xml,application/xmlq=0.9,/;q=0.8',
'content-type': 'application/json; charset=utf-8'
}
}

//--- GetJSONfromHTML ---


// null = error in JSON
// "" = no JSON (typical case with no options)
// != "" valid JSON
function GetJSONfromHTML(desc) {
if (desc == null) // A VL items whose Description has never been edited,
retruns null
return ("")
// Sometimes the unicode 0x200b character is included by Archer...
// This must be removed otherwise JSON.parse fails
desc = desc.replace(/[\u200B-\u200D\uFEFF]/g, '')
let tempDivElement = document.createElement("div")
tempDivElement.innerHTML = desc
let txt = tempDivElement.textContent || tempDivElement.innerText || ""

if (txt.length < 2) { // No JSON object or empty (characters "{}")


return ("")
}
try {
let jdesc = JSON.parse(txt)
return (jdesc)
} catch (e) {
return (null)
}
}

class ArcherSessions {
constructor(Scope, Baseurl, csrfToken, SessionToken) {
this.Scope = Scope
this.Baseurl = Baseurl
this.csrfToken = csrfToken
this.SessionToken = SessionToken
this.AppList = []
this.metaCache = null
this.context = {
RecordId: 0,
AppAlias: "",
StatusFieldAlias: "",
DisplayLayerAlias: "",
DisplayStatusPersistent: null,
SummaryDetailsDiv: null, // summary/details element to
collapse/expand the DWT
ChevronStyle: "",
TailStyle: "",
DisplayForNewRecords: "",
CheckmarkIfDone: false,
DefaultPhaseLayer: [],
PhaseLayer: [],
DWTcontainerEl: null
}
}
}
class VLItems {
constructor(Name, NameId, ParentName, ParentId, Description, NumericValue,
SortOrder, IsActive, IsDefault) {
this._Name = Name
this._NameId = NameId
this._ParentName = ParentName
this._ParentId = ParentId
this._Description = Description
this._NumericValue = NumericValue
this._SortOrder = SortOrder
this._IsActive = IsActive
this._IsDefault = IsDefault
this._Status = ''
this._Step = null
this._SubStep = null
this._IsFirst = false
this._IsLast = false
this._LayerMatch = false
}
get Name() {
return (this._Name)
}
set Name(name) {
this._Name = name
}
get NameId() {
return (this._NameId)
}
set NameId(name) {
this._NameId = name
}
get ParentName() {
return (this._ParentName)
}
set ParentName(pname) {
this._ParentName = pname
}
get ParentId() {
return (this._ParentId)
}
set ParentId(pid) {
this._ParentId = pid
}
get Description() {
return (this._Description)
}
set Description(desc) {
this._Description = desc
}
get NumericValue() {
return (this._NumericValue)
}
set NumericValue(nv) {
this._NumericValue = nv
}
get SortOrder() {
return (this._SortOrder)
}
set SortOrder(so) {
this._SortOrder = so
}
get IsActive() {
return (this._IsActive)
}
set IsActive(ia) {
this._IsActive = ia
}
get IsDefault() {
return (this._IsDefault)
}
set IsDefault(idef) {
this._IsDefault = idef
}
get Status() {
return (this._Status)
}
set Status(sts) {
this._Status = sts
}
get Step() {
return (this._Step)
}
set Step(st) {
this._Step = st
}
get SubStep() {
return (this._SubStep)
}
set SubStep(ss) {
this._SubStep = ss
}
get IsFirst() {
return (this._IsFirst)
}
set IsFirst(sts) {
this._IsFirst = sts
}
get IsLast() {
return (this._IsLast)
}
set IsLast(sts) {
this._IsLast = sts
}
get LayerMatch() {
return (this._LayerMatch)
}
set LayerMatch(sts) {
this._LayerMatch = sts
}
}
//--- AddSessionTokenToHeaders ---
// Helper function to build the http header
function AddSessionTokenToHeaders(ThisArcherSession, FetchRequest) {
// Depending on the type of session, att the proper authorization token
if (ThisArcherSession.Scope == 'Internal') {
FetchRequest.headers = {
...FetchRequest.headers,
...{
'x-csrf-token': ThisArcherSession.csrfToken
}
}
} else {
FetchRequest.headers.Authorization = 'Archer session-id="' +
ThisArcherSession.SessionToken + '"'
}
return
}

//--- ConnectToArcher ---


// This is a simplified version of the corresponding JSA call.
// It assumes the session is established from a C.O
const ConnectToArcher = () => {
let NewArcherSession, baseURL
let csrfToken = window.sessionStorage ? window.sessionStorage.getItem("x-
csrf-token") : parent.parent.ArcherApp.globals['xCsrfToken']
if (csrfToken) {
// Get the baseurl from the current session: this will be used in the
REST calls
baseURL = window.location.protocol + '//' + window.location.host +
parent.parent.ArcherApp.globals['baseUrl']
if (baseURL.endsWith("/") == false)
baseURL += "/"
// A singleton is not used as the script lifecyle is the same as the
page's
NewArcherSession = new ArcherSessions('Internal', baseURL, csrfToken,
null)
} else {
throw new Error('Cannot connect with Archer: Archer URL not defined')
}
return (NewArcherSession)
}

//--- jsaPOSTwithGEToverride ---


// Invoke a REST call to get data, using a POST to pass params and a GET
override
const jsaPOSTwithGEToverride = async (ThisArcherSession, endpoint,
requestbody) => {
if (ThisArcherSession === null)
throw new Error('Cannot connect with Archer: No Active Session')

const requestUrl = ThisArcherSession.Baseurl + endpoint


let contentRequest = {
...FetchRequestTemplate
}
// This call can be GET or POST. The latter is used to specify a filter,
// but the http override must be used to use the body, otherwise error 405
contentRequest.method = 'POST'
AddSessionTokenToHeaders(ThisArcherSession, contentRequest)
contentRequest.headers = {
...contentRequest.headers,
...{
'X-Http-Method-Override': 'GET'
}
}

// Define the body, which is typically a filter


contentRequest.body = JSON.stringify({
Value: requestbody
})

const response = await fetch(requestUrl, contentRequest)


if (!response.ok)
throw new Error(`[jsaPOSTwithGEToverride] - Fetch error "$
{ response.status})"`)

const responseJ = await response.json()


if (responseJ[0].RequestedObject == null)
throw new Error(`[jsaPOSTwithGEToverride] - No JSON content`)

return responseJ
}

//------------ jsaGetAppIdFromAppAlias ------------


// Get the Application Id from the Application Alias
const jsaGetAppIdFromAppAlias = async (ThisArcherSession, AppAlias) => {
if (!AppAlias)
throw new Error(`Wrong Application Alias ${AppAlias}`)
let endpoint = "api/core/system/application/"
let requestbody = `?$filter=Alias eq '${AppAlias}'&$select=Id,Alias`
let responseJ = await jsaPOSTwithGEToverride(ThisArcherSession, endpoint,
requestbody)
return responseJ[0].RequestedObject.Id
}

//------------jsaGetLevelIdFromFieldAliasAndAppId ------------
// Get the Level Id starting from the Application Id and Field Alias
const jsaGetLevelIdFromFieldAliasAndAppId = async (ThisArcherSession,
FieldAlias, AppId) => {

if (!FieldAlias || !AppId)
throw new Error(`Wrong Field Alias (${FieldAlias}) or Application Alias
(${AppId})`)

let endpoint = "api/core/system/fielddefinition/application/"+AppId


let requestbody = `?$filter=Alias eq '$
{FieldAlias}'&$select=Type,Id,LevelId`
let responseJ = await jsaPOSTwithGEToverride(ThisArcherSession, endpoint,
requestbody)
return responseJ[0].RequestedObject.LevelId
}

//------------ jsaGetVLIdFromLevelIdAndVLAlias ------------


// Get the Id of the values list from the level Id and the values list alias
const jsaGetVLIdFromLevelIdAndVLAlias = async (ThisArcherSession, LevelId,
VLAlias) => {
if (!LevelId || !VLAlias)
throw new Error(`Wrong Level Id (${LevelId}) or VL Alias (${VLAlias})`)

let endpoint =
`api/core/system/fielddefinition/level/${LevelId}/valueslist`
let requestbody = `?$filter=Alias eq '$
{VLAlias}'&$select=Type,Id,LevelId,RelatedValuesListId`
let responseJ = await jsaPOSTwithGEToverride(ThisArcherSession, endpoint,
requestbody)
if (responseJ[0].RequestedObject.Type != 4) // If the field is not a values
list... error!
throw new Error(`The selected Status Field is not of type Values List`)

return {
ValuesListId: responseJ[0].RequestedObject.RelatedValuesListId,
VLFieldId: responseJ[0].RequestedObject.Id
}
}

//------------ jsaGetVLItemsFromVLId ------------


// Get the list of the VL items starting from its Id
const jsaGetVLItemsFromVLId = async (ThisArcherSession, VLId) => {

if (ThisArcherSession === null)


throw new Error('[jsaGetVLItemsFromVLId] - No Active Session')
if (!VLId)
throw new Error(`Wrong VL Id (${VLId})`)
const requestUrl = ThisArcherSession.Baseurl +
`api/core/system/valueslistvalue/flat/valueslist/${VLId}`
let contentRequest = {
...FetchRequestTemplate
}
contentRequest.method = 'GET'
AddSessionTokenToHeaders(ThisArcherSession, contentRequest)

const response = await fetch(requestUrl, contentRequest)

if (!response.ok)
throw new Error(`[jsaGetVLItemsFromVLId] - Fetch error "$
{ response.status})"`)

const responseJ = await response.json()


if (responseJ[0].RequestedObject == null)
throw new Error(`[jsaGetVLItemsFromVLId] - No JSON content`)

let vlmap = new Map() //Selected Values List Items


for (let element of responseJ) {
let ro = element.RequestedObject
let item = new VLItems(ro.Name, ro.Id, "", ro.ParentId, ro.Description,
ro.NumericValue, ro.SortOrder, ro.IsActive, ro.IsDefault)
vlmap.set(ro.Id, item)
}
return (vlmap)
}

//------------ jsaGetLevelIdFromRecordId ------------


// Get the level Id from the record Id (this means the record bust have been
saved at least)
const jsaGetLevelIdFromRecordId = async (ThisArcherSession, RecordId) => {
if (ThisArcherSession === null)
throw new Error('Cannot connect with Archer: No Active Session')
if (!RecordId)
throw new Error(`Unexpected Record Id (${RecordId}): should not be 0`)
const requestUrl = ThisArcherSession.Baseurl + `api/core/content/$
{RecordId}`
let contentRequest = {
...FetchRequestTemplate
}
contentRequest.method = 'GET'
AddSessionTokenToHeaders(ThisArcherSession, contentRequest)

const response = await fetch(requestUrl, contentRequest)

if (!response.ok)
throw new Error(`[jsaGetLevelIdFromRecordId] - Fetch error "$
{ response.status})"`)

const responseJ = await response.json()


if (responseJ.RequestedObject == null)
throw new Error(`[jsaGetLevelIdFromRecordId] - No JSON content`)

return (responseJ.RequestedObject.LevelId)
}

//------------ jsaGetVLItemNumbers ------------


// Read the structure of a values list (list of items) organized as one or two
levels
const jsaGetVLItemNumbers = async (MyArcherSession, PhaseLayerFieldAlias) => {
let LevelId=null, response=null
// This information is included in the session context to easy its
transport across the functions
let ContentRecordId=MyArcherSession.context.RecordId

try {
// The LevelId and the Field Alias are necessary to read the VL data
LevelId= await jsaGetLevelIdFromRecordId(MyArcherSession,
ContentRecordId)
// Get the VLId along with the FieldId (used later)
try {
response = await jsaGetVLIdFromLevelIdAndVLAlias(MyArcherSession,
LevelId, PhaseLayerFieldAlias)
} catch (error) {
throw new Error(`Display Layer VL Alias "${PhaseLayerFieldAlias}"
not found in the application, check the configuration`)
}
const {ValuesListId, VLFieldId} = response

// Get the map of item definitions of the Display Layers VL


// The key is the VL Item Id and the value is a object with the
definition
const vl_items = await jsaGetVLItemsFromVLId(MyArcherSession,
ValuesListId)

//console.log(`%c[jsaGetVLItemNumbers] - VLID="${ValuesListId}" -
FieldId="${VLFieldId}" - VL ITEM MAP`, "color: #CC0000", vl_items)

// Get the list of the selected VL items


const resp = await jsaGetVLSelectedItems(MyArcherSession, VLFieldId)

// Build the Display Layers array to hand over to the next functions
let Numbers = []
if (resp.length == 1 && resp[0].IsSuccessful) {
let ro =
resp[0].RequestedObject.FieldContents[VLFieldId.toString()]
if (ro != null && ro.Type != 4){
throw new Error(`The Field with id="${VLFieldId}" is not a
values list`)
}
if (ro.Value != null && ro.Value.ValuesListIds.length>0) { // At
least one level is selected in the values list
let vlids = [...ro.Value.ValuesListIds] // array of VLIds
let index
for (index=0; index<ro.Value.ValuesListIds.length; index++) {
let item = vl_items.get(vlids[index])
if (typeof item !== 'undefined')
Numbers.push(item.NumericValue)
}
}
}
return (Numbers)
} catch(err) {
throw new Error(err.message)
}
}

//------------jsaGetStatusVLDefinition ------------
// Read the structure of a values list (list of items) organized as one or two
levels
// The function wraps several of the previous calls and is aware of the content
record status
// If the record is new (never saved) the Application Alias and Field Alias are
necessary
// to derive the information to pull the VL data structure (items)
// If the record has been saved at least once, only the Field Alias is
necessary
const jsaGetStatusVLDefinition = async (MyArcherSession) => {
let LevelId=null, AppId=null, response=null, vl_items=null
// This information is included in the session context to easy its
transport across the functions
let ContentRecordId=MyArcherSession.context.RecordId
let AppAlias=MyArcherSession.context.AppAlias
let StatusFieldAlias=MyArcherSession.context.StatusFieldAlias

try {
// The LevelId and the Field Alias are necessary to read the VL data
// but pulling the Level Id is different depending on the content
record status (new or saved)
if (ContentRecordId != 0) {
// The record has been saved at least once, so it has a RecordId
LevelId= await jsaGetLevelIdFromRecordId(MyArcherSession,
ContentRecordId)
console.log(`EXISTING RECORD - Level Id "${LevelId}" read from
Record Id "${ContentRecordId}"`)
} else {
// The record has not yet been saved, so the Record Id is 0. In
this case, the App Alias is also needed
if (!AppAlias)
throw new Error(`The Application Alias config parameter is
required to display the DWT for new records`)

try {
AppId = await jsaGetAppIdFromAppAlias(MyArcherSession,
AppAlias)
//console.log(`[jsaGetStatusVLDefinition] - Application Id "$
{AppId}"`)
} catch (error) {
// If an error happens it's likely to be a wrongly configured
App Alias
throw new Error(`Application Alias "${AppAlias}" not found in
the Archer application, check the configuration`)
}
try {
LevelId = await
jsaGetLevelIdFromFieldAliasAndAppId(MyArcherSession, StatusFieldAlias, AppId)
console.log(`%cNEW RECORD detected - Level Id "${LevelId}" read
from App Alias "${AppAlias}"`, "color: #00AA00")
} catch (error) {
// If an error happens it's likely to be a wrongly configured
Status VL Alias
throw new Error(`Status VL Alias "${StatusFieldAlias}" not
found in the application, check the configuration`)
}
}
// The Status Field Alias is needed for both record conditions
// The function returns the VLId along with the FieldId (used later)
try {
response = await jsaGetVLIdFromLevelIdAndVLAlias(MyArcherSession,
LevelId, StatusFieldAlias)
} catch (error) {
throw new Error(`Status VL Alias "${StatusFieldAlias}" not found in
the application, check the configuration`)
}

const {ValuesListId, VLFieldId} = response

vl_items = await jsaGetVLItemsFromVLId(MyArcherSession, ValuesListId)

return ({
StatusFieldId: VLFieldId, // This is needed later
ValuesListItems: vl_items
})
} catch(err) {
throw new Error(err.message) // Rethrow new Error
}
}

//------------ jsaGetVLSelectedItems ------------


// Pull the selected item from the specified values list field Id
// Of course to read the VL item the record must have been saved, so the record
Id is required
const jsaGetVLSelectedItems = async (ThisArcherSession, VLFieldId) => {
if (!VLFieldId || !ThisArcherSession.context.RecordId)
throw new Error(`[jsaGetVLSelectedItems] - Wrong parameters`)

const requestUrl = ThisArcherSession.Baseurl +


"platformapi/core/content/fieldcontent"
let contentRequest = {
...FetchRequestTemplate
}
contentRequest.method = 'POST' // No Get override
AddSessionTokenToHeaders(ThisArcherSession, contentRequest)

// Select the values list IDs to pull along with the specific content
record
contentRequest.body = JSON.stringify({
FieldIds: [VLFieldId.toString()],
ContentIds: [ThisArcherSession.context.RecordId]
})

const response = await fetch(requestUrl, contentRequest)


if (!response.ok)
throw new Error(`[jsaGetVLSelectedItems] - Fetch error "$
{response.status}"`)
const responseJ = await response.json()
if (responseJ[0].RequestedObject == null)
throw new Error(`[jsaGetVLSelectedItems] - No JSON content`)
return responseJ
}

//------------jsaGetStatusVLDefinitionAndSelectedItem ------------
// Read the VL definition and the selected item (status to display) considering
also the record status
// Is the record has been saved, the selected item corresponds to the selected
status fo the VL
// If the record is new, the selected item is the first in the VL structure
const jsaGetStatusVLDefinitionAndSelectedItem = async (ThisArcherSession) => {

let SelectedItems = [], iterator = 0, FirstVLItem = null,


FirstDefaultVLItem = null, DefaultItemFound=false

// Get the definition (map) of the Values List Items and a selection of
their attributes
const {StatusFieldId, ValuesListItems} = await
jsaGetStatusVLDefinition(ThisArcherSession)

// Enrich each object in the map with the name of the parent values list
for (const k of ValuesListItems.keys()) {
let pid = (ValuesListItems.get(k)).ParentId
ValuesListItems.get(k).ParentName = pid != null ?
ValuesListItems.get(pid).Name : ""
if (iterator++ == 0)
FirstVLItem = ValuesListItems.get(k) // Save the first item to use
when RecordId=0
if (ValuesListItems.get(k).IsDefault) {
FirstDefaultVLItem = ValuesListItems.get(k)
DefaultItemFound = true
}
}

// The list of items is returned for new and saved records, but the
selected item
// can be returned only if the record has been saved at least once
if (ThisArcherSession.context.RecordId != 0) {

if (ThisArcherSession.context.DisplayLayerAlias != "") {
// DYNAMIC DISPLAY LEVELS: the PhaseLayer VL alias is set, so the
"static" approach is ignored
// Fetch the Display Layers set via the VL (Dynamic Display Layers)
ThisArcherSession.context.PhaseLayer = await
jsaGetVLItemNumbers(ThisArcherSession, ThisArcherSession.context.DisplayLayerAlias)
// If no layers are selected via the VL, revert back to the static
configuration
if (ThisArcherSession.context.PhaseLayer.length == 0) {
ThisArcherSession.context.PhaseLayer =
ThisArcherSession.context.DefaultPhaseLayer
console.warn(`No dynamic layers selected, layers pulled from
static config (${ThisArcherSession.context.PhaseLayer}). If empty, all the phases
are displayed by default.`)
}
}
// Read the currently selected status VL item
let jresp = await jsaGetVLSelectedItems(ThisArcherSession,
StatusFieldId)

// Build the Selected Values List Items array to return


let index = StatusFieldId.toString()
if (jresp['0'].RequestedObject.FieldContents[index].Value != null) {

(jresp['0'].RequestedObject.FieldContents[index].Value.ValuesListIds).forEach(
(vlid) => {
let tobj = ValuesListItems.get(vlid)
let itm = new VLItems(tobj.Name, tobj.NameId,
tobj.ParentName, tobj.ParentId, tobj.Description, tobj.NumericValue,
tobj.SortOrder, tobj.IsActive, tobj.IsDefault)
SelectedItems.push(itm)
}
)
// An object containing the VL definition and the selected items is
returned
return ({
AllItems: ValuesListItems,
SelectedItems: SelectedItems,
LastUpdateStr:
jresp['0'].RequestedObject.UpdateInformation.UpdateDate
})
} else {
throw new Error(`No item selected in Status Values List "$
{ThisArcherSession.context.StatusFieldAlias}"`)
}
} else { // The record is new, that is it's not yet been saved
let tobj
// If an item set as default is found, then that is set as selected
item, otherwise the first is used
if (DefaultItemFound)
tobj = FirstDefaultVLItem
else
tobj = FirstVLItem
let itm = new VLItems(tobj.Name, tobj.NameId, tobj.ParentName,
tobj.ParentId, tobj.Description, tobj.NumericValue, tobj.SortOrder, tobj.IsActive,
tobj.IsDefault)
SelectedItems.push(itm)
return ({
AllItems: ValuesListItems,
SelectedItems: SelectedItems,
LastUpdateStr: null
})
}
}

//#################################################################################
####
// DWT HELPER FUNCTIONS

//#################################################################################
####

//------------ Spinner ------------


// display a spinning wheel while waiting for the API framework become
available and get data
function Spinner(targetEl) {
let SpinnerEl
if (targetEl == null) {
console.error("ERROR: spinner selector is null -> Exit")
return
}
SpinnerEl = document.createElement("div")
SpinnerEl.id = "DWTSpinner"
SpinnerEl.style.cssText = "position: relative; display:flex; justify-
content:center; align-items:center; flex-direction:row; margin-right: calc(var(--
chevron-height) / 2);min-height: 50px;"
Spinner.svgEl = document.createElementNS("http://www.w3.org/2000/svg",
"svg")
Spinner.svgEl.setAttribute("viewBox", "0 0 50 50")
Spinner.svgEl.setAttribute("width", "50")
Spinner.svgEl.setAttribute("height", "50")
let circle = document.createElementNS("http://www.w3.org/2000/svg",
"circle")
circle.classList.add('path')
circle.setAttribute("cx", "25")
circle.setAttribute("cy", "25")
circle.setAttribute("r", "20")
circle.setAttribute("fill", "none")
circle.setAttribute("stroke-width", "4")
Spinner.svgEl.appendChild(circle)
SpinnerEl.appendChild(Spinner.svgEl)
targetEl.appendChild(SpinnerEl)
Spinner.svgEl.style.display = "none" // Initially hidden
}

Spinner.show = function () {
if (Spinner == null || Spinner.svgEl == null)
return
Spinner.svgEl.classList.add("DWTspinner")
Spinner.svgEl.style.display = "block"

}
Spinner.hide = function () {
if (Spinner == null || Spinner.svgEl == null)
return
Spinner.svgEl.classList.remove("DWTspinner")
Spinner.svgEl.style.display = "none"
}

//------------ DisplayMessage ------------


// Displays a message on the target container for error management
function DisplayMessage(targetEl, msg, bkgColor=DEFAULT_MESSAGE_BKGCOLOR) {
if (targetEl.firstChild!=null)
targetEl.firstChild.style.display="none" //Remove summary/detail
if (targetEl == null || msg == null) {
console.error("[DisplayMessage] - Wrong params, exit")
return
}
Spinner.hide()
let msgEl = document.createElement("div")
msgEl.id = "DWTMessage"
msgEl.innerText=msg
msgEl.style.background = bkgColor
targetEl.appendChild(msgEl)
}

//------------DWTGetConfigParam ------------
// Reads the config param defined by a CSS variable.
// Returns null is the input params are wrong
// Returns "" is the variable does not exist
function DWTGetConfigParam(containerEl, cssvariable) {
if ((containerEl !== null) && (cssvariable !== ""))
return
window.getComputedStyle(containerEl).getPropertyValue(cssvariable).trim()
else
return null
}

//------------ GetDescriptionOption ------------


// Read the specfied property from the JSON object optionally stored
// in the Description. Return empty string in case of any error
function GetDescriptionOption(description, property) {
let value=""
if (description == null || typeof description == 'undefined')
return "" // No JSON object in the Description
switch (property) {
case "mdicon":
value = description.mdicon
break
case "titleprefix":
value = description.titleprefix
break
case "subtitle":
value = description.subtitle
break
case "spoverride":
value = description.spoverride
break
case "displaytitle":
value = description.displaytitle
break
case "displaylayer":
value = description.displaylayer
break
default:
value = ""
}
if (value == null || typeof value == 'undefined')
return "" // The required property in the JSON object is not configured
return (value)
}

//------------ DWTSetPhaseStatus ------------


// Set the phase status (past, current, future) depenging on the ordering of
the items
function DWTSetPhaseStatus(item, SelectedItems) {
if (item.SortOrder == SelectedItems.SortOrder) {
item.Status = STS_Current
} else {
if (item.SortOrder < SelectedItems.SortOrder) item.Status = STS_Past
else {
item.Status = STS_Future
}
}
return item
}

//------------ DWTRenderPhase ------------


// Generate the markup for a workflow phase, A phase is a group of one Core,
made of an icon and label
// and an optional set of Substates. The function returns the generated markup
as a string
function DWTRenderPhase(ThisArcherSession, CurrentPhase, DWTcontainerEl) {

// Check if the configured display layer is included in the list of the


layers to display for this instance of DWT
let ConfiguredDisplayLayer =
GetDescriptionOption(CurrentPhase.Core.Description, "displaylayer")
if (ConfiguredDisplayLayer == '') // The opton is not set. Try to pull the
value from the Item's Value Number
ConfiguredDisplayLayer = CurrentPhase.Core.NumericValue
if (ConfiguredDisplayLayer == null) { // Also the Item's value number is
not configured
ConfiguredDisplayLayer = '' // Disable the level, so it's not displayed
}
if (!CurrentPhase.Core.LayerMatch) {
console.log(`%cThe Display Layer "${ConfiguredDisplayLayer}" for phase
"${CurrentPhase.Core.Name}" is not in the configured list, so it'll not be
displayed`,"color: #CC0000;")
return ("")
}

if (!CurrentPhase.Core.IsActive) // Don't render phases whose values list


items are disabled
return ("")

let icon = GetDescriptionOption(CurrentPhase.Core.Description, "mdicon")


let displaytitle = GetDescriptionOption(CurrentPhase.Core.Description,
"displaytitle")
let done_icon = DWTGetConfigParam(DWTcontainerEl, "--chevron-phasedone-
icon")
let done_color = DWTGetConfigParam(DWTcontainerEl, "--chevron-phasedone-
color")

// This flag sets an attribute that prevents the last chevron triangle to
display
let FlatTailFlag = (ThisArcherSession.context.TailStyle == DWT_STYLE_FLAT
&& CurrentPhase.Core.IsLast)? "true":"false"
// Flag to set they style of the chevron (except the tail) which can
standard (triangle) or flat (vertical line)
let isChevronFlat = (ThisArcherSession.context.ChevronStyle ==
DWT_STYLE_FLAT)? "true":"false"

// This flag is aimed to detect when the WF is complete, which means the
last phase has status=current
let Completed = (CurrentPhase.Core.Status == STS_Current &&
CurrentPhase.Core.IsLast)?true:false
// Based on the configuration, set the condition to coloring the last phase
when the WF is completed
let IfCompletedInheritLastPhaseStatus = (DWTGetConfigParam(DWTcontainerEl,
"--chevron-tail-coloring") == "inherited") && Completed?true:false
// Put the checkmark on past phases. If the phase is the last (completed),
put a checkmark as well

if (ThisArcherSession.context.CheckmarkIfDone) { // Checkmark is enabled


if (CurrentPhase.Core.Status == STS_Past || Completed)
icon = `${done_color!=""?"<span
style=\"color:"+done_color+"\">"+done_icon+"</span>":done_icon}` // Tag the phase
as done
}

let mk = `<div class="DWTPhaseContainer">


<div class="DWTDisplayLayerContainer"> <div class="DWTDisplayLayerTag"
id="Step${CurrentPhase.Core.Step}">${ConfiguredDisplayLayer}</div> </div>
<div class="DWTPhase" id="Step${CurrentPhase.Core.Step}.$
{CurrentPhase.Core.SubStep}" status="${IfCompletedInheritLastPhaseStatus?
STS_Past:CurrentPhase.Core.Status}" isChevronFlat=${isChevronFlat} isTailFlat="$
{FlatTailFlag}">`

mk += `<div class="DWTPhaseLabel">${typeof icon == "undefined" || icon ==


""?"":`<div class="DWTPhaseLabelIcon"><span
class="material-icons">${icon}</span></div>`}`
if (displaytitle != "no") // If no explicit option to hide title...
mk += `<div
class="DWTPhaseLabelText"><span>${CurrentPhase.Core.Name}</span></div>`
mk += `</div></div>`

if (CurrentPhase.Children.length > 0) {
// Setup the initial markup for the children
mk += `<div class="DWTSubPhaseContainer"><div class="DWTSubphaseBar"
status="${IfCompletedInheritLastPhaseStatus?STS_Past:CurrentPhase.Core.Status}"></
div>`
for (let idx=0; idx<CurrentPhase.Children.length; idx++) { // ForEach
did not work here..
let child = CurrentPhase.Children[idx]
if (child.Status == STS_Current && child.LayerMatch) {
if (!child.IsActive) // Do not render sub-phases whose values
list items are disabled or whose layer is not active
return ("")
let spoverride = GetDescriptionOption(child.Description,
"spoverride")
// Check if an override string exists for the current sub-base
if (spoverride == "") {
// No, use the name of sub-phase
mk += `<div class="DWTCurrentSubphase">${child.Name}</div>`
} else {
// Yes, use the override string
mk += `<div class="DWTCurrentSubphase">${spoverride}</div>`
}
}
}
mk += `</div>`
}
mk += `</div>`
return mk
}

//------------DWTDisplayPhase ------------
// Callback for the animated rendering
function DWTDisplayPhase(item) {
if (item == null) return
item.style.visibility = "visible"
}

//------------ DWTMarkupRendering ------------


// Render the markup included in the target element
// Depending on the CSS property, the animation is displayed with the
configured delay
const DWTMarkupRendering = (TargetDWTElement) => {
if (TargetDWTElement == null) return
let aniDelay = DWTGetConfigParam(TargetDWTElement, "--chevron-animation-
delay")
if (aniDelay == "" || aniDelay == 0) { // Property not defined or set to 0
-> disable animation
TargetDWTElement.style.visibility = "visible"
} else {
TargetDWTElement.style.visibility = "hidden"
//console.log("[DWTMarkupRendering] - Animation Delay: " + aniDelay +
"ms")
let i = 0
for (let item of
TargetDWTElement.getElementsByClassName("DWTPhaseContainer"))
setTimeout(() => {
DWTDisplayPhase(item)
}, i++ * aniDelay)
}
}

//------------ DWTRender ------------


// Renders the CTW graphics, starting from the data tha defines the statuses
and the selected status
// If the record is new (not yet saved) the SelectedItem is the first in the
sequence
const DWTRender = async (ThisArcherSession, AllItems, SelectedItems,
DWTcontainerEl) => {

let renderedPhaseNumber = 0

// BUILD THE DATA STRUCTURE as an array of objects {Phases, Subphases}


where the Phase is the top level VL item while
// the Subphase is an array of VL items that are children of the top level
item
// If a JSON object is included in the Description of the VL item, then
this is pulled in and added to the items
// A Status attribute is assigned to the Phases or Subphases based on the
position of the SelectedItem
let wfphases = [],
wfstep = -1,
SubStep,
CurrentStatusParent
//console.log("[DWTRender] -%c VALUES LIST DEFINITION ","background:
#00A800; color: #FFF")
for (let item of AllItems) {
item.Description = GetJSONfromHTML(item.Description)
if (item.Description == null)
console.log(`%cWARNING: Empty or wrong JSON object in item "$
{item.Name}", ignored. Please check the syntax of the JSON `,"color: #BB0000;")

// Loop through all the items of the VL


item = DWTSetPhaseStatus(item, SelectedItems)
if (item.ParentName == "") {
// This is a parent phase
item.IsFirst = item.SortOrder == 0 ? true : false // Tag the first
state in the flow
SubStep = 1
wfstep += 1
item.Step = wfstep
item.SubStep = 0
CurrentStatusParent = item // This is used to set to Active the
parent of the item if the latter is a substate
if (item.SortOrder == SelectedItems.SortOrder) {
SelectedItems.Step = wfstep
SelectedItems.SubStep = 0
SelectedItems.Status = STS_Current
}

// Mathing layer rules


if (item.NumericValue == null) // Always display items with no
layer assigned
item.LayerMatch = true
else {
if (ThisArcherSession.context.PhaseLayer.length==0) {
// If no layers are selected, display the phase
item.LayerMatch = true
} else {
// Selected layer is included in the configured list
if
(ThisArcherSession.context.PhaseLayer.includes(item.NumericValue))
item.LayerMatch = true
}
}

wfphases.push({
Core: item,
Children: new Array(),
})
} else {
// This is a children (sub state)
if (item.SortOrder == SelectedItems.SortOrder) {
// Selected (Current) sub state
SelectedItems.Step = wfstep
SelectedItems.SubStep = SubStep
SelectedItems.Status = STS_Current
CurrentStatusParent.Status = STS_Current // Set to active also
the parent state of the selected item
}
// Check if parent of this children item is disabled. If yes, the
chindren item must be disabled as well
let myparent = AllItems.filter(element => {
if ((item.ParentId == element.NameId))
return (element)
})
if (myparent[0].IsActive && !item.IsActive) // Phase active, sub-
phase not active
item.IsActive = false // Sub-phase wins
if (!myparent[0].IsActive) // If Phase is not active
item.IsActive = false // Sub-phases are disabled as well
// Calculate the step numbering to identify the items and their
order
item.Step = wfstep
item.SubStep = SubStep++

// Mathing layer rules


if (item.NumericValue == null)
item.LayerMatch = true
else {
if (ThisArcherSession.context.PhaseLayer.length==0) {
item.LayerMatch = true
} else {
if
(ThisArcherSession.context.PhaseLayer.includes(item.NumericValue))
item.LayerMatch = true
}
}
wfphases[wfstep].Children.push(item) // Attach a children (sub-
state)
}
}
// Calculate the index of the last displayed phase
let LastPhaseIndex=-1
wfphases.forEach((phase)=>{
if (phase.Core.LayerMatch)
LastPhaseIndex=phase.Core.Step
if (phase.Children!=null) {
phase.Children.forEach((subphase)=>{
if (phase.Core.LayerMatch && subphase.LayerMatch)
LastPhaseIndex=phase.Core.Step
})
}
})
if (LastPhaseIndex != -1)
wfphases[LastPhaseIndex].Core.IsLast = true

//============================================
// BUILD THE MARKUP to display the DWT phases
let markup, phaseMarkup=""
// This init markup is about the tooltip
markup = `<details id="DetailsOf-${DWTcontainerEl.id}"><summary></summary>
<div class="DWTContainer">`
DWTcontainerEl.innerHTML = markup
// Build the markup of each phase in the map
markup += `${wfphases.map((CurrentPhase) => {
phaseMarkup=DWTRenderPhase(ThisArcherSession, CurrentPhase,
DWTcontainerEl)
if (phaseMarkup!="") renderedPhaseNumber++
return phaseMarkup
}).join("")}`
markup += `</div></details>`

// Render in the DOM the generated markup, but not display it yet
DWTcontainerEl.style.visibility = "hidden"
DWTcontainerEl.innerHTML = markup

// The popover markup is dynamically built pulling the content from the
values list items
// The markup is dynamically added buy using the Tippy open source library
wfphases.forEach((phase) => {
let PhaseSelectorId = `[Id="Step${phase.Core.Step}.$
{phase.Core.SubStep}"]`
let PhaseEl = DWTcontainerEl.querySelectorAll(PhaseSelectorId)
if (PhaseEl != null) {
let contentmk = `<span class="tooltip-title">$
{GetDescriptionOption(phase.Core.Description,
"titleprefix")}${phase.Core.Name}</span>`
contentmk +=`<span class="tooltip-subtitle">$
{GetDescriptionOption(phase.Core.Description, "subtitle")}</span>`

if (phase.Children.length > 0) {
contentmk += `<ul class="tooltip-subphases">`
phase.Children.forEach((child) => {
if (child.LayerMatch)
contentmk += `<li>${child.Name}</li>`
})
contentmk += `</ul>`
}
tippy(PhaseEl, {
content: contentmk,
trigger: 'click',
arrow: true,
allowHTML: true,
animation: 'fade',
duration: [550, 200],
theme: 'custom',
distance: 10,
interactive: true,
placement: 'top',
showOnInit: true,
})
}
})

// Render the markup, with the optional animation


DWTMarkupRendering(DWTcontainerEl)

if (renderedPhaseNumber == 0 || LastPhaseIndex == -1)


throw new Error(`can't display any phase. Display Layers are enabled,
but no assigned layers match the selected layers`)

// The collapsed/expanded status is made persistent through the browser


local storage
// If the cache has never been created, the DWT is just expanded, otherwise
// the expansion status is picked from the cache
ThisArcherSession.context.SummaryDetailsDiv = `DetailsOf-$
{DWTcontainerEl.id}`
let
sdel=document.getElementById(ThisArcherSession.context.SummaryDetailsDiv)
DWTcache=JSON.parse(localStorage.getItem(DWT_CACHE_BASE+DWTcontainerEl.id))
if (DWTcache != null) {
if ((ThisArcherSession.context.DisplayStatusPersistent == "yes" &&
DWTcache.DisplayStatus == "open") ||
(ThisArcherSession.context.DisplayStatusPersistent == "no")) {
if (sdel != null)
sdel.setAttribute("open", "")
}
} else {
// The cache does not exist, so the DWT is expanded by default
console.log(`Cache empty, DWT expanded by default`)
if (sdel != null)
sdel.setAttribute("open", "")
}

// Attach the event listener to capture the collapsed/expanded status of


the DWT, so that it can be made persistent (if configured)
if (ThisArcherSession.context.DisplayStatusPersistent == "yes") {
const chapters = document.querySelectorAll('details');
chapters.forEach((chapter) => {
chapter.addEventListener('toggle', (evt) => {
let DisplayStatus =
document.querySelector('#'+ThisArcherSession.context.SummaryDetailsDiv).hasAttribut
e("open")?"open":"closed"
// The DWT open/close status is cached in the browser local storage
using a name that depends on the container name
localStorage.setItem(DWT_CACHE_BASE+DWTcontainerEl.id,
JSON.stringify({DisplayStatus: DisplayStatus}))
console.log(`DWT expand/collapse status gathered from cache
(set to: "${DisplayStatus}")`)
});
});

}
let DWTWrapperEl=DWTcontainerEl.getElementsByClassName("DWTContainer")
if (DWTWrapperEl != null) {
// Adapt the right margin depending on the tail style, so that the DWT
is always centered
DWTcontainerEl.getElementsByClassName("DWTContainer")
[0].style.marginRight=ThisArcherSession.context.TailStyle ==
DWT_STYLE_FLAT?"0":"calc(var(--chevron-height) / 2)"
// Apply a left margin to the label of the first phase, so that it does
not overlap with the collaps/expand button
DWTcontainerEl.getElementsByClassName("DWTContainer")
[0].firstChild.getElementsByClassName("DWTPhase")
[0].firstChild.style.marginLeft="1.8rem"
}
}

//------------ GetTargetContainerEl ------------


// Returns the element that corresponds to the div container to attach the DWT
to
// If targetContainerId is null or equal to the defaul "StickOnTop", a
container is built and attached "stick on top",
// otherwise a container with the specified name is searched in the html page
// The name of container is also used to define the namespace of CSS variables,
especially when multiple DWTs are used

const GetTargetContainerEl = (targetContainerId) => {


let containerEl = null
if (targetContainerId == null || targetContainerId=="") // Default to Stick
on Top
targetContainerId = DIV_ID_STICKONTOP

console.log(`[GetTargetContainerEl] - Configured target div container Id:


"${targetContainerId}"`)
if (targetContainerId == DIV_ID_STICKONTOP) {
// If the layout includes tabs and the user switches them, the DWT div
already exist,
// so it muset be removed otherwise it gets replicated on every tab
switch
containerEl = document.getElementById(targetContainerId)
if (containerEl != null)
containerEl.remove();
// Container on top does not exist, create a div with an Id and class
using the same name
// The class name must be the same as the one using to wrap the CSS
variables, sicne it defines the
// namespace for these variables. If they don't match, the CSS
variables cannot be found!
containerEl = document.createElement("div")
containerEl.id = targetContainerId
containerEl.style.width = "100%"
containerEl.style.color = "gray"
containerEl.style.visibility = "visible"
containerEl.className = targetContainerId

// Attach the newly created container to the top of the record page
let hookEl = document.getElementById(ARCHER_TOP_DIV)
if (ARCHER_TOP_DIV == "master_windowContainer" )
hookEl.prepend(containerEl)
else
hookEl.append(containerEl)
} else {
// Retrieve through the CSS variables the name of the target div
container
containerEl = document.getElementById(targetContainerId)
if (containerEl == null) {
console.log(`%cWARNING: Div container "${targetContainerId}" not
found in any C.O in the layout. It might be hiddent in a non-selected tab or check
the JS configuration. Exiting..."`,"color: #CC0000")
return
}
// Set the class to the same container Id name, so that the CSS
variable scope is set,
// and the correct variables can be found
containerEl.className = targetContainerId
}
return (containerEl)
}

//-----------------------------------------------------------------------------
--------
// DisplayDWT: main function to display the Workflow Tracker
//-----------------------------------------------------------------------------
--------
const DisplayDWT = async (pDWTStatusFieldAlias=null, DWTcontainerEl=null) => {

DWTStatusFieldAlias="", DWTAppAlias="", DWTDispLayAlias=""


let DisplayForNewRecords=null, DefaultPhaseLayer=[], jresp=null
let MyArcherSession = null
if (DWTcontainerEl == null) {
console.log(`[%cTarget Container Element is null. Exiting..."`,"color:
#CC0000")
return
}
// The CSS namespace is already set, so we can get name of the color schema
from the CSS variable
let confColorScheme =
window.getComputedStyle(DWTcontainerEl).getPropertyValue("--chevron-current-color-
scheme").trim()
if (confColorScheme == "") { // Variable not found in CSS scope
console.log(`[%cColor scheme NOT FOUND in container/scope "$
{DWTcontainerEl.id}". Is ".${DWTcontainerEl.id}" class configured in CSS? Now
exiting..."`,"color: #CC0000")
return
} else {
console.log(`Configured color theme: "${confColorScheme}"`)
}
// Add the class with the color schema name to the DTW instance element
DWTcontainerEl.classList.add(confColorScheme)

let ThisContentRecordId = getRecordId() // It's 0 for new records

// Get some configuration parameters from the CSS styles in the current
namespace
DisplayForNewRecords = DWTGetConfigParam(DWTcontainerEl, "--chevron-
display-for-new-records") == "no"?"no":"yes" // Default: "yes"

DWTAppAlias = DWTGetConfigParam(DWTcontainerEl, "--chevron-app-alias")

// If input params is not specified, grab it from the CSS property. set a
default- grab them from the CSS variables. This is aimed to
// avoid changing the custom object code and configure it via the CSS
variable only
if (pDWTStatusFieldAlias == null) {
let VLalias = DWTGetConfigParam(DWTcontainerEl, "--chevron-statusvl-
alias")
VLalias == ""?DWTStatusFieldAlias =
DEFAULT_VALUES_LIST:DWTStatusFieldAlias = VLalias
}
else {
DWTStatusFieldAlias = pDWTStatusFieldAlias
}
console.log(`%cConfigured App VL Alias="${DWTAppAlias}" - Status VL Alias
"${DWTStatusFieldAlias}"`, "color: #00AA00")

// The record is new, but the configuration does not require to display the
DWT, so the custom object immediately terminates
if (ThisContentRecordId==0 && DisplayForNewRecords=="no") {
console.log(`%cNew Content Record DETECTED, but config param "--
chevron-display-for-new-records" is set to "no". Silently exiting...`,"color:
#DD0000")
return
}

// Get the Display Layers to display, converting the list into an array of
integers, filtering out strings
DWTDispLayAlias = DWTGetConfigParam(DWTcontainerEl, "--chevron-
displaylayervl-alias")
DefaultPhaseLayer = (DWTGetConfigParam(DWTcontainerEl, "--chevron-display-
layers")).split(',').map(Number).filter(Boolean)

if (DefaultPhaseLayer!="")
console.log(`%cSTATIC Display Layers (${DefaultPhaseLayer})
configured`, "color: #00AA00")
if (DWTDispLayAlias!="")
console.log(`%cDYNAMIC Display Layers configured (pulled from VL alias
"${DWTDispLayAlias}")`, "color: #00AA00")
if (DefaultPhaseLayer+DWTDispLayAlias == "")
console.log(`%cNo Display Layers configured`, "color: #00AA00")

// To render the arrows in the DWT the "CSS triangle pattern" is used, but
this uses the zIndex to overlam the triangle
// However, the AWF and Toolbar dropdowns have a zIndex=1 which cannot be
changed and this the triangle overlaps the manu
// The following code raises on the fly the zIndex of the dropdowns so that
they are correctly displayed
// A Timeout is used as an asych function to run after the custom object
"ends" because the toolbar is rendered after the CO is run
const zIndexRaiseTargets=['workflow-dropdown-content', 'share-dropdown-
content', 'ellipsis-dropdown-content']
setTimeout(() => {
zIndexRaiseTargets.forEach ((targetEl) => {
const sel = document.getElementById(targetEl)
if (sel != null)
sel.style.zIndex=2
})
}, 1000)

Spinner(DWTcontainerEl)
Spinner.show()

try {
console.log(`Connecting to the Archer API framework`)

MyArcherSession = ConnectToArcher()
// The below information must be available in multiple areas, so a
context object is defined
// and added to the session so that it can be transported across the
functions easier (convoy!)
MyArcherSession.context.RecordId = ThisContentRecordId
MyArcherSession.context.AppAlias = DWTAppAlias
MyArcherSession.context.DisplayLayerAlias = DWTDispLayAlias
MyArcherSession.context.StatusFieldAlias = DWTStatusFieldAlias
MyArcherSession.context.DisplayForNewRecords = DisplayForNewRecords
MyArcherSession.context.DefaultPhaseLayer = DefaultPhaseLayer
MyArcherSession.context.PhaseLayer = DefaultPhaseLayer // Set current =
default
MyArcherSession.context.DisplayStatusPersistent =
DWTGetConfigParam(DWTcontainerEl, "--chevron-display-status-persistent")
MyArcherSession.context.TailStyle = DWTGetConfigParam(DWTcontainerEl,
"--chevron-tail-style") == "flat"?DWT_STYLE_FLAT:DWT_STYLE_STANDARD // Default
"DWT_STYLE_STANDARD"
MyArcherSession.context.ChevronStyle =
DWTGetConfigParam(DWTcontainerEl, "--chevron-style") == "flat"?
DWT_STYLE_FLAT:DWT_STYLE_STANDARD // Default "DWT_STYLE_STANDARD"
MyArcherSession.context.CheckmarkIfDone =
DWTGetConfigParam(DWTcontainerEl, "--chevron-checkmark_if_done")=="yes"?true:false
MyArcherSession.context.DWTcontainerEl = DWTcontainerEl

// Fetch the defined DWT phases and the selected status fetiching them
form the Status VL
jresp = await jsaGetStatusVLDefinitionAndSelectedItem(MyArcherSession)

// Graphically render the DWT


await DWTRender(MyArcherSession, Array.from(jresp.AllItems.values()),
jresp.SelectedItems[0] , DWTcontainerEl)
} catch (err) { // TRY CATCH END
Spinner.hide()
console.error("[DisplayDWT] - DWT ERROR: ", err)
DisplayMessage(DWTcontainerEl, `DWT ERROR: ${err.message}`)
return
}

// Add the event handler to display/hide the Display Layers overalayed on


each CTW phase
// The key/mouse combination is: ALT KEY-DOUBLE CLICK on any DWT phase
MyArcherSession.context.DWTcontainerEl.addEventListener('dblclick', zEvent
=> {
if (zEvent.altKey) {
let DWTContainerEl =
MyArcherSession.context.DWTcontainerEl.querySelectorAll("#"+DWTcontainerEl.id+ "
div.DWTPhaseContainer")
DWTContainerEl.forEach((phaseContainer) => {
// Toggle the visualization of the Display Layers overlayed on
top the chevrons
let DLContainer =
phaseContainer.querySelector("div.DWTDisplayLayerContainer")

DLContainer.style.display=['','none'].includes(DLContainer.style.display)?"flex":"n
one"
})
}
})
return
}
return {
// Publicly available methods
GetTargetContainerEl,
DisplayDWT
}
})() // End of module

// The custom object execution starts here


Sys.Application.add_load(function () { // When the document is loaded...

// List of div containers aimed to host the DWT instances.


// A custom object with the div container name must be configured and placed in
the layout
let DWTContainers=["StickOnTop"]
let t0, t1, targetContainerEl

// Loop through the configured div containers to display the DWT


DWTContainers.forEach((targetContainerId) => {
targetContainerEl = DWT.GetTargetContainerEl(targetContainerId)
if (targetContainerEl == null)
return
console.log(`%cDWT START [container "${targetContainerId}"]`,"color:
#00BBBB;")
t0 = performance.now();
DWT.DisplayDWT(null, targetContainerEl).then((resp) => {
t1 = performance.now();
console.log(`%cDWT END [container "${targetContainerId}"] (elapsed $
{parseInt(t1-t0)}ms)`,"color: #00BBBB;")
}).catch((err) => {
console.error(`DWT ERROR [container "${targetContainerId}"]: DisplayDWT
call failed: `, err)
})
})
})
</script>

You might also like