Merge pull request #242 from sartography/feature/multiple_editor_users_display_user
Feature/multiple editor users display user
This commit is contained in:
commit
5debe44391
|
@ -0,0 +1,46 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 6aa02463da9c
|
||||||
|
Revises: 68adb1d504e1
|
||||||
|
Create Date: 2023-05-04 12:50:07.979692
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6aa02463da9c'
|
||||||
|
down_revision = '68adb1d504e1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('active_user',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('last_visited_identifier', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('last_seen_in_seconds', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('user_id', 'last_visited_identifier', name='user_last_visited_unique')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('active_user', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_active_user_last_seen_in_seconds'), ['last_seen_in_seconds'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_active_user_last_visited_identifier'), ['last_visited_identifier'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_active_user_user_id'), ['user_id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('active_user', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_active_user_user_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_active_user_last_visited_identifier'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_active_user_last_seen_in_seconds'))
|
||||||
|
|
||||||
|
op.drop_table('active_user')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -163,6 +163,50 @@ paths:
|
||||||
$ref: "#/components/schemas/OkTrue"
|
$ref: "#/components/schemas/OkTrue"
|
||||||
|
|
||||||
|
|
||||||
|
/active-users/updates/{last_visited_identifier}:
|
||||||
|
parameters:
|
||||||
|
- name: last_visited_identifier
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The identifier for the last visited page for the user.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Active User
|
||||||
|
operationId: spiffworkflow_backend.routes.active_users_controller.active_user_updates
|
||||||
|
summary: An SSE (Server Sent Events) endpoint that returns what users are also currently viewing the same page as you.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: list of users
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/User"
|
||||||
|
|
||||||
|
/active-users/unregister/{last_visited_identifier}:
|
||||||
|
parameters:
|
||||||
|
- name: last_visited_identifier
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The identifier for the last visited page for the user.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Active User
|
||||||
|
operationId: spiffworkflow_backend.routes.active_users_controller.active_user_unregister
|
||||||
|
summary: Unregisters a user from a page. To be used when the /active-users/updates endpoint is closed.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The current user has unregistered.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/OkTrue"
|
||||||
|
|
||||||
/process-groups:
|
/process-groups:
|
||||||
parameters:
|
parameters:
|
||||||
- name: process_group_identifier
|
- name: process_group_identifier
|
||||||
|
@ -1708,6 +1752,7 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ServiceTask"
|
$ref: "#/components/schemas/ServiceTask"
|
||||||
|
|
||||||
/tasks/{process_instance_id}:
|
/tasks/{process_instance_id}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: process_instance_id
|
- name: process_instance_id
|
||||||
|
@ -1729,7 +1774,6 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Task"
|
$ref: "#/components/schemas/Task"
|
||||||
|
|
||||||
|
|
||||||
/tasks/{process_instance_id}/{task_guid}:
|
/tasks/{process_instance_id}/{task_guid}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: task_guid
|
- name: task_guid
|
||||||
|
|
|
@ -68,5 +68,8 @@ from spiffworkflow_backend.models.bpmn_process_definition_relationship import (
|
||||||
from spiffworkflow_backend.models.process_instance_queue import (
|
from spiffworkflow_backend.models.process_instance_queue import (
|
||||||
ProcessInstanceQueueModel,
|
ProcessInstanceQueueModel,
|
||||||
) # noqa: F401
|
) # noqa: F401
|
||||||
|
from spiffworkflow_backend.models.active_user import (
|
||||||
|
ActiveUserModel,
|
||||||
|
) # noqa: F401
|
||||||
|
|
||||||
add_listeners()
|
add_listeners()
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.db import db
|
||||||
|
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
||||||
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveUserModel(SpiffworkflowBaseDBModel):
|
||||||
|
__tablename__ = "active_user"
|
||||||
|
__table_args__ = (db.UniqueConstraint("user_id", "last_visited_identifier", name="user_last_visited_unique"),)
|
||||||
|
|
||||||
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id: int = db.Column(ForeignKey(UserModel.id), nullable=False, index=True) # type: ignore
|
||||||
|
last_visited_identifier: str = db.Column(db.String(255), nullable=False, index=True)
|
||||||
|
last_seen_in_seconds: int = db.Column(db.Integer, nullable=False, index=True)
|
|
@ -0,0 +1,61 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
|
import flask.wrappers
|
||||||
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
from flask import stream_with_context
|
||||||
|
from flask.wrappers import Response
|
||||||
|
|
||||||
|
from spiffworkflow_backend.models.active_user import ActiveUserModel
|
||||||
|
from spiffworkflow_backend.models.db import db
|
||||||
|
from spiffworkflow_backend.models.user import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
def active_user_updates(last_visited_identifier: str) -> Response:
|
||||||
|
active_user = ActiveUserModel.query.filter_by(
|
||||||
|
user_id=g.user.id, last_visited_identifier=last_visited_identifier
|
||||||
|
).first()
|
||||||
|
if active_user is None:
|
||||||
|
active_user = ActiveUserModel(
|
||||||
|
user_id=g.user.id, last_visited_identifier=last_visited_identifier, last_seen_in_seconds=round(time.time())
|
||||||
|
)
|
||||||
|
db.session.add(active_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
stream_with_context(_active_user_updates(last_visited_identifier, active_user=active_user)),
|
||||||
|
mimetype="text/event-stream",
|
||||||
|
headers={"X-Accel-Buffering": "no"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _active_user_updates(last_visited_identifier: str, active_user: ActiveUserModel) -> Generator[str, None, None]:
|
||||||
|
while True:
|
||||||
|
active_user.last_seen_in_seconds = round(time.time())
|
||||||
|
db.session.add(active_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
cutoff_time_in_seconds = time.time() - 15
|
||||||
|
active_users = (
|
||||||
|
UserModel.query.join(ActiveUserModel)
|
||||||
|
.filter(ActiveUserModel.last_visited_identifier == last_visited_identifier)
|
||||||
|
.filter(ActiveUserModel.last_seen_in_seconds > cutoff_time_in_seconds)
|
||||||
|
.filter(UserModel.id != g.user.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
yield f"data: {current_app.json.dumps(active_users)} \n\n"
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
def active_user_unregister(last_visited_identifier: str) -> flask.wrappers.Response:
|
||||||
|
active_user = ActiveUserModel.query.filter_by(
|
||||||
|
user_id=g.user.id, last_visited_identifier=last_visited_identifier
|
||||||
|
).first()
|
||||||
|
if active_user is not None:
|
||||||
|
db.session.delete(active_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
|
|
@ -130,6 +130,7 @@ def setup_logger(app: Flask) -> None:
|
||||||
|
|
||||||
# these loggers have been deemed too verbose to be useful
|
# these loggers have been deemed too verbose to be useful
|
||||||
garbage_loggers_to_exclude = ["connexion", "flask_cors.extension"]
|
garbage_loggers_to_exclude = ["connexion", "flask_cors.extension"]
|
||||||
|
loggers_to_exclude_from_debug = ["sqlalchemy"]
|
||||||
|
|
||||||
# make all loggers act the same
|
# make all loggers act the same
|
||||||
for name in logging.root.manager.loggerDict:
|
for name in logging.root.manager.loggerDict:
|
||||||
|
@ -149,8 +150,17 @@ def setup_logger(app: Flask) -> None:
|
||||||
for garbage_logger in garbage_loggers_to_exclude:
|
for garbage_logger in garbage_loggers_to_exclude:
|
||||||
if name.startswith(garbage_logger):
|
if name.startswith(garbage_logger):
|
||||||
exclude_logger_name_from_logging = True
|
exclude_logger_name_from_logging = True
|
||||||
|
|
||||||
|
exclude_logger_name_from_debug = False
|
||||||
|
for logger_to_exclude_from_debug in loggers_to_exclude_from_debug:
|
||||||
|
if name.startswith(logger_to_exclude_from_debug):
|
||||||
|
exclude_logger_name_from_debug = True
|
||||||
|
if exclude_logger_name_from_debug:
|
||||||
|
the_logger.setLevel("INFO")
|
||||||
|
|
||||||
if not exclude_logger_name_from_logging:
|
if not exclude_logger_name_from_logging:
|
||||||
the_logger.addHandler(logging.StreamHandler(sys.stdout))
|
the_logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||||
|
|
||||||
for the_handler in the_logger.handlers:
|
for the_handler in the_logger.handlers:
|
||||||
the_handler.setFormatter(log_formatter)
|
the_handler.setFormatter(log_formatter)
|
||||||
the_handler.setLevel(log_level)
|
the_handler.setLevel(log_level)
|
||||||
|
|
|
@ -3139,6 +3139,8 @@ class TestProcessApi(BaseTest):
|
||||||
"filterable": False,
|
"filterable": False,
|
||||||
},
|
},
|
||||||
{"Header": "Status", "accessor": "status", "filterable": False},
|
{"Header": "Status", "accessor": "status", "filterable": False},
|
||||||
|
{"Header": "Task", "accessor": "task_title", "filterable": False},
|
||||||
|
{"Header": "Waiting For", "accessor": "waiting_for", "filterable": False},
|
||||||
{"Header": "awesome_var", "accessor": "awesome_var", "filterable": True},
|
{"Header": "awesome_var", "accessor": "awesome_var", "filterable": True},
|
||||||
{"Header": "invoice_number", "accessor": "invoice_number", "filterable": True},
|
{"Header": "invoice_number", "accessor": "invoice_number", "filterable": True},
|
||||||
{"Header": "key1", "accessor": "key1", "filterable": True},
|
{"Header": "key1", "accessor": "key1", "filterable": True},
|
||||||
|
@ -3155,6 +3157,8 @@ class TestProcessApi(BaseTest):
|
||||||
"end_in_seconds",
|
"end_in_seconds",
|
||||||
"process_initiator_username",
|
"process_initiator_username",
|
||||||
"status",
|
"status",
|
||||||
|
"task_title",
|
||||||
|
"waiting_for",
|
||||||
]
|
]
|
||||||
assert accessors == stock_columns + ["awesome_var", "invoice_number", "key1", "key2", "key3"]
|
assert accessors == stock_columns + ["awesome_var", "invoice_number", "key1", "key2", "key3"]
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ type OwnProps = {
|
||||||
onElementsChanged?: (..._args: any[]) => any;
|
onElementsChanged?: (..._args: any[]) => any;
|
||||||
url?: string;
|
url?: string;
|
||||||
callers?: ProcessModelCaller[];
|
callers?: ProcessModelCaller[];
|
||||||
|
activeUserElement?: React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference
|
// https://codesandbox.io/s/quizzical-lake-szfyo?file=/src/App.js was a handy reference
|
||||||
|
@ -114,6 +115,7 @@ export default function ReactDiagramEditor({
|
||||||
onElementsChanged,
|
onElementsChanged,
|
||||||
url,
|
url,
|
||||||
callers,
|
callers,
|
||||||
|
activeUserElement,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const [diagramXMLString, setDiagramXMLString] = useState('');
|
const [diagramXMLString, setDiagramXMLString] = useState('');
|
||||||
const [diagramModelerState, setDiagramModelerState] = useState(null);
|
const [diagramModelerState, setDiagramModelerState] = useState(null);
|
||||||
|
@ -672,6 +674,7 @@ export default function ReactDiagramEditor({
|
||||||
)}
|
)}
|
||||||
</Can>
|
</Can>
|
||||||
{getReferencesButton()}
|
{getReferencesButton()}
|
||||||
|
{activeUserElement || null}
|
||||||
</ButtonSet>
|
</ButtonSet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -679,9 +682,9 @@ export default function ReactDiagramEditor({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{userActionOptions()}
|
{userActionOptions()}
|
||||||
{showReferences()}
|
{showReferences()}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -455,3 +455,103 @@ svg.notification-icon {
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-1 {
|
||||||
|
background-color: #8e8e8e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-2 {
|
||||||
|
background-color: #a57c63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-3 {
|
||||||
|
background-color: #8f7c7c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-4 {
|
||||||
|
background-color: #9a927f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-5 {
|
||||||
|
background-color: #5e5d5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-6 {
|
||||||
|
background-color: #676767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-7 {
|
||||||
|
background-color: #6d7d6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-8 {
|
||||||
|
background-color: #7a7171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-9 {
|
||||||
|
background-color: #837d63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle.color-10 {
|
||||||
|
background-color: #686a70;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Randomize color assignment */
|
||||||
|
.user-circle:nth-child(1) {
|
||||||
|
background-color: #8e8e8e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(2) {
|
||||||
|
background-color: #a57c63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(3) {
|
||||||
|
background-color: #8f7c7c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(4) {
|
||||||
|
background-color: #9a927f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(5) {
|
||||||
|
background-color: #5e5d5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(6) {
|
||||||
|
background-color: #676767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(7) {
|
||||||
|
background-color: #6d7d6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(8) {
|
||||||
|
background-color: #7a7171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-circle:nth-child(9) {
|
||||||
|
background-color: #837d63;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default to first color if more than 10 users */
|
||||||
|
.user-circle:nth-child(n+11) {
|
||||||
|
background-color: #8e8e8e;
|
||||||
|
}
|
||||||
|
|
|
@ -25,17 +25,24 @@ import Col from 'react-bootstrap/Col';
|
||||||
import Editor, { DiffEditor } from '@monaco-editor/react';
|
import Editor, { DiffEditor } from '@monaco-editor/react';
|
||||||
|
|
||||||
import MDEditor from '@uiw/react-md-editor';
|
import MDEditor from '@uiw/react-md-editor';
|
||||||
|
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||||
|
import { BACKEND_BASE_URL } from '../config';
|
||||||
|
import HttpService, { getBasicHeaders } from '../services/HttpService';
|
||||||
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
import ReactDiagramEditor from '../components/ReactDiagramEditor';
|
||||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||||
import HttpService from '../services/HttpService';
|
|
||||||
import useAPIError from '../hooks/UseApiError';
|
import useAPIError from '../hooks/UseApiError';
|
||||||
import { makeid, modifyProcessIdentifierForPathParam } from '../helpers';
|
import {
|
||||||
|
makeid,
|
||||||
|
modifyProcessIdentifierForPathParam,
|
||||||
|
encodeBase64,
|
||||||
|
} from '../helpers';
|
||||||
import {
|
import {
|
||||||
CarbonComboBoxProcessSelection,
|
CarbonComboBoxProcessSelection,
|
||||||
ProcessFile,
|
ProcessFile,
|
||||||
ProcessModel,
|
ProcessModel,
|
||||||
ProcessModelCaller,
|
ProcessModelCaller,
|
||||||
ProcessReference,
|
ProcessReference,
|
||||||
|
User,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import ProcessSearch from '../components/ProcessSearch';
|
import ProcessSearch from '../components/ProcessSearch';
|
||||||
import { Notification } from '../components/Notification';
|
import { Notification } from '../components/Notification';
|
||||||
|
@ -66,6 +73,7 @@ export default function ProcessModelEditDiagram() {
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [processModelFileInvalidText, setProcessModelFileInvalidText] =
|
const [processModelFileInvalidText, setProcessModelFileInvalidText] =
|
||||||
useState<string>('');
|
useState<string>('');
|
||||||
|
const [activeUsers, setActiveUsers] = useState<User[]>([]);
|
||||||
|
|
||||||
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
|
||||||
|
|
||||||
|
@ -124,6 +132,7 @@ export default function ProcessModelEditDiagram() {
|
||||||
|
|
||||||
usePrompt('Changes you made may not be saved.', diagramHasChanges);
|
usePrompt('Changes you made may not be saved.', diagramHasChanges);
|
||||||
|
|
||||||
|
const lastVisitedIdentifier = encodeBase64(window.location.pathname);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Grab all available process models in case we need to search for them.
|
// Grab all available process models in case we need to search for them.
|
||||||
// Taken from the Process Group List
|
// Taken from the Process Group List
|
||||||
|
@ -139,17 +148,39 @@ export default function ProcessModelEditDiagram() {
|
||||||
path: `/processes`,
|
path: `/processes`,
|
||||||
successCallback: processResults,
|
successCallback: processResults,
|
||||||
});
|
});
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const unregisterUser = () => {
|
||||||
const processResult = (result: ProcessModel) => {
|
|
||||||
setProcessModel(result);
|
|
||||||
};
|
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/${processModelPath}?include_file_references=true`,
|
path: `/active-users/unregister/${lastVisitedIdentifier}`,
|
||||||
successCallback: processResult,
|
successCallback: setActiveUsers,
|
||||||
});
|
});
|
||||||
}, [processModelPath]);
|
};
|
||||||
|
fetchEventSource(
|
||||||
|
`${BACKEND_BASE_URL}/active-users/updates/${lastVisitedIdentifier}`,
|
||||||
|
{
|
||||||
|
headers: getBasicHeaders(),
|
||||||
|
onmessage(ev) {
|
||||||
|
const retValue = JSON.parse(ev.data);
|
||||||
|
if ('error_code' in retValue) {
|
||||||
|
addError(retValue);
|
||||||
|
} else {
|
||||||
|
setActiveUsers(retValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
unregisterUser();
|
||||||
|
},
|
||||||
|
onerror(err: any) {
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// FIXME: this is not getting called when navigating away from this page.
|
||||||
|
// we do not know why yet.
|
||||||
|
return unregisterUser;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []); // it is critical to only run this once.
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fileResult = (result: any) => {
|
const fileResult = (result: any) => {
|
||||||
|
@ -157,6 +188,11 @@ export default function ProcessModelEditDiagram() {
|
||||||
setBpmnXmlForDiagramRendering(result.file_contents);
|
setBpmnXmlForDiagramRendering(result.file_contents);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `/${processModelPath}?include_file_references=true`,
|
||||||
|
successCallback: setProcessModel,
|
||||||
|
});
|
||||||
|
|
||||||
if (params.file_name) {
|
if (params.file_name) {
|
||||||
HttpService.makeCallToBackend({
|
HttpService.makeCallToBackend({
|
||||||
path: `/${processModelPath}/files/${params.file_name}`,
|
path: `/${processModelPath}/files/${params.file_name}`,
|
||||||
|
@ -930,6 +966,20 @@ export default function ProcessModelEditDiagram() {
|
||||||
return searchParams.get('file_type') === 'dmn' || fileName.endsWith('.dmn');
|
return searchParams.get('file_type') === 'dmn' || fileName.endsWith('.dmn');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const activeUserElement = () => {
|
||||||
|
const au = activeUsers.map((activeUser: User) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
title={`${activeUser.username} is also viewing this page`}
|
||||||
|
className="user-circle"
|
||||||
|
>
|
||||||
|
{activeUser.username.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return <div className="user-list">{au}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
const appropriateEditor = () => {
|
const appropriateEditor = () => {
|
||||||
if (isDmn()) {
|
if (isDmn()) {
|
||||||
return (
|
return (
|
||||||
|
@ -974,6 +1024,7 @@ export default function ProcessModelEditDiagram() {
|
||||||
onSearchProcessModels={onSearchProcessModels}
|
onSearchProcessModels={onSearchProcessModels}
|
||||||
onElementsChanged={onElementsChanged}
|
onElementsChanged={onElementsChanged}
|
||||||
callers={callers}
|
callers={callers}
|
||||||
|
activeUserElement={activeUserElement()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue