From 454dd85e0287e6a48764bbaa9fc1f375d5399aa9 Mon Sep 17 00:00:00 2001 From: Manas Kenge <110519001+Manas-Kenge@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:36:36 +0530 Subject: [PATCH 1/2] add assign dialog --- .../manager/api/services/workflow/base.py | 30 ++++ .../manager/api/services/workflow/workflow.py | 1 + .../src/actions/workflow.ts | 2 +- .../components/Workflow/ActionsToolbar.tsx | 12 +- .../components/Workflow/AssignWorkflow.tsx | 153 ++++++++++++++++++ .../src/types/workflow.ts | 12 ++ 6 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 frontend/packages/volto-workflow-manager/src/components/Workflow/AssignWorkflow.tsx diff --git a/backend/src/workflow/manager/api/services/workflow/base.py b/backend/src/workflow/manager/api/services/workflow/base.py index 7dd08aa..f4c5bc5 100644 --- a/backend/src/workflow/manager/api/services/workflow/base.py +++ b/backend/src/workflow/manager/api/services/workflow/base.py @@ -40,6 +40,12 @@ def portal(self): def portal_workflow(self): """Returns the portal_workflow tool.""" return getToolByName(self.context, "portal_workflow") + + @property + @memoize + def portal_types(self): + """Returns the portal_types tool.""" + return getToolByName(self.context, "portal_types") # --- Selected Object Properties (Driven by __init__) --- @@ -189,6 +195,30 @@ def assignable_types(self): key=lambda v: v['title'] ) + def get_assignable_types_for(self, workflow_id): + """ + Returns a list of content types that do not currently have the + specified workflow assigned. + """ + # Get the list of types already using this workflow + assigned_types = self.get_assigned_types_for(workflow_id) + + # Get all user-friendly content types + vocab_factory = getUtility(IVocabularyFactory, + name="plone.app.vocabularies.ReallyUserFriendlyTypes") + all_types = vocab_factory(self.context) + + assignable_types = [] + for term in all_types: + # A type is assignable if its ID is not in the assigned list + if term.value not in assigned_types: + assignable_types.append({ + "id": term.value, + "title": term.title + }) + + return sorted(assignable_types, key=lambda v: v['title']) + def get_assigned_types_for(self, workflow_id): """Returns a list of content type IDs assigned to a workflow.""" assigned = [] diff --git a/backend/src/workflow/manager/api/services/workflow/workflow.py b/backend/src/workflow/manager/api/services/workflow/workflow.py index 9bd7e30..8609a1c 100644 --- a/backend/src/workflow/manager/api/services/workflow/workflow.py +++ b/backend/src/workflow/manager/api/services/workflow/workflow.py @@ -40,6 +40,7 @@ def _serialize_workflow(workflow, base): ], "assigned_types": workflow_base.get_assigned_types_for(workflow.id), "context_data": { + "assignable_types": workflow_base.get_assignable_types_for(workflow.id), "managed_permissions": workflow_base.managed_permissions, "available_roles": list(workflow.getAvailableRoles()), "groups": workflow_base.getGroups() diff --git a/frontend/packages/volto-workflow-manager/src/actions/workflow.ts b/frontend/packages/volto-workflow-manager/src/actions/workflow.ts index 4b9ee91..18f37af 100644 --- a/frontend/packages/volto-workflow-manager/src/actions/workflow.ts +++ b/frontend/packages/volto-workflow-manager/src/actions/workflow.ts @@ -91,7 +91,7 @@ export function assignWorkflow(workflowId: string, contentType: string) { type: ASSIGN_WORKFLOW, request: { op: 'post', - path: `/@workflows/${workflowId}/@assign`, // Fixed path + path: `/@workflow-assign/${workflowId}`, data: { type_id: contentType, }, diff --git a/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx b/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx index b23131e..5702ee3 100644 --- a/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx +++ b/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx @@ -18,6 +18,7 @@ import blank from '@plone/volto/icons/blank.svg'; import CreateState from '../States/CreateState'; import CreateTransition from '../Transitions/CreateTransition'; import WorkflowValidation from './WorkflowValidation'; +import AssignWorkflow from './AssignWorkflow'; const messages = defineMessages({ validationSuccessTitle: { @@ -50,6 +51,10 @@ const ActionsToolbar = ({ workflowId }: { workflowId: string }) => { const intl = useIntl(); const [isCreateStateOpen, setCreateStateOpen] = useState(false); const [isCreateTransitionOpen, setCreateTransitionOpen] = useState(false); + const [isAssignDialogOpen, setAssignDialogOpen] = useState(false); + const workflow = useAppSelector( + (state) => state.workflow.workflow.currentWorkflow, + ); const validation = useAppSelector((state) => state.workflow.validation); const wasLoading = usePrevious(validation.loading); @@ -121,7 +126,7 @@ const ActionsToolbar = ({ workflowId }: { workflowId: string }) => { )} {validation.loading ? 'Checking...' : 'Sanity Check'} - @@ -143,6 +148,11 @@ const ActionsToolbar = ({ workflowId }: { workflowId: string }) => { isOpen={isCreateTransitionOpen} onClose={() => setCreateTransitionOpen(false)} /> + setAssignDialogOpen(false)} + /> ); }; diff --git a/frontend/packages/volto-workflow-manager/src/components/Workflow/AssignWorkflow.tsx b/frontend/packages/volto-workflow-manager/src/components/Workflow/AssignWorkflow.tsx new file mode 100644 index 0000000..d6d46c2 --- /dev/null +++ b/frontend/packages/volto-workflow-manager/src/components/Workflow/AssignWorkflow.tsx @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from 'react'; +import { + Button, + ButtonGroup, + Content, + Dialog, + DialogContainer, + Heading, + Item, + Picker, + ProgressCircle, +} from '@adobe/react-spectrum'; +import { toast } from 'react-toastify'; +import Toast from '@plone/volto/components/manage/Toast/Toast'; +import { useIntl, defineMessages } from 'react-intl'; +import { assignWorkflow, getWorkflow } from '../../actions'; +import { useAppDispatch } from '../../types'; +import type { AssignWorkflowProps } from '../../types/workflow'; + +const messages = defineMessages({ + title: { id: 'Assign Workflow', defaultMessage: 'Assign Workflow' }, + label: { + id: 'Select the content type you would like to assign this workflow to.', + defaultMessage: + 'Select the content type you would like to assign this workflow to.', + }, + placeholder: { + id: 'Select a content type...', + defaultMessage: 'Select a content type...', + }, + assign: { + id: 'Assign', + defaultMessage: 'Assign', + }, + assigning: { + id: 'Assigning...', + defaultMessage: 'Assigning...', + }, + cancel: { + id: 'Cancel', + defaultMessage: 'Cancel', + }, + success: { + id: 'Workflow Assigned', + defaultMessage: 'Workflow Assigned', + }, + successContent: { + id: 'The workflow has been assigned successfully.', + defaultMessage: 'The workflow has been assigned successfully.', + }, + error: { id: 'Assignment Failed', defaultMessage: 'Assignment Failed' }, +}); + +const AssignWorkflow: React.FC = ({ + workflow, + isOpen, + onClose, +}) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const [selectedTypeId, setSelectedTypeId] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!isOpen) { + setSelectedTypeId(null); + setIsSubmitting(false); + } + }, [isOpen]); + + const handleAssign = async () => { + if (!selectedTypeId || !workflow?.id) return; + + setIsSubmitting(true); + try { + await dispatch(assignWorkflow(workflow.id, selectedTypeId)); + toast.success( + , + ); + await dispatch(getWorkflow(workflow.id)); + onClose(); + } catch (error: any) { + toast.error( + , + ); + } finally { + setIsSubmitting(false); + } + }; + + if (!isOpen || !workflow) { + return null; + } + + const assignableTypes = workflow.context_data?.assignable_types || []; + + return ( + + + {intl.formatMessage(messages.title)} + + setSelectedTypeId(key as string)} + isDisabled={isSubmitting || assignableTypes.length === 0} + marginTop="size-200" + width="100%" + > + {(item) => {item.title}} + + + + + + + + + ); +}; + +export default AssignWorkflow; diff --git a/frontend/packages/volto-workflow-manager/src/types/workflow.ts b/frontend/packages/volto-workflow-manager/src/types/workflow.ts index e932666..06cacd7 100644 --- a/frontend/packages/volto-workflow-manager/src/types/workflow.ts +++ b/frontend/packages/volto-workflow-manager/src/types/workflow.ts @@ -32,7 +32,19 @@ export interface GroupInfo { title: string; } +export interface AssignableType { + id: string; + title: string; +} + +export interface AssignWorkflowProps { + workflow: Workflow | null | undefined; + isOpen: boolean; + onClose: () => void; +} + export interface ContextData { + assignable_types: AssignableType[]; available_roles: string[]; groups: GroupInfo[]; managed_permissions: PermissionInfo[]; From 611ecb75cee787c31ce6a5ca813d7adfc6a5eda6 Mon Sep 17 00:00:00 2001 From: Manas Kenge <110519001+Manas-Kenge@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:04:15 +0530 Subject: [PATCH 2/2] change icon --- backend/src/workflow/manager/api/services/workflow/base.py | 7 ++----- .../src/workflow/manager/api/services/workflow/workflow.py | 2 +- .../src/components/Workflow/ActionsToolbar.tsx | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/src/workflow/manager/api/services/workflow/base.py b/backend/src/workflow/manager/api/services/workflow/base.py index f4c5bc5..f3fb386 100644 --- a/backend/src/workflow/manager/api/services/workflow/base.py +++ b/backend/src/workflow/manager/api/services/workflow/base.py @@ -200,17 +200,14 @@ def get_assignable_types_for(self, workflow_id): Returns a list of content types that do not currently have the specified workflow assigned. """ - # Get the list of types already using this workflow - assigned_types = self.get_assigned_types_for(workflow_id) + assigned_types = self._get_assigned_types_for(workflow_id) - # Get all user-friendly content types vocab_factory = getUtility(IVocabularyFactory, name="plone.app.vocabularies.ReallyUserFriendlyTypes") all_types = vocab_factory(self.context) assignable_types = [] for term in all_types: - # A type is assignable if its ID is not in the assigned list if term.value not in assigned_types: assignable_types.append({ "id": term.value, @@ -219,7 +216,7 @@ def get_assignable_types_for(self, workflow_id): return sorted(assignable_types, key=lambda v: v['title']) - def get_assigned_types_for(self, workflow_id): + def _get_assigned_types_for(self, workflow_id): """Returns a list of content type IDs assigned to a workflow.""" assigned = [] for p_type, chain in self.portal_workflow.listChainOverrides(): diff --git a/backend/src/workflow/manager/api/services/workflow/workflow.py b/backend/src/workflow/manager/api/services/workflow/workflow.py index 8609a1c..976555f 100644 --- a/backend/src/workflow/manager/api/services/workflow/workflow.py +++ b/backend/src/workflow/manager/api/services/workflow/workflow.py @@ -38,7 +38,7 @@ def _serialize_workflow(workflow, base): } for t in workflow.transitions.objectValues() ], - "assigned_types": workflow_base.get_assigned_types_for(workflow.id), + "assigned_types": workflow_base._get_assigned_types_for(workflow.id), "context_data": { "assignable_types": workflow_base.get_assignable_types_for(workflow.id), "managed_permissions": workflow_base.managed_permissions, diff --git a/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx b/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx index 5702ee3..de1656e 100644 --- a/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx +++ b/frontend/packages/volto-workflow-manager/src/components/Workflow/ActionsToolbar.tsx @@ -12,7 +12,7 @@ import Toast from '@plone/volto/components/manage/Toast/Toast'; import { useIntl, defineMessages } from 'react-intl'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import add from '@plone/volto/icons/add.svg'; -import adduser from '@plone/volto/icons/add-user.svg'; +import contentexisting from '@plone/volto/icons/content-existing.svg'; import checkboxChecked from '@plone/volto/icons/checkbox-checked.svg'; import blank from '@plone/volto/icons/blank.svg'; import CreateState from '../States/CreateState'; @@ -127,7 +127,7 @@ const ActionsToolbar = ({ workflowId }: { workflowId: string }) => { {validation.loading ? 'Checking...' : 'Sanity Check'}