diff --git a/backend/src/workflow/manager/api/services/workflow/base.py b/backend/src/workflow/manager/api/services/workflow/base.py index 7dd08aa..f3fb386 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,7 +195,28 @@ def assignable_types(self): key=lambda v: v['title'] ) - def get_assigned_types_for(self, workflow_id): + def get_assignable_types_for(self, workflow_id): + """ + Returns a list of content types that do not currently have the + specified workflow assigned. + """ + assigned_types = self._get_assigned_types_for(workflow_id) + + vocab_factory = getUtility(IVocabularyFactory, + name="plone.app.vocabularies.ReallyUserFriendlyTypes") + all_types = vocab_factory(self.context) + + assignable_types = [] + for term in all_types: + 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 = [] 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 9bd7e30..976555f 100644 --- a/backend/src/workflow/manager/api/services/workflow/workflow.py +++ b/backend/src/workflow/manager/api/services/workflow/workflow.py @@ -38,8 +38,9 @@ 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, "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..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,12 +12,13 @@ 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'; 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,8 +126,8 @@ 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[];