diff --git a/web-client/src/public/components/Sidebar/Sidebar.tsx b/web-client/src/public/components/Sidebar/Sidebar.tsx index 166177a..a6c6c1c 100644 --- a/web-client/src/public/components/Sidebar/Sidebar.tsx +++ b/web-client/src/public/components/Sidebar/Sidebar.tsx @@ -2,7 +2,7 @@ /* prettier-ignore */ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import * as classnames from 'classnames'; +import classnames from 'classnames'; import PerfectScrollbar from 'react-perfect-scrollbar'; import { IntlShape } from 'react-intl'; diff --git a/web-client/src/public/components/TaskCard/TaskCard.tsx b/web-client/src/public/components/TaskCard/TaskCard.tsx index 543c2e5..6f714e0 100644 --- a/web-client/src/public/components/TaskCard/TaskCard.tsx +++ b/web-client/src/public/components/TaskCard/TaskCard.tsx @@ -79,12 +79,12 @@ export interface ITaskCardProps { authUser: IAuthUser; addTaskPerformer(payload: TAddTaskPerformerPayload): void; removeTaskPerformer(payload: TRemoveTaskPerformerPayload): void; - changeWorkflowLog(value: Partial): void; + changeTaskWorkflowLog(value: Partial): void; setTaskCompleted(payload: TSetTaskCompletedPayload): void; setTaskReverted(payload: TSetTaskRevertedPayload): void; setWorkflowFinished(payload: TSetWorkflowFinishedPayload): void; - sendWorkflowLogComments(payload: ISendWorkflowLogComment): void; - changeWorkflowLogViewSettings(payload: IChangeWorkflowLogViewSettingsPayload): void; + sendTaskWorkflowLogComments(payload: ISendWorkflowLogComment): void; + changeTaskWorkflowLogViewSettings(payload: IChangeWorkflowLogViewSettingsPayload): void; setCurrentTask(task: ITask | null): void; clearWorkflow(): void; openWorkflowLogPopup(payload: TOpenWorkflowLogPopupPayload): void; @@ -103,12 +103,12 @@ export function TaskCard({ accountId, users, authUser, - changeWorkflowLog, + changeTaskWorkflowLog, setCurrentTask, setTaskCompleted, setTaskReverted, - sendWorkflowLogComments, - changeWorkflowLogViewSettings, + sendTaskWorkflowLogComments, + changeTaskWorkflowLogViewSettings, clearWorkflow, addTaskPerformer, removeTaskPerformer, @@ -490,21 +490,21 @@ export function TaskCard({ isCommentsShown={workflowLog.isCommentsShown} isOnlyAttachmentsShown={workflowLog.isOnlyAttachmentsShown} workflowId={workflowLog.workflowId} - changeWorkflowLogViewSettings={changeWorkflowLogViewSettings} includeHeader - sendComment={sendWorkflowLogComments} currentTask={workflow?.currentTask} isLogMinimized={isLogMinimized && viewMode !== ETaskCardViewMode.Guest} areTasksClickable={viewMode === ETaskCardViewMode.Single} + minimizedLogMaxEvents={MINIMIZED_LOG_MAX_EVENTS} + isCommentFieldHidden={viewMode === ETaskCardViewMode.Guest && status === ETaskStatus.Completed} + isToggleCommentHidden={viewMode === ETaskCardViewMode.Guest} + sendComment={sendTaskWorkflowLogComments} + changeWorkflowLogViewSettings={changeTaskWorkflowLogViewSettings} onUnmount={() => - changeWorkflowLog({ + changeTaskWorkflowLog({ isOnlyAttachmentsShown: false, sorting: EWorkflowsLogSorting.New, }) } - minimizedLogMaxEvents={MINIMIZED_LOG_MAX_EVENTS} - isCommentFieldHidden={viewMode === ETaskCardViewMode.Guest && status === ETaskStatus.Completed} - isToggleCommentHidden={viewMode === ETaskCardViewMode.Guest} /> {isWorkflowInfoVisible && (
diff --git a/web-client/src/public/components/TaskCard/container.ts b/web-client/src/public/components/TaskCard/container.ts index 3550eef..dc7f071 100644 --- a/web-client/src/public/components/TaskCard/container.ts +++ b/web-client/src/public/components/TaskCard/container.ts @@ -4,11 +4,8 @@ import { setTaskCompleted, setTaskReverted, setWorkflowFinished, - changeWorkflowLogViewSettings, - sendWorkflowLogComments, ETaskStatus, setCurrentTask, - changeWorkflowLog, clearWorkflow, addTaskPerformer, removeTaskPerformer, @@ -16,6 +13,9 @@ import { setCurrentTaskDueDate, deleteCurrentTaskDueDate, openSelectTemplateModal, + changeTaskWorkflowLogViewSettings, + changeTaskWorkflowLog, + sendTaskWorkflowLogComments, } from '../../redux/actions'; import { IApplicationState } from '../../types/redux'; import { getNotDeletedUsers } from '../../utils/users'; @@ -29,10 +29,10 @@ type TDispatchProps = Pick< | 'setTaskCompleted' | 'setTaskReverted' | 'setWorkflowFinished' - | 'changeWorkflowLogViewSettings' - | 'sendWorkflowLogComments' + | 'changeTaskWorkflowLogViewSettings' + | 'sendTaskWorkflowLogComments' | 'setCurrentTask' - | 'changeWorkflowLog' + | 'changeTaskWorkflowLog' | 'clearWorkflow' | 'addTaskPerformer' | 'removeTaskPerformer' @@ -43,9 +43,8 @@ type TDispatchProps = Pick< >; export function mapStateToProps({ - workflows: { workflowLog, workflow, isWorkflowLoading }, authUser, - task: { data: task, status }, + task: { data: task, status, workflowLog, workflow, isWorkflowLoading }, accounts: { users }, }: IApplicationState): TStoreProps { const taskStatus = task?.isCompleted ? ETaskStatus.Completed : status; @@ -66,10 +65,10 @@ export const mapDispatchToProps: TDispatchProps = { setTaskCompleted, setTaskReverted, setWorkflowFinished, - changeWorkflowLogViewSettings, - sendWorkflowLogComments, + changeTaskWorkflowLogViewSettings, + sendTaskWorkflowLogComments, setCurrentTask, - changeWorkflowLog, + changeTaskWorkflowLog, clearWorkflow, addTaskPerformer, removeTaskPerformer, diff --git a/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/Conditions.tsx b/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/Conditions.tsx index 43ad53a..1103ac1 100644 --- a/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/Conditions.tsx +++ b/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/Conditions.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import produce from 'immer'; import { useIntl } from 'react-intl'; -import * as classnames from 'classnames'; +import classnames from 'classnames'; import { isArrayWithItems } from '../../../../utils/helpers'; import { DropdownList } from '../../../UI/DropdownList'; diff --git a/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/utils/getEmptyConditions.ts b/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/utils/getEmptyConditions.ts index 82b1a8f..78dec55 100644 --- a/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/utils/getEmptyConditions.ts +++ b/web-client/src/public/components/TemplateEdit/TaskForm/Conditions/utils/getEmptyConditions.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ -/* prettier-ignore */ import { createConditionApiName } from '../../../../../utils/createId'; import { EConditionAction, ICondition } from '../types'; import { getEmptyRule } from './getEmptyRule'; @@ -12,7 +10,7 @@ export function getEmptyConditions(isSubscribed: boolean): ICondition[] { order: 1, apiName: createConditionApiName(), rules, - action: EConditionAction.EndProcess, + action: EConditionAction.StartTask, }, ]; } diff --git a/web-client/src/public/components/TemplateEdit/TaskForm/DueDate/DueDate.tsx b/web-client/src/public/components/TemplateEdit/TaskForm/DueDate/DueDate.tsx index 443fe56..0467e7f 100644 --- a/web-client/src/public/components/TemplateEdit/TaskForm/DueDate/DueDate.tsx +++ b/web-client/src/public/components/TemplateEdit/TaskForm/DueDate/DueDate.tsx @@ -19,57 +19,41 @@ interface IDueInProps { } type TDueDateKeys = keyof ITemplateTask['rawDueDate']; -export function DueDate({ - dueDate, - currentTask, - tasks, - kickoff, - onChange, -}: IDueInProps) { +export function DueDate({ dueDate, currentTask, tasks, kickoff, onChange }: IDueInProps) { const { formatMessage } = useIntl(); const prepositionOptions = React.useMemo(() => { - return getRulePrepositionOptions(dueDate.ruleTarget) + return getRulePrepositionOptions(dueDate.ruleTarget); }, [dueDate.ruleTarget]); - const [ - systemRules, - dateFieldsRules, - tasksRules, - ] = React.useMemo(() => { - return getRuleTargetOptions( - currentTask, - tasks, - kickoff, - ); + const [systemRules, dateFieldsRules, tasksRules] = React.useMemo(() => { + return getRuleTargetOptions(currentTask, tasks, kickoff); }, [tasks, kickoff]); const currentPrepositionOption = React.useMemo(() => { - return prepositionOptions.find(rule => rule.rulePreposition === dueDate.rulePreposition) || null; + return prepositionOptions.find((rule) => rule.rulePreposition === dueDate.rulePreposition) || null; }, [dueDate.rulePreposition]); const currentTargetOption = React.useMemo(() => { - return [ - ...systemRules, - ...dateFieldsRules, - ...tasksRules, - ].find(rule => rule.sourceId === dueDate.sourceId && rule.ruleTarget === dueDate.ruleTarget) || null; + return ( + [...systemRules, ...dateFieldsRules, ...tasksRules].find( + (rule) => rule.sourceId === dueDate.sourceId && rule.ruleTarget === dueDate.ruleTarget, + ) || null + ); }, [dueDate.sourceId, dueDate.ruleTarget]); - useUpdatePreposition( - prepositionOptions, - currentPrepositionOption, - currentTargetOption, - option => onChange({ + useUpdatePreposition(prepositionOptions, currentPrepositionOption, currentTargetOption, (option) => + onChange({ ...dueDate, rulePreposition: option, - }) + }), ); - const handleChange = (field: T) => - (value: ITemplateTask['rawDueDate'][T]) => { - onChange({ ...dueDate, [field]: value }); - } + const handleChange = + (field: T) => + (value: ITemplateTask['rawDueDate'][T]) => { + onChange({ ...dueDate, [field]: value }); + }; return (
@@ -88,7 +72,7 @@ export function DueDate({ onChange({ ...dueDate, rulePreposition: option.rulePreposition, - }) + }); }} isClearable={false} options={prepositionOptions} @@ -104,7 +88,7 @@ export function DueDate({ ...dueDate, sourceId: option.sourceId, ruleTarget: option.ruleTarget, - }) + }); }} isClearable={false} options={[ diff --git a/web-client/src/public/components/TemplateEdit/TaskItem/TaskItemUsers/TaskItemUsers.tsx b/web-client/src/public/components/TemplateEdit/TaskItem/TaskItemUsers/TaskItemUsers.tsx index 3abd434..1a671ec 100644 --- a/web-client/src/public/components/TemplateEdit/TaskItem/TaskItemUsers/TaskItemUsers.tsx +++ b/web-client/src/public/components/TemplateEdit/TaskItem/TaskItemUsers/TaskItemUsers.tsx @@ -59,7 +59,7 @@ export function TaskItemUsers({ task, maxUsers = MAX_SHOW_USERS, onClick }: ITas }, }; - return performerRenderMap[performer.type](performer); + return performer.type && performerRenderMap[performer.type](performer); }; return ( diff --git a/web-client/src/public/components/TemplateEdit/TemplateEdit.tsx b/web-client/src/public/components/TemplateEdit/TemplateEdit.tsx index 9fd3608..6df9ce8 100644 --- a/web-client/src/public/components/TemplateEdit/TemplateEdit.tsx +++ b/web-client/src/public/components/TemplateEdit/TemplateEdit.tsx @@ -16,7 +16,6 @@ import { EMPTY_DURATION, DEFAULT_TEMPLATE_NAME } from './constants'; import { getVariables } from './TaskForm/utils/getTaskVariables'; import { TemplateIntegrations } from './Integrations'; import { TemplateControllsContainer } from './TemplateControlls'; - import { NAVBAR_HEIGHT } from '../../constants/defaultValues'; import { TemplateLastUpdateInfo } from './TemplateLastUpdateInfo'; import { ERoutes } from '../../constants/routes'; @@ -29,15 +28,47 @@ import { NotificationManager } from '../UI/Notifications'; import { isArrayWithItems } from '../../utils/helpers'; import { createTaskApiName, createUUID } from '../../utils/createId'; import { EMoveDirections } from '../../types/workflow'; -import { ITemplate, ITemplateTask } from '../../types/template'; +import { ETaskPerformerType, ITemplate, ITemplateTask } from '../../types/template'; import { EditableText } from '../UI/EditableText'; import { TLoadTemplateVariablesSuccessPayload } from '../../redux/actions'; import { ETemplateStatus, IAuthUser } from '../../types/redux'; -import { createEmptyDueDate } from '../../utils/dueDate/createEmptyDueDate'; - -import styles from './TemplateEdit.css'; +import { createStartedTaskDueDate } from '../../utils/dueDate/createStartedTaskDueDate'; import { usePrevious } from '../../hooks/usePrevious'; import { ConditionsBanner } from './ConditionsBanner'; +import { getUserFullName } from '../../utils/users'; + +import styles from './TemplateEdit.css'; + +export interface ITemplateEditProps { + match: any; + location: any; + authUser: IAuthUser; + template: ITemplate; + aiTemplate: ITemplate | null; + templateStatus: ETemplateStatus; + users: TUserListItem[]; + isSubscribed: boolean; + loadTemplate(id: number): void; + loadTemplateFromSystem(id: string): void; + resetTemplateStore(): void; + saveTemplate(): void; + setTemplate(payload: ITemplate): void; + setTemplateStatus(status: ETemplateStatus): void; + loadTemplateVariablesSuccess(payload: TLoadTemplateVariablesSuccessPayload): void; +} + +export type TTemplateEditProps = ITemplateEditProps & RouteComponentProps; + +export interface ITemplateEditParams { + id: string; +} + +export interface ITemplateEditState { + isInfoWarningsModaOpen: boolean; + infoWarnings: ((props: IInfoWarningProps) => JSX.Element)[]; + openedTasks: { [key: string]: boolean }; + openedDelays: { [key: string]: boolean }; +} export function TemplateEdit({ match, @@ -156,20 +187,28 @@ export function TemplateEdit({ }; const sortedTasks = () => [...tasks].sort((a, b) => a.number - b.number); - const getNewTask = (templateTask?: Partial): ITemplateTask => { + const taskApiName = createTaskApiName(); + return { - apiName: createTaskApiName(), + apiName: taskApiName, delay: null, description: '', name: 'New Step', number: 1, fields: [], - rawPerformers: [], + rawPerformers: [ + { + id: authUser.id, + label: getUserFullName(authUser), + type: ETaskPerformerType.User, + sourceId: String(authUser.id), + }, + ], uuid: createUUID(), - requireCompletionByAll: true, + requireCompletionByAll: false, conditions: getEmptyConditions(isSubscribed), - rawDueDate: createEmptyDueDate(), + rawDueDate: createStartedTaskDueDate(taskApiName), checklists: [], ...templateTask, }; @@ -464,34 +503,3 @@ export function TemplateEdit({
); } - -export interface ITemplateEditProps { - match: any; - location: any; - authUser: IAuthUser; - template: ITemplate; - aiTemplate: ITemplate | null; - templateStatus: ETemplateStatus; - users: TUserListItem[]; - isSubscribed: boolean; - loadTemplate(id: number): void; - loadTemplateFromSystem(id: string): void; - resetTemplateStore(): void; - saveTemplate(): void; - setTemplate(payload: ITemplate): void; - setTemplateStatus(status: ETemplateStatus): void; - loadTemplateVariablesSuccess(payload: TLoadTemplateVariablesSuccessPayload): void; -} - -export type TTemplateEditProps = ITemplateEditProps & RouteComponentProps; - -export interface ITemplateEditParams { - id: string; -} - -export interface ITemplateEditState { - isInfoWarningsModaOpen: boolean; - infoWarnings: ((props: IInfoWarningProps) => JSX.Element)[]; - openedTasks: { [key: string]: boolean }; - openedDelays: { [key: string]: boolean }; -} diff --git a/web-client/src/public/components/TemplateEdit/utils/__tests__/getRunnableWorkflow.test.ts b/web-client/src/public/components/TemplateEdit/utils/__tests__/getRunnableWorkflow.test.ts index a2a6875..ef2eab5 100644 --- a/web-client/src/public/components/TemplateEdit/utils/__tests__/getRunnableWorkflow.test.ts +++ b/web-client/src/public/components/TemplateEdit/utils/__tests__/getRunnableWorkflow.test.ts @@ -23,12 +23,14 @@ const templateResponseMock: ITemplateResponse = { sourceId: '306', type: ETaskPerformerType.User, label: 'Yevgeniy Tsymbalyuk', + apiName: 'raw-performer-024t43' }, { id: 10060, sourceId: null, type: ETaskPerformerType.WorkflowStarter, label: 'Workflow starter', + apiName: 'raw-performer-024t43' }, ], checklists: [], @@ -60,6 +62,7 @@ const templateResponseMock: ITemplateResponse = { sourceId: '306', type: ETaskPerformerType.User, label: 'Yevgeniy Tsymbalyuk', + apiName: 'raw-performer-024t43' }, ], checklists: [], @@ -81,6 +84,7 @@ const templateResponseMock: ITemplateResponse = { sourceId: '306', type: ETaskPerformerType.User, label: 'Yevgeniy Tsymbalyuk', + apiName: 'raw-performer-024t43' }, ], checklists: [], @@ -102,6 +106,7 @@ const templateResponseMock: ITemplateResponse = { sourceId: '306', type: ETaskPerformerType.User, label: 'Yevgeniy Tsymbalyuk', + apiName: 'raw-performer-024t43' }, ], checklists: [], @@ -124,6 +129,7 @@ const templateResponseMock: ITemplateResponse = { sourceId: '306', type: ETaskPerformerType.User, label: 'Yevgeniy Tsymbalyuk', + apiName: 'raw-performer-024t43' }, ], checklists: [], diff --git a/web-client/src/public/components/TemplateEdit/utils/getClonedTask.ts b/web-client/src/public/components/TemplateEdit/utils/getClonedTask.ts index 1804e46..6a644e5 100644 --- a/web-client/src/public/components/TemplateEdit/utils/getClonedTask.ts +++ b/web-client/src/public/components/TemplateEdit/utils/getClonedTask.ts @@ -3,6 +3,7 @@ import { IExtraField, IExtraFieldSelection, ITemplateTask, + ITemplateTaskPerformer, TOutputChecklist, TOutputChecklistItem, } from '../../../types/template'; @@ -17,6 +18,7 @@ import { createСhecklistApiName, createСhecklistSelectionApiName, createDueDateApiName, + createPerformerApiName, } from '../../../utils/createId'; import { omit } from '../../../utils/helpers'; import { ICondition, TConditionRule } from '../TaskForm/Conditions'; @@ -115,12 +117,20 @@ export function getClonedTask(task: ITemplateTask) { }; }; + const cloneRawPerformers = (rawPerformers: ITemplateTaskPerformer[]): ITemplateTaskPerformer[] => { + return rawPerformers.map((performer) => ({ + ...omit(performer, ['apiName']), + apiName: createPerformerApiName(), + })); + }; + const clonedChecklist = cloneChecklists(task.checklists); const clonedDescription = cloneDescription(task.description); const clonedRawDueDate = cloneRawDueDate(task.rawDueDate); + const clonedTask: ITemplateTask = { - ...omit(task, ['id', 'apiName', 'uuid', 'fields', 'conditions', 'delay', 'name', 'description', 'rawDueDate']), + ...omit(task, ['id', 'apiName', 'uuid', 'fields', 'conditions', 'delay', 'name', 'description', 'rawDueDate', 'rawPerformers']), name: `${task.name} (Clone)`, checklists: clonedChecklist, description: clonedDescription, @@ -129,7 +139,8 @@ export function getClonedTask(task: ITemplateTask) { fields: cloneFields(task.fields), conditions: cloneConditions(task.conditions), delay: null, - rawDueDate: clonedRawDueDate + rawDueDate: clonedRawDueDate, + rawPerformers: cloneRawPerformers(task.rawPerformers), }; return clonedTask; diff --git a/web-client/src/public/components/UI/Dropdown/Dropdown.tsx b/web-client/src/public/components/UI/Dropdown/Dropdown.tsx index 528e632..6f06fb6 100644 --- a/web-client/src/public/components/UI/Dropdown/Dropdown.tsx +++ b/web-client/src/public/components/UI/Dropdown/Dropdown.tsx @@ -66,7 +66,7 @@ export function Dropdown({ }; const renderOptions = (items: TDropdownOption[], level = 0): React.ReactNode => { - const renderedOptions = items.map((option) => { + const renderedOptions = items.map((option, index) => { const { label, color, @@ -80,9 +80,7 @@ export function Dropdown({ onClick, } = option; - if (isHidden) { - return null; - } + if (isHidden) return null; const renderOptionContent = () => { if (typeof label === 'string') { @@ -99,8 +97,10 @@ export function Dropdown({ }; if (!isArrayWithItems(subOptions)) { + const stringLabel = typeof label === 'string' ? label : index; + return ( -
+
{withUpperline &&
} onClick?.(() => setIsOpen(false)) })} diff --git a/web-client/src/public/components/UI/Filter/Filter.tsx b/web-client/src/public/components/UI/Filter/Filter.tsx index 4eada58..7812260 100644 --- a/web-client/src/public/components/UI/Filter/Filter.tsx +++ b/web-client/src/public/components/UI/Filter/Filter.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; // tslint:disable-next-line: no-duplicate-imports import { useCallback, useMemo, useState, useEffect } from 'react'; import { useIntl } from 'react-intl'; -import * as classnames from 'classnames'; +import classnames from 'classnames'; import { Checkbox, InputField, Loader, RadioButton, TCheckboxTriState } from '..'; import { isArrayWithItems } from '../../../utils/helpers'; @@ -247,7 +247,7 @@ export function Filter +
{optionsToShow.map((option) => ( -
+
+ ); diff --git a/web-client/src/public/components/Workflows/WorkflowControlls/WorkflowControlls.tsx b/web-client/src/public/components/Workflows/WorkflowControlls/WorkflowControlls.tsx index bf321f0..20c30c0 100644 --- a/web-client/src/public/components/Workflows/WorkflowControlls/WorkflowControlls.tsx +++ b/web-client/src/public/components/Workflows/WorkflowControlls/WorkflowControlls.tsx @@ -129,6 +129,7 @@ export function WorkflowControllsComponents({ setIsUrgent(isChecked); dispatch( editWorkflow({ + typeChange: 'control', isUrgent: isChecked, workflowId, }), diff --git a/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLog.tsx b/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLog.tsx index f2322b9..733dd2f 100644 --- a/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLog.tsx +++ b/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLog.tsx @@ -88,11 +88,11 @@ export const WorkflowLog = ({ values={[ { id: EWorkflowLogAttachmentsModes.Timeline, - label: formatMessage({id: 'sorting.timeline'}), + label: formatMessage({ id: 'sorting.timeline' }), }, { id: EWorkflowLogAttachmentsModes.Attachments, - label: formatMessage({id: 'sorting.attachments'}), + label: formatMessage({ id: 'sorting.attachments' }), }, ]} onChange={toggleAttachments} @@ -190,7 +190,7 @@ export const WorkflowLog = ({ } if (isLoading) { - return ; + return ; } const normalizedItems = isLogMinimized && minimizedLogMaxEvents ? items.slice(0, minimizedLogMaxEvents) : items; diff --git a/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.css b/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.css index 8ceccd3..2b47d9e 100644 --- a/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.css +++ b/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.css @@ -2,7 +2,6 @@ margin-bottom: 16px; overflow: visible; width: 100%; - background-color: white; } .skeleton__data { @@ -11,7 +10,10 @@ align-items: center; overflow: hidden; flex-flow: column; - background-color: white; + + &.is-white { + background-color: red; + } @media (--mobile) { padding-bottom: 12px; diff --git a/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.tsx b/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.tsx index 6bae2a3..bc39a8a 100644 --- a/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.tsx +++ b/web-client/src/public/components/Workflows/WorkflowLog/WorkflowLogSkeleton/WorkflowLogSkeleton.tsx @@ -2,20 +2,22 @@ import React from 'react'; import classnames from 'classnames'; import { Skeleton } from '../../../UI/Skeleton'; +import { TWorkflowLogTheme } from '../WorkflowLog'; import styles from './WorkflowLogSkeleton.css'; export interface IWorkflowLogSkeletonProps { className?: string; + theme?: TWorkflowLogTheme; } -export function WorkflowLogSkeleton({ className }: IWorkflowLogSkeletonProps) { +export function WorkflowLogSkeleton({ className, theme }: IWorkflowLogSkeletonProps) { return ( -
+
- - - + + +
); diff --git a/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModal.tsx b/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModal.tsx index aa531c0..7bb2aa1 100644 --- a/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModal.tsx +++ b/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModal.tsx @@ -60,6 +60,7 @@ export interface IWorkflowModalStoreProps { workflowEdit: IWorkflowEdit; items: IWorkflowLogItem[]; isLoading?: boolean; + isLogLoading: boolean; canEdit: boolean; isRunWorkflowOpen: boolean; isFullscreenImageOpen: boolean; @@ -326,6 +327,7 @@ export class WorkflowModal extends React.Component { isLoading, changeWorkflowLogViewSettings, sendWorkflowLogComments, + isLogLoading } = this.props; if (isLoading) { @@ -336,9 +338,7 @@ export class WorkflowModal extends React.Component { ); } - if (!workflow) { - return null; - } + if (!workflow) return
; return ( <> @@ -372,6 +372,7 @@ export class WorkflowModal extends React.Component { theme="white" items={items} sorting={sorting} + isLoading={isLogLoading} isCommentsShown={isCommentsShown} isOnlyAttachmentsShown={isOnlyAttachmentsShown} workflowId={workflowId} diff --git a/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModalHeaderProgressBar/WorkflowModalHeaderProgressBar.tsx b/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModalHeaderProgressBar/WorkflowModalHeaderProgressBar.tsx index e73a0a1..cf85276 100644 --- a/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModalHeaderProgressBar/WorkflowModalHeaderProgressBar.tsx +++ b/web-client/src/public/components/Workflows/WorkflowModal/WorkflowModalHeaderProgressBar/WorkflowModalHeaderProgressBar.tsx @@ -1,7 +1,7 @@ /* eslint-disable */ /* prettier-ignore */ import * as React from 'react'; -import * as classnames from 'classnames'; +import classnames from 'classnames'; import { CircleProgressBar } from '../../../CircleProgressBar'; import { EProgressbarColor, ProgressBar } from '../../../ProgressBar'; diff --git a/web-client/src/public/components/Workflows/WorkflowModal/container.ts b/web-client/src/public/components/Workflows/WorkflowModal/container.ts index ca26dfa..91905f1 100644 --- a/web-client/src/public/components/Workflows/WorkflowModal/container.ts +++ b/web-client/src/public/components/Workflows/WorkflowModal/container.ts @@ -22,6 +22,7 @@ export type TStoreProps = Pick< | 'sorting' | 'isCommentsShown' | 'isOnlyAttachmentsShown' + | 'isLogLoading' | 'workflow' | 'items' | 'canEdit' @@ -47,7 +48,7 @@ export type TDispatchProps = Pick< export function mapStateToProps({ authUser: { id: currentUserId, isAccountOwner, timezone, dateFmt, language }, workflows: { - workflowLog: { workflowId, isCommentsShown, isOnlyAttachmentsShown, isOpen, items, sorting }, + workflowLog: { workflowId, isCommentsShown, isOnlyAttachmentsShown, isOpen, items, sorting, isLoading: isLogLoading }, isWorkflowLoading, workflow, workflowEdit, @@ -70,6 +71,7 @@ export function mapStateToProps({ canEdit, workflowEdit, isCommentsShown, + isLogLoading, isOnlyAttachmentsShown, isLoading: isWorkflowLoading, isOpen, diff --git a/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowCard/WorkflowCard.tsx b/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowCard/WorkflowCard.tsx index 6c78643..eae61c1 100644 --- a/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowCard/WorkflowCard.tsx +++ b/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowCard/WorkflowCard.tsx @@ -1,6 +1,6 @@ /* eslint-disable */ import * as React from 'react'; -import * as classnames from 'classnames'; +import classnames from 'classnames'; import { IntlShape } from 'react-intl'; import { EWorkflowStatus, IWorkflow } from '../../../../types/workflow'; @@ -67,17 +67,13 @@ export class WorkflowCard extends React.PureComponent { const progress = getWorkflowProgress({ currentTask, tasksCount, status }); const getSnoozedText = () => { - if (!delay) { - return ''; - } + if (!delay) return ''; return formatMessage({ id: 'task.log-delay' }, { date: getSnoozedUntilDate(delay) }); }; const renderCardFooter = () => { - if (status !== EWorkflowStatus.Running) { - return null; - } + if (status !== EWorkflowStatus.Running) return null; return (
diff --git a/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowsGridPage.tsx b/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowsGridPage.tsx index cb573f9..033ab67 100644 --- a/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowsGridPage.tsx +++ b/web-client/src/public/components/Workflows/WorkflowsGridPage/WorkflowsGridPage.tsx @@ -26,7 +26,7 @@ import { WorkflowsFiltersContainer } from './WorkflowsFilters'; import styles from './WorkflowsGridPage.css'; -export const WorkflowsGridPage = React.memo(function Workflows({ +export const WorkflowsGridPage = function Workflows({ workflowsLoadingStatus, workflowsList: { count, items }, templatesFilter, @@ -122,7 +122,7 @@ export const WorkflowsGridPage = React.memo(function Workflows({ const renderContent = () => { if (workflowsLoadingStatus === EWorkflowsLoadingStatus.LoadingList) { const INIT_SKELETION_QUANTITY = 16; - const loader = Array(INIT_SKELETION_QUANTITY).fill(); + const loader = Array(INIT_SKELETION_QUANTITY).map((item) => ); return
{loader}
; } @@ -130,7 +130,7 @@ export const WorkflowsGridPage = React.memo(function Workflows({ const SCROLL_LOADERS_QUANTITY = 2; const isListFullLoaded = count === items.length && workflowsLoadingStatus !== EWorkflowsLoadingStatus.LoadingNextPage; - const loader = Array(SCROLL_LOADERS_QUANTITY).fill(); + const loader = Array(SCROLL_LOADERS_QUANTITY).map((item) => ); return ( removeWorkflowFromList({ workflowId: item.id })} - onWorkflowSnoozed={() => removeWorkflowFromList({ workflowId: item.id })} + onWorkflowEnded={() => loadWorkflowsList(0)} onWorkflowDeleted={() => removeWorkflowFromList({ workflowId: item.id })} - onWorkflowResumed={() => removeWorkflowFromList({ workflowId: item.id })} /> ))} @@ -170,7 +168,7 @@ export const WorkflowsGridPage = React.memo(function Workflows({ setSearchQuery(e.currentTarget.value)} + onChange={(e) => setSearchQuery(e.currentTarget.value)} containerClassName={styles['search-field']} className={styles['search-field__input']} placeholder={formatMessage({ id: 'workflows.search' })} @@ -183,4 +181,4 @@ export const WorkflowsGridPage = React.memo(function Workflows({
); -}); +}; diff --git a/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/Columns/Cells/WorkflowColumn/WorkflowColumn.tsx b/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/Columns/Cells/WorkflowColumn/WorkflowColumn.tsx index 078fdcf..7432a86 100644 --- a/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/Columns/Cells/WorkflowColumn/WorkflowColumn.tsx +++ b/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/Columns/Cells/WorkflowColumn/WorkflowColumn.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as classnames from 'classnames'; +import classnames from 'classnames'; import { CellProps } from 'react-table'; import { Link } from 'react-router-dom'; diff --git a/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/WorkflowsTable.tsx b/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/WorkflowsTable.tsx index fc28633..86f4f16 100644 --- a/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/WorkflowsTable.tsx +++ b/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTable/WorkflowsTable.tsx @@ -232,10 +232,8 @@ export function WorkflowsTable({ Cell: (props) => ( removeWorkflowFromList({ workflowId: props.value.id })} - onWorkflowSnoozed={() => removeWorkflowFromList({ workflowId: props.value.id })} + onWorkflowEnded={() => loadWorkflowsList(0)} onWorkflowDeleted={() => removeWorkflowFromList({ workflowId: props.value.id })} - onWorkflowResumed={() => removeWorkflowFromList({ workflowId: props.value.id })} handleOpenModal={() => openWorkflowLogPopup({ workflowId: props.value.id, diff --git a/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTablePage.tsx b/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTablePage.tsx index 8401d8d..669d1b4 100644 --- a/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTablePage.tsx +++ b/web-client/src/public/components/Workflows/WorkflowsTablePage/WorkflowsTablePage.tsx @@ -17,6 +17,7 @@ export const WorkflowsTablePage = function Workflows({ openWorkflowLogPopup, removeWorkflowFromList, }: IWorkflowsProps) { + return ( <>
diff --git a/web-client/src/public/layout/MainLayout/MainLayout.tsx b/web-client/src/public/layout/MainLayout/MainLayout.tsx index 75fb4a7..4b6e1ff 100644 --- a/web-client/src/public/layout/MainLayout/MainLayout.tsx +++ b/web-client/src/public/layout/MainLayout/MainLayout.tsx @@ -89,30 +89,29 @@ export function MainLayout({ const accountOwner = users.filter((localUser) => localUser.isAccountOwner)[0] as IUnsavedUser; const isPlanExpired = - pendingActions.includes(EPlanActions.ChoosePlan) && - !checkSomeRouteIsActive(...EXPIRED_TRIAL_PERMITTED_ROUTES); + pendingActions.includes(EPlanActions.ChoosePlan) && !checkSomeRouteIsActive(...EXPIRED_TRIAL_PERMITTED_ROUTES); React.useEffect(() => { - if (user.account.paymentCardProvided) { - loadNotificationsList(); - watchUserWSEventsAction(); - loadTasksCount(); - loadUsers(); - loadActiveUsersCount(); - generateMenu(); - loadPlan(); - - if (isEnvPush) initFirebaseMessaging(); - - if ( - user.isAdmin && - user.account.leaseLevel !== 'tenant' && - user.account.billingPlan !== ESubscriptionPlan.Free && - user.account.isSubscribed - ) { - loadTenantsCount(); - } + // if (user.account.paymentCardProvided) { + loadNotificationsList(); + watchUserWSEventsAction(); + loadTasksCount(); + loadUsers(); + loadActiveUsersCount(); + generateMenu(); + loadPlan(); + + if (isEnvPush) initFirebaseMessaging(); + + if ( + user.isAdmin && + user.account.leaseLevel !== 'tenant' && + user.account.billingPlan !== ESubscriptionPlan.Free && + user.account.isSubscribed + ) { + loadTenantsCount(); } + // } }, [user.id, user.account.paymentCardProvided]); React.useLayoutEffect(() => { diff --git a/web-client/src/public/layout/TasksLayout/TasksLayout.tsx b/web-client/src/public/layout/TasksLayout/TasksLayout.tsx index 6e013dd..59bcad6 100644 --- a/web-client/src/public/layout/TasksLayout/TasksLayout.tsx +++ b/web-client/src/public/layout/TasksLayout/TasksLayout.tsx @@ -1,9 +1,10 @@ import React, { useEffect } from 'react'; import { matchPath } from 'react-router-dom'; import { useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; import { TasksSortingContainer } from './TasksSortingContainer'; -import { TLoadTasksFilterStepsPayload } from '../../redux/actions'; +import { loadWorkflowsList, TLoadTasksFilterStepsPayload } from '../../redux/actions'; import { TopNavContainer } from '../../components/TopNav'; import { ERoutes } from '../../constants/routes'; import { FilterIcon } from '../../components/icons'; @@ -65,6 +66,7 @@ export function TasksLayoutComponent({ clearFilters, changeTasksSorting, }: TTasksLayoutProps) { + const dispatch = useDispatch(); const { formatMessage } = useIntl(); const renderTaskDetailLeftContent = () => { return ( @@ -236,7 +238,10 @@ export function TasksLayoutComponent({
{ + dispatch(loadWorkflowsList(0)); + closeWorkflowLogPopup() + }} onWorkflowSnoozed={() => { closeWorkflowLogPopup(); diff --git a/web-client/src/public/layout/WorkflowsLayout/WorkflowsLayout.tsx b/web-client/src/public/layout/WorkflowsLayout/WorkflowsLayout.tsx index c9e36f7..fd38973 100644 --- a/web-client/src/public/layout/WorkflowsLayout/WorkflowsLayout.tsx +++ b/web-client/src/public/layout/WorkflowsLayout/WorkflowsLayout.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; +import { useDispatch } from 'react-redux'; -import { TRemoveWorkflowFromListPayload } from '../../redux/actions'; +import { loadWorkflowsList, TRemoveWorkflowFromListPayload } from '../../redux/actions'; import { TopNavContainer } from '../../components/TopNav'; import { ERoutes } from '../../constants/routes'; import { history } from '../../utils/history'; @@ -55,6 +56,7 @@ export function WorkflowsLayoutComponent({ updateWorkflowsTemplateStepsCounters, updateWorkflowStartersCounters, }: IWorkflowsLayoutComponentProps) { + const dispatch = useDispatch(); const { formatMessage } = useIntl(); const [isLoadSteps, setIsLoadSteps] = useState(false); @@ -224,10 +226,10 @@ export function WorkflowsLayoutComponent({ removeWorkflowFromList({ workflowId })} + onWorkflowEnded={() => { + dispatch(loadWorkflowsList(0)); + }} onWorkflowDeleted={() => removeWorkflowFromList({ workflowId })} - onWorkflowSnoozed={() => removeWorkflowFromList({ workflowId })} - onWorkflowResumed={() => removeWorkflowFromList({ workflowId })} /> diff --git a/web-client/src/public/redux/selectors/task.ts b/web-client/src/public/redux/selectors/task.ts index 2f458eb..e249eaa 100644 --- a/web-client/src/public/redux/selectors/task.ts +++ b/web-client/src/public/redux/selectors/task.ts @@ -1,7 +1,8 @@ -import { IApplicationState } from '../../types/redux'; +import { IApplicationState, IWorkflowLog } from '../../types/redux'; import { ITask } from '../../types/tasks'; import { ETaskStatus } from '../actions'; export const getCurrentTask = (state: IApplicationState): ITask | null => state.task.data; +export const getCurrentTaskWorkflowLog = (state: IApplicationState): IWorkflowLog => state.task.workflowLog; export const getCurrentTaskStatus = (state: IApplicationState): ETaskStatus => state.task.status; export const getTaskPerformers = (state: IApplicationState): number[] => state.task.data?.performers || []; diff --git a/web-client/src/public/redux/task/actions.ts b/web-client/src/public/redux/task/actions.ts index e1be1ec..c3bf8df 100644 --- a/web-client/src/public/redux/task/actions.ts +++ b/web-client/src/public/redux/task/actions.ts @@ -1,14 +1,20 @@ -/* eslint-disable */ -/* prettier-ignore */ -import { ITypedReduxAction } from '../../types/redux'; +import { ITypedReduxAction, IWorkflowLog } from '../../types/redux'; import { actionGenerator } from '../../utils/redux'; import { IExtraField } from '../../types/template'; import { ITask } from '../../types/tasks'; import { ETaskCardViewMode } from '../../components/TaskCard'; +import { IWorkflowDetails, IWorkflowLogItem } from '../../types/workflow'; +import { IChangeWorkflowLogViewSettingsPayload, ISendWorkflowLogComment } from '../actions'; export const enum ETaskActions { LoadCurrentTask = 'LOAD_CURRENT_TASK', SetCurrentTask = 'SET_CURRENT_TASK', + ChangeTaskWorkflowLog = 'CHANGE_TASK_WORKFLOW_LOG', + ChangeTaskWorkflow = 'CHANGE_TASK_WORKFLOW', + SetTaskWorkflowIsLoading = 'SET_TASK_WORKFLOW_IS_LOADING', + ChangeTaskWorkflowLogViewSettings = 'CHANGE_TASK_WORKFLOW_LOG_VIEW_SETTINGS', + SendTaskWorkflowLogComment = 'SEND_TASK_WORKFLOW_LOG_COMMENT', + UpdateTaskWorkflowLogItem = 'UPDATE_TASK_WORKFLOW_LOG_ITEM', PatchCurrentTask = 'PATCH_CURRENT_TASK', SetTaskCompleted = 'SET_TASK_COMPLETED', SetTaskReverted = 'SET_TASK_REVERTED', @@ -41,6 +47,35 @@ export const setCurrentTask: (payload: ITask | null) => TSetCurrentTask = action ITask | null >(ETaskActions.SetCurrentTask); +export type TChangeTaskWorkflow = ITypedReduxAction; +export const changeTaskWorkflow: (payload: IWorkflowDetails) => TChangeTaskWorkflow = actionGenerator< + ETaskActions.ChangeTaskWorkflow, + IWorkflowDetails +>(ETaskActions.ChangeTaskWorkflow); + +export type TChangeTaskWorkflowLog = ITypedReduxAction>; +export const changeTaskWorkflowLog: (payload: Partial) => TChangeTaskWorkflowLog = actionGenerator< + ETaskActions.ChangeTaskWorkflowLog, + Partial +>(ETaskActions.ChangeTaskWorkflowLog); + +export type TSetTaskWorkflowIsLoading = ITypedReduxAction; +export const setTaskWorkflowIsLoading: (payload: boolean) => TSetTaskWorkflowIsLoading = actionGenerator< + ETaskActions.SetTaskWorkflowIsLoading, + boolean +>(ETaskActions.SetTaskWorkflowIsLoading); + +export type TChangeTaskWorkflowLogViewSettings = ITypedReduxAction< + ETaskActions.ChangeTaskWorkflowLogViewSettings, + IChangeWorkflowLogViewSettingsPayload +>; +export const changeTaskWorkflowLogViewSettings: ( + payload: IChangeWorkflowLogViewSettingsPayload, +) => TChangeTaskWorkflowLogViewSettings = actionGenerator< + ETaskActions.ChangeTaskWorkflowLogViewSettings, + IChangeWorkflowLogViewSettingsPayload +>(ETaskActions.ChangeTaskWorkflowLogViewSettings); + export type TPatchCurrentTask = ITypedReduxAction>; export const patchCurrentTask: (payload: Partial) => TPatchCurrentTask = actionGenerator< ETaskActions.PatchCurrentTask, @@ -166,8 +201,33 @@ export const setChecklistsHandling: (payload: boolean) => TSetChecklistsHandling boolean >(ETaskActions.SetChecklistsHandling); +export type TSendTaskWorkflowLogComment = ITypedReduxAction< +ETaskActions.SendTaskWorkflowLogComment, + ISendWorkflowLogComment +>; + +export const sendTaskWorkflowLogComments: (payload: ISendWorkflowLogComment) => TSendTaskWorkflowLogComment = actionGenerator< +ETaskActions.SendTaskWorkflowLogComment, + ISendWorkflowLogComment +>(ETaskActions.SendTaskWorkflowLogComment); + +export type TUpdateTaskWorkflowLogItemPayload = IWorkflowLogItem; +export type TUpdateTaskWorkflowLogItem = ITypedReduxAction< +ETaskActions.UpdateTaskWorkflowLogItem, +TUpdateTaskWorkflowLogItemPayload +>; +export const updateTaskWorkflowLogItem: (payload: TUpdateTaskWorkflowLogItemPayload) => TUpdateTaskWorkflowLogItem = + actionGenerator( + ETaskActions.UpdateTaskWorkflowLogItem, + ); + + export type TTaskActions = | TLoadCurrentTask + | TChangeTaskWorkflow + | TChangeTaskWorkflowLog + | TSetTaskWorkflowIsLoading + | TChangeTaskWorkflowLogViewSettings | TSetCurrentTask | TPatchCurrentTask | TSetCurrentTaskDueDate @@ -180,6 +240,8 @@ export type TTaskActions = | TChangeTaskPerformers | TAddTaskGuest | TChangeChecklistItem + | TUpdateTaskWorkflowLogItem | TMarkChecklistItem + | TSendTaskWorkflowLogComment | TUnmarkChecklistItem | TSetChecklistsHandling; diff --git a/web-client/src/public/redux/task/reducer.ts b/web-client/src/public/redux/task/reducer.ts index 73b39cb..2278936 100644 --- a/web-client/src/public/redux/task/reducer.ts +++ b/web-client/src/public/redux/task/reducer.ts @@ -1,13 +1,23 @@ -/* eslint-disable */ -/* prettier-ignore */ import produce from 'immer'; import { IStoreTask } from '../../types/redux'; import { getTaskChecklist, getTaskChecklistItem } from '../../utils/tasks'; import { ETaskActions, ETaskStatus, TTaskActions } from './actions'; +import { EWorkflowsLogSorting, IWorkflowLogItem } from '../../types/workflow'; export const INIT_STATE: IStoreTask = { data: null, status: ETaskStatus.WaitingForAction, + isWorkflowLoading: false, + workflow: null, + workflowLog: { + items: [] as IWorkflowLogItem[], + isCommentsShown: true, + isOnlyAttachmentsShown: false, + sorting: EWorkflowsLogSorting.New, + isOpen: false, + isLoading: false, + workflowId: null, + }, }; export const reducer = (state = INIT_STATE, action: TTaskActions): IStoreTask => { @@ -54,6 +64,38 @@ export const reducer = (state = INIT_STATE, action: TTaskActions): IStoreTask => draftState.data.areChecklistsHandling = action.payload; }); } + case ETaskActions.ChangeTaskWorkflowLog: + return produce(state, (draftState) => { + draftState.workflowLog = { ...state.workflowLog, ...action.payload }; + }); + + case ETaskActions.ChangeTaskWorkflow: + return produce(state, (draftState) => { + draftState.workflow = action.payload; + }); + + case ETaskActions.SetTaskWorkflowIsLoading: + return produce(state, (draftState) => { + draftState.isWorkflowLoading = action.payload; + }); + + case ETaskActions.ChangeTaskWorkflowLogViewSettings: + return produce(state, (draftState) => { + draftState.workflowLog.isCommentsShown = action.payload.comments; + draftState.workflowLog.isOnlyAttachmentsShown = action.payload.isOnlyAttachmentsShown; + draftState.workflowLog.sorting = action.payload.sorting; + }); + + case ETaskActions.UpdateTaskWorkflowLogItem: + return produce(state, (draftState) => { + const index = draftState.workflowLog.items.findIndex((item) => item.id === action.payload.id); + + if (index !== -1) { + draftState.workflowLog.items[index] = action.payload; + } else { + draftState.workflowLog.items = [action.payload, ...draftState.workflowLog.items]; + } + }); default: return { ...state }; diff --git a/web-client/src/public/redux/task/saga.ts b/web-client/src/public/redux/task/saga.ts index 793c889..dfbd29f 100644 --- a/web-client/src/public/redux/task/saga.ts +++ b/web-client/src/public/redux/task/saga.ts @@ -1,6 +1,5 @@ /* eslint-disable */ /* prettier-ignore */ -// tslint:disable: max-file-line-count import { all, fork, @@ -12,6 +11,7 @@ import { ActionPattern, ActionChannelEffect, actionChannel, + takeLatest, } from 'redux-saga/effects'; import { ETaskActions, @@ -32,6 +32,9 @@ import { TSetChecklistsHandling, TSetCurrentTaskDueDate, patchCurrentTask, + changeTaskWorkflow, + changeTaskWorkflowLog, + setTaskWorkflowIsLoading, } from './actions'; import { getTask } from '../../api/getTask'; import { ITaskAPI } from '../../types/tasks'; @@ -44,9 +47,11 @@ import { revertTask } from '../../api/revertTask'; import { changeWorkflowLogViewSettings, ETaskListActions, - loadWorkflow, patchTaskInList, + setGeneralLoaderVisibility, shiftTaskList, + TChangeWorkflowLogViewSettings, + TSendWorkflowLogComment, usersFetchFinished, } from '../actions'; import { getErrorMessage } from '../../utils/getErrorMessage'; @@ -61,18 +66,22 @@ import { removeTaskPerformer } from '../../api/removeTaskPerformer'; import { addTaskGuest } from '../../api/addTaskGuest'; import { removeTaskGuest } from '../../api/removeTaskGuest'; import { TUserListItem } from '../../types/user'; -import { getWorkflowLog } from '../selectors/workflows'; +import { getTaskStore } from '../selectors/workflows'; import { EResponseStatuses } from '../../constants/defaultValues'; import { getNormalizedTask, getTaskChecklist, getTaskChecklistItem } from '../../utils/tasks'; import { markChecklistItem } from '../../api/markChecklistItem'; import { unmarkChecklistItem } from '../../api/unmarkChecklistItem'; import { deleteTaskDueDate } from '../../api/deleteTaskDueDate'; import { changeTaskDueDate } from '../../api/changeTaskDueDate'; +import { IStoreTask } from '../../types/redux'; +import { getWorkflow } from '../../api/getWorkflow'; +import { getWorkflowLog } from '../../api/getWorkflowLog'; +import { EWorkflowsLogSorting, IWorkflowDetails, IWorkflowLogItem } from '../../types/workflow'; +import { sendWorkflowComment } from '../../api/sendWorkflowComment'; +import { mapFilesToRequest } from '../../utils/workflows'; function* fetchTask({ payload: { taskId, viewMode } }: TLoadCurrentTask) { - const { sorting, isCommentsShown, isOnlyAttachmentsShown }: ReturnType = yield select( - getWorkflowLog, - ); + const { workflowLog: { sorting, isCommentsShown, isOnlyAttachmentsShown } }: IStoreTask = yield select(getTaskStore); const prevTask: ReturnType = yield select(getCurrentTask); yield put(setCurrentTask(null)); @@ -91,17 +100,16 @@ function* fetchTask({ payload: { taskId, viewMode } }: TLoadCurrentTask) { yield put(setCurrentTask(normalizedTask)); - const action = - viewMode !== ETaskCardViewMode.Guest - ? loadWorkflow(task.workflow.id) - : changeWorkflowLogViewSettings({ - id: task.workflow.id, - sorting, - comments: isCommentsShown, - isOnlyAttachmentsShown, - }); - - yield put(action); + if (viewMode !== ETaskCardViewMode.Guest) { + yield loadTaskWorkflow(task.workflow.id) + } else { + yield put(changeWorkflowLogViewSettings({ + id: task.workflow.id, + sorting, + comments: isCommentsShown, + isOnlyAttachmentsShown, + })); + } } catch (error) { yield put(setCurrentTask(prevTask)); @@ -128,6 +136,91 @@ function* fetchTask({ payload: { taskId, viewMode } }: TLoadCurrentTask) { } } +function* loadTaskWorkflow(workflowId: number) { + const { workflowLog: { isCommentsShown, sorting } }: IStoreTask = yield select(getTaskStore); + + try { + yield put(setTaskWorkflowIsLoading(true)); + + const [workflow, workflowLog]: [IWorkflowDetails, IWorkflowLogItem[]] = yield all([ + getWorkflow(workflowId), + getWorkflowLog({ + workflowId, + sorting, + comments: isCommentsShown, + }), + ]); + + yield put(changeTaskWorkflow(workflow)); + yield put(changeTaskWorkflowLog({ items: workflowLog, workflowId })); + } catch (error) { + logger.info('fetch prorcess error : ', error); + throw error; + } finally { + yield put(setTaskWorkflowIsLoading(false)); + } +} + +function* loadTaskWorkflowLog({ + payload: { id, sorting, comments, isOnlyAttachmentsShown }, +}: TChangeWorkflowLogViewSettings) { + yield put(changeTaskWorkflowLog({ isLoading: true })); + + try { + const fetchedProcessLog: IWorkflowLogItem[] = yield getWorkflowLog({ + workflowId: id, + sorting, + comments, + isOnlyAttachmentsShown, + }); + yield put(changeTaskWorkflowLog({ workflowId: id, items: fetchedProcessLog })); + } catch (error) { + logger.info('fetch process log error : ', error); + NotificationManager.error({ message: 'workflows.fetch-in-work-process-log-fail' }); + } finally { + yield put(changeTaskWorkflowLog({ isLoading: false })); + } +} + + +function* saveWorkflowLogComment({ payload: { text, attachments } }: TSendWorkflowLogComment) { + const {workflowLog: { + items, + workflowId: processId, + sorting, + }}: ReturnType = yield select(getTaskStore); + + if (!processId) { + return; + } + + const normalizedAttachments = mapFilesToRequest(attachments); + + try { + yield put(setGeneralLoaderVisibility(true)); + const newComment: IWorkflowLogItem = yield sendWorkflowComment({ + id: processId, + text, + attachments: normalizedAttachments, + }); + + const preLoadedProcessLogMap = { + [EWorkflowsLogSorting.New]: [newComment, ...items], + [EWorkflowsLogSorting.Old]: [...items, newComment], + }; + + const preLoadedProcessLog = preLoadedProcessLogMap[sorting]; + + yield put(changeTaskWorkflowLog({ items: preLoadedProcessLog })); + } catch (error) { + logger.info('send process log comment error:', error); + NotificationManager.error({ message: 'workflows.send-process-log-comment-fail' }); + yield put(changeTaskWorkflowLog({ items })); + } finally { + yield put(setGeneralLoaderVisibility(false)); + } +} + function* addTaskGuestSaga({ payload: { taskId, guestEmail, onStartUploading, onEndUploading, onError }, }: TAddTaskGuest) { @@ -413,6 +506,14 @@ export function* watchDeleteCurrentTaskDueDate() { yield takeEvery(ETaskActions.DeleteCurrentTaskDueDate, deleteCurrentTaskDueDateSaga); } +export function* watchChangeTaskWorkflowLogViewSettings() { + yield takeLatest(ETaskActions.ChangeTaskWorkflowLogViewSettings, loadTaskWorkflowLog); +} + +export function* watchSendWorkflowLogComment() { + yield takeEvery(ETaskActions.SendTaskWorkflowLogComment, saveWorkflowLogComment); +} + export function* rootSaga() { yield all([ fork(watchLoadCurrentTask), @@ -424,5 +525,7 @@ export function* rootSaga() { fork(watchUnmarkChecklistItem), fork(watchSetCurrentTaskDueDate), fork(watchDeleteCurrentTaskDueDate), + fork(watchChangeTaskWorkflowLogViewSettings), + fork(watchSendWorkflowLogComment), ]); } diff --git a/web-client/src/public/redux/tasks/reducer.ts b/web-client/src/public/redux/tasks/reducer.ts index a7ca798..6460973 100644 --- a/web-client/src/public/redux/tasks/reducer.ts +++ b/web-client/src/public/redux/tasks/reducer.ts @@ -50,6 +50,7 @@ export const INIT_STATE: IStoreTasks = { export const reducer = (state = INIT_STATE, action: TTaskListActions | TGeneralActions): IStoreTasks => { switch (action.type) { + case ETaskListActions.SetTaskListStatus: return { ...state, taskListStatus: action.payload }; diff --git a/web-client/src/public/redux/tasks/saga.ts b/web-client/src/public/redux/tasks/saga.ts index 6d1172d..8625783 100644 --- a/web-client/src/public/redux/tasks/saga.ts +++ b/web-client/src/public/redux/tasks/saga.ts @@ -1,7 +1,6 @@ -/* eslint-disable */ -/* prettier-ignore */ -/* tslint:disable:max-file-line-count */ import uniqBy from 'lodash.uniqby'; +import { EventChannel } from 'redux-saga'; + import { all, fork, @@ -61,7 +60,6 @@ import { getTemplateSteps } from '../../api/getTemplateSteps'; import { parseCookies } from '../../utils/cookie'; import { getBrowserConfigEnv } from '../../utils/getConfig'; import { mergePaths } from '../../utils/urls'; -import { EventChannel } from 'redux-saga'; import { createWebSocketChannel } from '../utils/createWebSocketChannel'; import { getTaskListWithNewTask } from './utils/getTaskListWithNewTask'; import { checkShouldInsertNewTask } from './utils/checkShouldInsertNewTask'; @@ -246,7 +244,7 @@ function* handleShiftTaskList({ payload: { currentTaskId } }: TShiftTaskList) { } export function* watchFetchTaskList() { - yield takeEvery(ETaskListActions.LoadTaskList, function* ({ payload: offset }: TLoadTaskList) { + yield takeEvery(ETaskListActions.LoadTaskList, function* loadTask({ payload: offset }: TLoadTaskList) { yield fetchTaskList(offset, ETaskListStatus.Loading); }); } diff --git a/web-client/src/public/redux/workflows/actions.ts b/web-client/src/public/redux/workflows/actions.ts index 64d8534..cf2a02d 100644 --- a/web-client/src/public/redux/workflows/actions.ts +++ b/web-client/src/public/redux/workflows/actions.ts @@ -130,6 +130,7 @@ export const editWorkflowSuccess: (payload?: void) => TEditWorkflowSuccess = act >(EWorkflowsActions.EditWorkflowSuccess); export type TEditWorkflowPayload = { + typeChange?: string; name?: string; kickoff?: IKickoff | null; isUrgent?: boolean; diff --git a/web-client/src/public/redux/workflows/reducer.ts b/web-client/src/public/redux/workflows/reducer.ts index 4db790c..4c6211c 100644 --- a/web-client/src/public/redux/workflows/reducer.ts +++ b/web-client/src/public/redux/workflows/reducer.ts @@ -71,7 +71,6 @@ export const INIT_STATE: IStoreWorkflows = { performersCounters: [], }, }, - isUrgent: false, }; export const reducer = (state = INIT_STATE, action: TWorkflowsActions | TGeneralActions): IStoreWorkflows => { @@ -148,10 +147,6 @@ export const reducer = (state = INIT_STATE, action: TWorkflowsActions | TGeneral ...state, workflowsLoadingStatus: EWorkflowsLoadingStatus.Loaded, }; - case EWorkflowsActions.EditWorkflow: - return { ...state, saving: true }; - case EWorkflowsActions.EditWorkflowSuccess: - return { ...state, saving: false }; case EWorkflowsActions.LoadFilterTemplates: return produce(state, (draftState) => { draftState.workflowsSettings.templateList.isLoading = true; diff --git a/web-client/src/public/redux/workflows/saga.ts b/web-client/src/public/redux/workflows/saga.ts index fec8805..2ec9641 100644 --- a/web-client/src/public/redux/workflows/saga.ts +++ b/web-client/src/public/redux/workflows/saga.ts @@ -28,7 +28,9 @@ import { loadWorkflowsFilterStepsSuccess, ETaskListActions, setGeneralLoaderVisibility, - loadCurrentTask, + setCurrentTask, + patchTaskInList, + updateTaskWorkflowLogItem, } from '../actions'; import { @@ -105,6 +107,7 @@ import { deleteReactionComment } from '../../api/workflows/deleteReactionComment import { createReactionComment } from '../../api/workflows/createReactionComment'; import { watchedComment } from '../../api/workflows/watchedComment'; import { envWssURL } from '../../constants/enviroment'; +import { getCurrentTask } from '../selectors/task'; function* handleLoadWorkflow({ workflowId, showLoader = true }: { workflowId: number; showLoader?: boolean }) { const { @@ -145,25 +148,6 @@ function* fetchWorkflow({ payload: id }: TLoadWorkflow) { } } -// Just to update the task when changing the subprocess -function* handleCloseWorkflowLogPopup() { - try { - if (history.location.pathname.includes('/tasks/')) { - const {workflow}: ReturnType = yield select(getWorkflowsStore); - const {data: task}: ReturnType = yield select(getTaskStore); - - if (!task || !workflow) return; - - if (task.id && task.id === workflow.ancestorTaskId) { - yield put(loadCurrentTask({taskId: task.id})) - } - } - - } catch (error) { - NotificationManager.error({ message: getErrorMessage(error) }); - } -} - function* handleOpenWorkflowLogPopup({ payload: { workflowId, shouldSetWorkflowDetailUrl, redirectTo404IfNotFound }, }: TOpenWorkflowLogPopup) { @@ -234,7 +218,7 @@ function* fetchWorkflowsList({ payload: offset = 0 }: TLoadWorkflowsList) { searchText, }); - const items = offset > 0 ? uniqBy([...workflowsList.items, ...results], 'id') : results; + const items = offset > 0 ? uniqBy([...workflowsList.items, ...results], ['id']) : results; yield put(changeWorkflowsList({ count, offset, items })); } catch (error) { logger.info('fetch workflows list error : ', error); @@ -283,26 +267,18 @@ function* saveWorkflowLogComment({ payload: { text, attachments } }: TSendWorkfl function* editWorkflowInWork({ payload }: TEditWorkflow) { const { - workflowsList: { items, count, offset }, - workflow + workflowsList: { items, count, offset } }: ReturnType = yield select(getWorkflowsStore); - const { name, kickoff, isUrgent, dueDate, workflowId } = payload; - - if (workflowId !== workflow?.id) { - return; - } - - if (name) { - yield put(setIsSavingWorkflowName(true)); - } + const { name, kickoff, isUrgent, dueDate} = payload; - if (kickoff) { - yield put(setIsSavingKickoff(true)); - } + if (name) yield put(setIsSavingWorkflowName(true)); + if (kickoff) yield put(setIsSavingKickoff(true)); yield deleteRemovedFilesFromFields(payload.kickoff?.fields); try { + yield put(setGeneralLoaderVisibility(true)); + const changedFields = { ...(typeof isUrgent !== 'undefined' && { isUrgent }), ...(typeof name !== 'undefined' && { name }), @@ -330,8 +306,14 @@ function* editWorkflowInWork({ payload }: TEditWorkflow) { kickoff: getEditKickoff(editedWorkflow.kickoff), }; + const task: ReturnType = yield select(getCurrentTask); yield put(setWorkflowEdit(newEditingProcess)); yield put(changeWorkflow(editedWorkflow)); + if (payload.workflowId === task?.workflow.id && task && typeof isUrgent !== 'undefined') { + yield put(setCurrentTask({...task, isUrgent})); + yield put(patchTaskInList({taskId: task.id, task: {...task, isUrgent}})); + } + yield put(loadWorkflowsList(0)); yield updateDetailedWorkflow(payload.workflowId); if (typeof isUrgent === 'undefined') { @@ -358,6 +340,7 @@ function* editWorkflowInWork({ payload }: TEditWorkflow) { yield put(setIsSavingWorkflowName(false)); yield put(setIsSavingKickoff(false)); yield put(editWorkflowSuccess()); + yield put(setGeneralLoaderVisibility(false)); } } @@ -638,10 +621,6 @@ export function* watchOpenWorkflowLogPopup() { yield takeEvery(EWorkflowsActions.OpenWorkflowLogPopup, handleOpenWorkflowLogPopup); } -export function* watchCloseWorkflowLogPopup() { - yield takeEvery(EWorkflowsActions.CloseWorkflowLogPopup, handleCloseWorkflowLogPopup); -} - export function* watchFetchWorkflow() { yield takeLatest(EWorkflowsActions.LoadWorkflow, fetchWorkflow); } @@ -674,6 +653,7 @@ export function* deleteCommentSaga({ payload: { id } }: TDeleteComment) { yield put(setGeneralLoaderVisibility(true)); const updateComment: IWorkflowLogItem = yield deleteComment({ id }); yield put(updateWorkflowLogItem(updateComment)); + yield put(updateTaskWorkflowLogItem(updateComment)); } catch (err) { NotificationManager.error({ message: getErrorMessage(err) }); } finally { @@ -686,6 +666,7 @@ export function* editCommentSaga({ payload: { id, text, attachments } }: TEditCo yield put(setGeneralLoaderVisibility(true)); const updateComment: IWorkflowLogItem = yield editComment({ id, text, attachments }); yield put(updateWorkflowLogItem(updateComment)); + yield put(updateTaskWorkflowLogItem(updateComment)); } catch (err) { NotificationManager.error({ message: getErrorMessage(err) }); } finally { @@ -705,10 +686,10 @@ export function* watchNewWorkflowsEvent() { const { data, }: IStoreTask = yield select(getTaskStore); - const dataWorkflow: IStoreWorkflows = yield select(getWorkflowsStore); - if (newEvent.task?.id === data?.id || dataWorkflow.workflow?.currentTask.id === newEvent.task?.id) { + if (newEvent.task?.id === data?.id) { yield put(updateWorkflowLogItem(newEvent)); + yield put(updateTaskWorkflowLogItem(newEvent)); } } } @@ -827,7 +808,6 @@ export function* rootSaga() { fork(watchCloneWorkflow), fork(watchLoadFilterSteps), fork(watchOpenWorkflowLogPopup), - fork(watchCloseWorkflowLogPopup), fork(watchUpdateCurrentPerformersCounters), fork(watchUpdateWorkflowStartersCounters), fork(watchSearchTextChanged), @@ -835,7 +815,6 @@ export function* rootSaga() { fork(watchSnoozeWorkflow), fork(watchDeleteComment), fork(watchEditComment), - fork(watchWatchedComment), fork(watchCreateReactionComment), fork(watchDeleteReactionComment), diff --git a/web-client/src/public/types/redux.ts b/web-client/src/public/types/redux.ts index 234cf91..04f8a8c 100644 --- a/web-client/src/public/types/redux.ts +++ b/web-client/src/public/types/redux.ts @@ -162,6 +162,9 @@ export interface IAccounts { export interface IStoreTask { data: ITask | null; + workflow: IWorkflowDetails | null; + isWorkflowLoading: boolean; + workflowLog: IWorkflowLog; status: ETaskStatus; } @@ -204,16 +207,14 @@ export interface IIntegrationsStore { } export interface IStoreWorkflows { - isUrgent: boolean; - saving?: boolean; - workflow: IWorkflowDetails | null; + workflowsLoadingStatus: EWorkflowsLoadingStatus; isWorkflowLoading: boolean; + workflow: IWorkflowDetails | null; + workflowLog: IWorkflowLog; workflowEdit: IWorkflowEdit; + workflowsSettings: IWorkflowsSettings; workflowsSearchText: string; - workflowLog: IWorkflowLog; workflowsList: IWorkflowsList; - workflowsSettings: IWorkflowsSettings; - workflowsLoadingStatus: EWorkflowsLoadingStatus; } export interface IStoreTasks { diff --git a/web-client/src/public/types/template.ts b/web-client/src/public/types/template.ts index c18d035..72dd68c 100644 --- a/web-client/src/public/types/template.ts +++ b/web-client/src/public/types/template.ts @@ -71,10 +71,11 @@ export type TOutputChecklistItem = { }; export interface ITemplateTaskPerformer { - id?: number; label: string; type: ETaskPerformerType; sourceId: string | null; + apiName?: string; + id?: number; } export enum ETaskPerformerType { diff --git a/web-client/src/public/utils/createId.ts b/web-client/src/public/utils/createId.ts index e72c6b7..a980114 100644 --- a/web-client/src/public/utils/createId.ts +++ b/web-client/src/public/utils/createId.ts @@ -20,6 +20,7 @@ export const createFieldSelectionApiName = () => createUniqueId('selection-xxxyx export const createСhecklistApiName = () => createUniqueId('clist-xxxyxx'); export const createСhecklistSelectionApiName = () => createUniqueId('citem-xxxyxx'); export const createConditionApiName = () => createUniqueId('condition-xxxyxx'); +export const createPerformerApiName = () => createUniqueId('raw-performer-xxxyxx'); export const createConditionRuleApiName = () => createUniqueId('rule-xxxyxx'); export const createConditionPredicateApiName = () => createUniqueId('predicate-xxxyxx'); export const createDueDateApiName = () => createUniqueId('due-date-xxxyxx'); diff --git a/web-client/src/public/utils/dueDate/createStartedTaskDueDate.ts b/web-client/src/public/utils/dueDate/createStartedTaskDueDate.ts new file mode 100644 index 0000000..52a5c49 --- /dev/null +++ b/web-client/src/public/utils/dueDate/createStartedTaskDueDate.ts @@ -0,0 +1,13 @@ +import { IDueDate } from '../../types/template'; +import { createDueDateApiName } from '../createId'; + +export function createStartedTaskDueDate(sourceId: string | null = null): IDueDate { + return { + apiName: createDueDateApiName(), + duration: null, + durationMonths: null, + rulePreposition: 'after', + ruleTarget: 'task started', + sourceId, + }; +} diff --git a/web-client/src/public/utils/dueDate/normalizeDueDateForBackend.ts b/web-client/src/public/utils/dueDate/normalizeDueDateForBackend.ts index 6269868..cffa9f5 100644 --- a/web-client/src/public/utils/dueDate/normalizeDueDateForBackend.ts +++ b/web-client/src/public/utils/dueDate/normalizeDueDateForBackend.ts @@ -2,7 +2,7 @@ import { EMPTY_DURATION } from '../../components/TemplateEdit/constants'; import { IDueDate, IDueDateAPI } from '../../types/template'; export const normalizeDueDateForBackend = (dueDate: IDueDate): IDueDateAPI | null => { - if (!dueDate.ruleTarget) { + if (!dueDate.duration && !dueDate.durationMonths) { return null; } diff --git a/web-client/src/public/utils/reactElementToText.tsx b/web-client/src/public/utils/reactElementToText.tsx index 30fbc06..04f366a 100644 --- a/web-client/src/public/utils/reactElementToText.tsx +++ b/web-client/src/public/utils/reactElementToText.tsx @@ -1,23 +1,14 @@ -import * as React from 'react'; -import { Provider } from 'react-redux'; -import ReactDOMServer from 'react-dom/server'; - -import { store } from '../redux/store'; - -export function reactElementToText(element: JSX.Element) { - if (typeof element === 'string') { - return element; - } - - const htmlMarkup = ReactDOMServer.renderToStaticMarkup({element}); - - return removeTags(htmlMarkup); -} - -function removeTags(str: string) { - if (!str) { +export function reactElementToText(elem: JSX.Element): any { + if (!elem) { return ''; } + if (typeof elem === 'string') { + return elem; + } - return str.replace(/(<([^>]+)>)/gi, ''); + const children = elem.props && elem.props.children; + if (children instanceof Array) { + return children.map(reactElementToText).join(''); + } + return reactElementToText(children); }