diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts index e291613f062..400e5f59b91 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts @@ -257,7 +257,6 @@ describe('DotFormComponent', () => { const workflowActions = spectator.query(DotWorkflowActionsComponent); expect(workflowActions).toBeTruthy(); - console.log(spectator.debugElement.nativeElement.innerHTML); const saveButton = spectator.query('.p-splitbutton-defaultbutton'); expect(saveButton).toBeTruthy(); expect(saveButton.textContent.trim()).toBe('Save'); diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts index 337976c8824..fd7fa7a6a36 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts @@ -1,84 +1,41 @@ -import { tapResponse } from '@ngrx/component-store'; -import { - patchState, - signalStore, - withComputed, - withHooks, - withMethods, - withState -} from '@ngrx/signals'; -import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { forkJoin, of, pipe } from 'rxjs'; +import { signalStore, withHooks, withState } from '@ngrx/signals'; -import { HttpErrorResponse } from '@angular/common/http'; -import { computed, inject } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; -import { MessageService, SelectItem } from 'primeng/api'; - -import { switchMap, tap } from 'rxjs/operators'; - -import { DotCMSContentlet } from '@dotcms/angular'; -import { - DotContentTypeService, - DotFireActionOptions, - DotHttpErrorManagerService, - DotMessageService, - DotRenderMode, - DotWorkflowActionsFireService, - DotWorkflowsActionsService -} from '@dotcms/data-access'; -import { - ComponentStatus, - DotCMSContentType, - DotCMSWorkflow, - DotCMSWorkflowAction, - FeaturedFlags, - WorkflowStep, - WorkflowTask -} from '@dotcms/dotcms-models'; +import { ComponentStatus } from '@dotcms/dotcms-models'; +import { withContent } from './features/content.feature'; import { withInformation } from './features/information.feature'; import { withSidebar } from './features/sidebar.feature'; import { withWorkflow } from './features/workflow.feature'; -import { DotEditContentService } from '../../../services/dot-edit-content.service'; -import { parseWorkflows, transformFormDataFn } from '../../../utils/functions.util'; -import { withDebug } from './features/debug.feature'; - export interface EditContentState { - /** ContentType full data */ - contentType: DotCMSContentType | null; - /** Contentlet full data */ - contentlet: DotCMSContentlet | null; - /** Schemas available for the content type */ - schemes: { - [key: string]: { - scheme: DotCMSWorkflow; - actions: DotCMSWorkflowAction[]; - firstStep: WorkflowStep; - }; - }; - /** Current workflow scheme id */ - currentSchemeId: string | null; - /** Actions available for the current content */ - currentContentActions: DotCMSWorkflowAction[]; - /** Current workflow step */ - currentStep: WorkflowStep | null; - /** Current workflow task */ - lastTask: WorkflowTask | null; + // /** ContentType full data */ + // contentType: DotCMSContentType | null; + // /** Contentlet full data */ + // contentlet: DotCMSContentlet | null; + // /** Schemas available for the content type */ + // schemes: { + // [key: string]: { + // scheme: DotCMSWorkflow; + // actions: DotCMSWorkflowAction[]; + // firstStep: WorkflowStep; + // }; + // }; + // /** Current workflow scheme id */ + // currentSchemeId: string | null; + // /** Actions available for the current content */ + // currentContentActions: DotCMSWorkflowAction[]; + // /** Current workflow step */ + // currentStep: WorkflowStep | null; + // /** Current workflow task */ + // lastTask: WorkflowTask | null; state: ComponentStatus; error: string | null; } const initialState: EditContentState = { - contentType: null, - contentlet: null, - schemes: {}, - currentSchemeId: null, - currentContentActions: [], - currentStep: null, - lastTask: null, state: ComponentStatus.INIT, error: null }; @@ -90,360 +47,10 @@ const initialState: EditContentState = { */ export const DotEditContentStore = signalStore( withState(initialState), - withComputed((store) => ({ - /** - * Computed property that determines if the new content editor feature is enabled. - * - * This function retrieves the content type from the store, accesses its metadata, - * and checks whether the content editor feature flag is set to true. - * - * @returns {boolean} True if the new content editor feature is enabled, false otherwise. - */ - isEnabledNewContentEditor: computed(() => { - const contentType = store.contentType(); - const metadata = contentType?.metadata; - - return metadata?.[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] === true; - }), - - /** - * A computed property that checks if the store is in a loading or saving state. - * - * @returns {boolean} True if the store's status is either LOADING or SAVING, false otherwise. - */ - isLoading: computed( - () => - store.state() === ComponentStatus.LOADING || - store.state() === ComponentStatus.SAVING - ), - - /** - * Computed property that determines if the store's status is equal to ComponentStatus.LOADED. - * - * @returns {boolean} - Returns true if the store's status is LOADED, otherwise false. - */ - isLoaded: computed(() => store.state() === ComponentStatus.LOADED), - - /** - * Computed property that determines if the store's status is equal to ComponentStatus.SAVING. - */ - isSaving: computed(() => store.state() === ComponentStatus.SAVING), - - /** - * A computed property that checks if an error exists in the store. - * - * @returns {boolean} True if there is an error in the store, false otherwise. - */ - hasError: computed(() => !!store.error()), - - /** - * Returns computed form data. - * - * @return {Object} The form data containing `contentlet` and `contentType`. - * - contentlet: The current contentlet from the store. - * - contentType: The current content type from the store. - */ - formData: computed(() => { - return { - contentlet: store.contentlet(), - contentType: store.contentType() - }; - }), - - /** - * Computed property that transforms the layout of the current content type - * into tabs and returns them. - */ - tabs: computed(() => transformFormDataFn(store.contentType())), - - /** - * Computed property that determines if the workflow selection warning should be shown. - * Shows warning when content is new AND no workflow scheme has been selected yet. - * - * @returns {boolean} True if warning should be shown, false otherwise - */ - showSelectWorkflowWarning: computed(() => { - const isNew = !store.contentlet(); - const hasNoSchemeSelected = !store.currentSchemeId(); - const hasMultipleSchemas = Object.keys(store.schemes()).length > 1; - - return isNew && hasMultipleSchemas && hasNoSchemeSelected; - }), - - /** - * Computed property that determines if workflow action buttons should be shown. - * Shows workflow buttons when: - * - Content type has only one workflow scheme OR - * - Content is existing AND has a selected workflow scheme OR - * - Content is new and content type has only one workflow scheme OR - * - Content is new and has selected a workflow scheme - * Hides workflow buttons when: - * - Content is new and has multiple schemes without selection - * - * @returns {boolean} True if workflow action buttons should be shown, false otherwise - */ - showWorkflowActions: computed(() => { - const hasOneScheme = Object.keys(store.schemes()).length === 1; - const isExisting = !!store.contentlet(); - const hasSelectedScheme = !!store.currentSchemeId(); - - if (hasOneScheme) { - return true; - } - - if (isExisting && hasSelectedScheme) { - return true; - } - - if (!isExisting && hasSelectedScheme) { - return true; - } - - return false; - }), - - /** - * Computed property that transforms the workflow schemes into dropdown options - * @returns Array of options with value (scheme id) and label (scheme name) - */ - workflowSchemeOptions: computed(() => - Object.entries(store.schemes()).map(([id, data]) => ({ - value: id, - label: data.scheme.name - })) - ), - - /** - * Computed property that determines if the content is new. - * Content is considered new when there is no contentlet in the store. - * - * @returns {boolean} True if content is new, false otherwise - */ - isNew: computed(() => !store.contentlet()), - - /** - * Computed property that retrieves the actions for the current workflow scheme. - * - * @returns {DotCMSWorkflowAction[]} The actions for the current workflow scheme. - */ - getActions: computed(() => { - const isNew = !store.contentlet(); - const currentSchemeId = store.currentSchemeId(); - const schemes = store.schemes(); - const currentContentActions = store.currentContentActions(); - - // If no scheme is selected, return empty array - if (!currentSchemeId || !schemes[currentSchemeId]) { - return []; - } - - // For existing content, use specific contentlet actions - if (!isNew && currentContentActions.length) { - return currentContentActions; - } - - // For new content, use scheme actions - return Object.values(schemes[currentSchemeId].actions).sort((a, b) => { - if (a.name === 'Save') return -1; - if (b.name === 'Save') return 1; - return a.name.localeCompare(b.name); - }); - }), - - /** - * Computed property that retrieves the first step of the current workflow scheme. - * - * @returns {WorkflowStep} The first step of the current workflow scheme. - */ - getFirstStep: computed(() => { - const schemes = store.schemes(); - const currentSchemeId = store.currentSchemeId(); - - return schemes[currentSchemeId]?.firstStep; - }) - })), - withMethods( - ( - store, - workflowActionService = inject(DotWorkflowsActionsService), - workflowActionsFireService = inject(DotWorkflowActionsFireService), - dotContentTypeService = inject(DotContentTypeService), - dotEditContentService = inject(DotEditContentService), - dotHttpErrorManagerService = inject(DotHttpErrorManagerService), - messageService = inject(MessageService), - dotMessageService = inject(DotMessageService), - - router = inject(Router) - ) => ({ - /** - * Method to initialize new content of a given type. - * New content - * - * @param {string} contentType - The type of content to initialize. - * @returns {Observable} An observable that completes when the initialization is done. - */ - initializeNewContent: rxMethod( - pipe( - switchMap((contentType) => { - patchState(store, { state: ComponentStatus.LOADING }); - - return forkJoin({ - contentType: dotContentTypeService.getContentType(contentType), - schemes: workflowActionService.getDefaultActions(contentType) - }).pipe( - tapResponse({ - next: ({ contentType, schemes }) => { - const parsedSchemes = parseWorkflows(schemes); - const schemeIds = Object.keys(parsedSchemes); - const defaultSchemeId = - schemeIds.length === 1 ? schemeIds[0] : null; - - patchState(store, { - contentType, - - schemes: parsedSchemes, - currentSchemeId: defaultSchemeId, - state: ComponentStatus.LOADED, - error: null - }); - }, - error: (error: HttpErrorResponse) => { - patchState(store, { - state: ComponentStatus.ERROR, - error: 'Error initializing content' - }); - dotHttpErrorManagerService.handle(error); - } - }) - ); - }) - ) - ), - - /** - * Initializes the existing content by loading its details and updating the state. - * Content existing - * - * @returns {Observable} An observable that emits the content ID. - */ - initializeExistingContent: rxMethod( - pipe( - switchMap((inode: string) => { - patchState(store, { state: ComponentStatus.LOADING }); - - return dotEditContentService.getContentById(inode).pipe( - switchMap((contentlet) => { - const { contentType } = contentlet; - - return forkJoin({ - contentType: dotContentTypeService.getContentType(contentType), - currentContentActions: workflowActionService.getByInode( - inode, - DotRenderMode.EDITING - ), - schemes: workflowActionService.getWorkFlowActions(contentType), - contentlet: of(contentlet) - }); - }), - tapResponse({ - next: ({ - contentType, - currentContentActions, - schemes, - contentlet - }) => { - const parsedSchemes = parseWorkflows(schemes); - patchState(store, { - contentType, - schemes: parsedSchemes, - currentContentActions, - contentlet, - state: ComponentStatus.LOADED - }); - }, - error: (error: HttpErrorResponse) => { - patchState(store, { - state: ComponentStatus.ERROR, - error: 'Error initializing content' - }); - dotHttpErrorManagerService.handle(error); - router.navigate(['/c/content']); - } - }) - ); - }) - ) - ), - - /** - * Fires a workflow action and updates the component state accordingly. - * - * This method triggers a sequence of events to fire a workflow action - * and handles the response or error. If the action is successful, - * it navigates to the content view with the updated contentlet and actions. - * In case of an error, it updates the state with an error message. - * - * @param options The options required to fire the workflow action. - */ - fireWorkflowAction: rxMethod>( - pipe( - tap(() => patchState(store, { state: ComponentStatus.SAVING })), - switchMap((options) => { - return workflowActionsFireService.fireTo(options).pipe( - tap((contentlet) => { - if (!contentlet.inode) { - router.navigate(['/c/content']); - } - }), - switchMap((contentlet) => { - return forkJoin({ - currentContentActions: workflowActionService.getByInode( - contentlet.inode, - DotRenderMode.EDITING - ), - contentlet: of(contentlet) - }); - }), - tapResponse({ - next: ({ contentlet, currentContentActions }) => { - router.navigate(['/content', contentlet.inode], { - replaceUrl: true, - queryParamsHandling: 'preserve' - }); - - patchState(store, { - contentlet, - currentContentActions, - state: ComponentStatus.LOADED, - error: null - }); - - messageService.add({ - severity: 'success', - summary: dotMessageService.get('success'), - detail: dotMessageService.get( - 'edit.content.success.workflow.message' - ) - }); - }, - error: (error: HttpErrorResponse) => { - patchState(store, { - state: ComponentStatus.LOADED, - error: 'Error firing workflow action' - }); - dotHttpErrorManagerService.handle(error); - } - }) - ); - }) - ) - ) - }) - ), + withContent(), withSidebar(), withInformation(), withWorkflow(), - withDebug(), withHooks({ onInit(store) { const activatedRoute = inject(ActivatedRoute); diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/content.feature.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/content.feature.ts new file mode 100644 index 00000000000..f4916bd6ebb --- /dev/null +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/content.feature.ts @@ -0,0 +1,241 @@ +import { tapResponse } from '@ngrx/component-store'; +import { + patchState, + signalStoreFeature, + type, + withComputed, + withMethods, + withState +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { forkJoin, of, pipe } from 'rxjs'; + +import { HttpErrorResponse } from '@angular/common/http'; +import { computed, inject } from '@angular/core'; +import { Router } from '@angular/router'; + +import { switchMap } from 'rxjs/operators'; + +import { + DotContentTypeService, + DotHttpErrorManagerService, + DotRenderMode, + DotWorkflowsActionsService +} from '@dotcms/data-access'; +import { + ComponentStatus, + DotCMSContentlet, + DotCMSContentType, + FeaturedFlags +} from '@dotcms/dotcms-models'; + +import { WorkflowState } from './workflow.feature'; + +import { DotEditContentService } from '../../../../services/dot-edit-content.service'; +import { parseWorkflows, transformFormDataFn } from '../../../../utils/functions.util'; +import { EditContentState } from '../edit-content.store'; + +export interface ContentState { + /** ContentType full data */ + contentType: DotCMSContentType | null; + /** Contentlet full data */ + contentlet: DotCMSContentlet | null; +} + +const initialState: ContentState = { + contentType: null, + contentlet: null +}; + +export function withContent() { + return signalStoreFeature( + { state: type() }, + withState(initialState), + withComputed((store) => ({ + /** + * Computed property that determines if the content is new. + * Content is considered new when there is no contentlet in the store. + * + * @returns {boolean} True if content is new, false otherwise + */ + isNew: computed(() => !store.contentlet()), + + /** + * Computed property that determines if the store's status is equal to ComponentStatus.LOADED. + * + * @returns {boolean} - Returns true if the store's status is LOADED, otherwise false. + */ + isLoaded: computed(() => store.state() === ComponentStatus.LOADED), + + /** + * A computed property that checks if an error exists in the store. + * + * @returns {boolean} True if there is an error in the store, false otherwise. + */ + hasError: computed(() => !!store.error()), + + /** + * Returns computed form data. + * + * @return {Object} The form data containing `contentlet` and `contentType`. + * - contentlet: The current contentlet from the store. + * - contentType: The current content type from the store. + */ + formData: computed(() => { + return { + contentlet: store.contentlet(), + contentType: store.contentType() + }; + }), + + /** + * Computed property that transforms the layout of the current content type + * into tabs and returns them. + */ + tabs: computed(() => transformFormDataFn(store.contentType())), + + /** + * Computed property that determines if the new content editor feature is enabled. + * + * This function retrieves the content type from the store, accesses its metadata, + * and checks whether the content editor feature flag is set to true. + * + * @returns {boolean} True if the new content editor feature is enabled, false otherwise. + */ + isEnabledNewContentEditor: computed(() => { + const contentType = store.contentType(); + const metadata = contentType?.metadata; + + return metadata?.[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] === true; + }), + + /** + * A computed property that checks if the store is in a loading or saving state. + * + * @returns {boolean} True if the store's status is either LOADING or SAVING, false otherwise. + */ + isLoading: computed( + () => + store.state() === ComponentStatus.LOADING || + store.state() === ComponentStatus.SAVING + ), + + /** + * Computed property that determines if the store's status is equal to ComponentStatus.SAVING. + */ + isSaving: computed(() => store.state() === ComponentStatus.SAVING) + })), + withMethods( + ( + store, + dotContentTypeService = inject(DotContentTypeService), + dotEditContentService = inject(DotEditContentService), + workflowActionService = inject(DotWorkflowsActionsService), + dotHttpErrorManagerService = inject(DotHttpErrorManagerService), + router = inject(Router) + ) => ({ + /** + * Method to initialize new content of a given type. + * New content + * + * @param {string} contentType - The type of content to initialize. + * @returns {Observable} An observable that completes when the initialization is done. + */ + initializeNewContent: rxMethod( + pipe( + switchMap((contentType) => { + patchState(store, { state: ComponentStatus.LOADING }); + + return forkJoin({ + contentType: dotContentTypeService.getContentType(contentType), + schemes: workflowActionService.getDefaultActions(contentType) + }).pipe( + tapResponse({ + next: ({ contentType, schemes }) => { + const parsedSchemes = parseWorkflows(schemes); + const schemeIds = Object.keys(parsedSchemes); + const defaultSchemeId = + schemeIds.length === 1 ? schemeIds[0] : null; + + patchState(store, { + contentType, + + schemes: parsedSchemes, + currentSchemeId: defaultSchemeId, + state: ComponentStatus.LOADED, + error: null + }); + }, + error: (error: HttpErrorResponse) => { + patchState(store, { + state: ComponentStatus.ERROR, + error: 'Error initializing content' + }); + dotHttpErrorManagerService.handle(error); + } + }) + ); + }) + ) + ), + + /** + * Initializes the existing content by loading its details and updating the state. + * Content existing + * + * @returns {Observable} An observable that emits the content ID. + */ + initializeExistingContent: rxMethod( + pipe( + switchMap((inode: string) => { + patchState(store, { state: ComponentStatus.LOADING }); + + return dotEditContentService.getContentById(inode).pipe( + switchMap((contentlet) => { + const { contentType } = contentlet; + + return forkJoin({ + contentType: + dotContentTypeService.getContentType(contentType), + currentContentActions: workflowActionService.getByInode( + inode, + DotRenderMode.EDITING + ), + schemes: + workflowActionService.getWorkFlowActions(contentType), + contentlet: of(contentlet) + }); + }), + tapResponse({ + next: ({ + contentType, + currentContentActions, + schemes, + contentlet + }) => { + const parsedSchemes = parseWorkflows(schemes); + patchState(store, { + contentType, + schemes: parsedSchemes, + currentContentActions, + contentlet, + state: ComponentStatus.LOADED + }); + }, + error: (error: HttpErrorResponse) => { + patchState(store, { + state: ComponentStatus.ERROR, + error: 'Error initializing content' + }); + dotHttpErrorManagerService.handle(error); + router.navigate(['/c/content']); + } + }) + ); + }) + ) + ) + }) + ) + ); +} diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/sidebar.feature.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/sidebar.feature.ts index 93f908302ba..c99cf31be55 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/sidebar.feature.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/sidebar.feature.ts @@ -10,21 +10,17 @@ import { import { computed } from '@angular/core'; -import { ComponentStatus } from '@dotcms/dotcms-models'; +import { ContentState } from './content.feature'; import { getPersistSidebarState, setPersistSidebarState } from '../../../../utils/functions.util'; import { EditContentState } from '../edit-content.store'; -interface AsideState { +interface SidebarState { showSidebar: boolean; - state: ComponentStatus; - error: string | null; } -const initialState: AsideState = { - showSidebar: false, - state: ComponentStatus.INIT, - error: null +const initialState: SidebarState = { + showSidebar: false }; /** @@ -35,7 +31,7 @@ const initialState: AsideState = { export function withSidebar() { return signalStoreFeature( { - state: type() + state: type() }, withState(initialState), withComputed(({ contentlet }) => ({ diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/workflow.feature.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/workflow.feature.ts index 997528e7ea0..4eff05ae0f7 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/workflow.feature.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/store/features/workflow.feature.ts @@ -8,29 +8,73 @@ import { withState } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { pipe } from 'rxjs'; +import { forkJoin, of, pipe } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { computed, inject } from '@angular/core'; +import { Router } from '@angular/router'; + +import { MessageService, SelectItem } from 'primeng/api'; import { switchMap, tap } from 'rxjs/operators'; -import { DotHttpErrorManagerService, DotWorkflowService } from '@dotcms/data-access'; -import { ComponentStatus, DotCMSWorkflow, WorkflowTask } from '@dotcms/dotcms-models'; +import { + DotFireActionOptions, + DotHttpErrorManagerService, + DotMessageService, + DotRenderMode, + DotWorkflowActionsFireService, + DotWorkflowsActionsService, + DotWorkflowService +} from '@dotcms/data-access'; +import { + ComponentStatus, + DotCMSWorkflow, + DotCMSWorkflowAction, + WorkflowStep, + WorkflowTask +} from '@dotcms/dotcms-models'; + +import { ContentState } from './content.feature'; import { EditContentState } from '../edit-content.store'; -interface WorkflowState { +export interface WorkflowState { + /** Schemas available for the content type */ + schemes: { + [key: string]: { + scheme: DotCMSWorkflow; + actions: DotCMSWorkflowAction[]; + firstStep: WorkflowStep; + }; + }; + + /** Current workflow scheme id */ + currentSchemeId: string | null; + + /** Actions available for the current content */ + currentContentActions: DotCMSWorkflowAction[]; + + /** Current workflow step */ + currentStep: WorkflowStep | null; + + /** Last workflow task of the contentlet */ + lastTask: WorkflowTask | null; + + // Nested workflow status workflow: { - task: WorkflowTask | null; status: ComponentStatus; error: string | null; }; } const initialState: WorkflowState = { + schemes: {}, + currentSchemeId: null, + currentContentActions: [], + currentStep: null, + lastTask: null, workflow: { - task: null, status: ComponentStatus.INIT, error: null } @@ -45,7 +89,7 @@ const initialState: WorkflowState = { export function withWorkflow() { return signalStoreFeature( { - state: type() + state: type() }, withState(initialState), withComputed((store) => ({ @@ -65,13 +109,117 @@ export function withWorkflow() { const currentSchemeId = store.currentSchemeId(); return currentSchemeId ? store.schemes()[currentSchemeId]?.scheme : undefined; + }), + + /** + * Computed property that determines if workflow action buttons should be shown. + * Shows workflow buttons when: + * - Content type has only one workflow scheme OR + * - Content is existing AND has a selected workflow scheme OR + * - Content is new and content type has only one workflow scheme OR + * - Content is new and has selected a workflow scheme + * Hides workflow buttons when: + * - Content is new and has multiple schemes without selection + * + * @returns {boolean} True if workflow action buttons should be shown, false otherwise + */ + showWorkflowActions: computed(() => { + const hasOneScheme = Object.keys(store.schemes()).length === 1; + const isExisting = !!store.contentlet(); + const hasSelectedScheme = !!store.currentSchemeId(); + + if (hasOneScheme) { + return true; + } + + if (isExisting && hasSelectedScheme) { + return true; + } + + if (!isExisting && hasSelectedScheme) { + return true; + } + + return false; + }), + + /** + * Computed property that determines if the workflow selection warning should be shown. + * Shows warning when content is new AND no workflow scheme has been selected yet. + * + * @returns {boolean} True if warning should be shown, false otherwise + */ + showSelectWorkflowWarning: computed(() => { + const isNew = !store.contentlet(); + const hasNoSchemeSelected = !store.currentSchemeId(); + const hasMultipleSchemas = Object.keys(store.schemes()).length > 1; + + return isNew && hasMultipleSchemas && hasNoSchemeSelected; + }), + + /** + * Computed property that retrieves the actions for the current workflow scheme. + * + * @returns {DotCMSWorkflowAction[]} The actions for the current workflow scheme. + */ + getActions: computed(() => { + const isNew = !store.contentlet(); + const currentSchemeId = store.currentSchemeId(); + const schemes = store.schemes(); + const currentContentActions = store.currentContentActions(); + + // If no scheme is selected, return empty array + if (!currentSchemeId || !schemes[currentSchemeId]) { + return []; + } + + // For existing content, use specific contentlet actions + if (!isNew && currentContentActions.length) { + return currentContentActions; + } + + // For new content, use scheme actions + return Object.values(schemes[currentSchemeId].actions).sort((a, b) => { + if (a.name === 'Save') return -1; + if (b.name === 'Save') return 1; + + return a.name.localeCompare(b.name); + }); + }), + + /** + * Computed property that transforms the workflow schemes into dropdown options + * @returns Array of options with value (scheme id) and label (scheme name) + */ + workflowSchemeOptions: computed(() => + Object.entries(store.schemes()).map(([id, data]) => ({ + value: id, + label: data.scheme.name + })) + ), + + /** + * Computed property that retrieves the first step of the current workflow scheme. + * + * @returns {WorkflowStep} The first step of the current workflow scheme. + */ + getFirstStep: computed(() => { + const schemes = store.schemes(); + const currentSchemeId = store.currentSchemeId(); + + return schemes[currentSchemeId]?.firstStep; }) })), withMethods( ( store, dotWorkflowService = inject(DotWorkflowService), - dotHttpErrorManagerService = inject(DotHttpErrorManagerService) + workflowActionService = inject(DotWorkflowsActionsService), + workflowActionsFireService = inject(DotWorkflowActionsFireService), + dotHttpErrorManagerService = inject(DotHttpErrorManagerService), + messageService = inject(MessageService), + dotMessageService = inject(DotMessageService), + router = inject(Router) ) => ({ /** * Get workflow status for an existing contentlet @@ -120,11 +268,82 @@ export function withWorkflow() { ) ), + /** + * Sets the selected workflow scheme ID in the store. + * + * @param {string} schemeId - The ID of the workflow scheme to be selected. + */ setSelectedWorkflow: (schemeId: string) => { patchState(store, { currentSchemeId: schemeId }); - } + }, + + /** + * Fires a workflow action and updates the component state accordingly. + * + * This method triggers a sequence of events to fire a workflow action + * and handles the response or error. If the action is successful, + * it navigates to the content view with the updated contentlet and actions. + * In case of an error, it updates the state with an error message. + * + * @param options The options required to fire the workflow action. + */ + fireWorkflowAction: rxMethod< + DotFireActionOptions<{ [key: string]: string | object }> + >( + pipe( + tap(() => patchState(store, { state: ComponentStatus.SAVING })), + switchMap((options) => { + return workflowActionsFireService.fireTo(options).pipe( + tap((contentlet) => { + if (!contentlet.inode) { + router.navigate(['/c/content']); + } + }), + switchMap((contentlet) => { + return forkJoin({ + currentContentActions: workflowActionService.getByInode( + contentlet.inode, + DotRenderMode.EDITING + ), + contentlet: of(contentlet) + }); + }), + tapResponse({ + next: ({ contentlet, currentContentActions }) => { + router.navigate(['/content', contentlet.inode], { + replaceUrl: true, + queryParamsHandling: 'preserve' + }); + + patchState(store, { + contentlet, + currentContentActions, + state: ComponentStatus.LOADED, + error: null + }); + + messageService.add({ + severity: 'success', + summary: dotMessageService.get('success'), + detail: dotMessageService.get( + 'edit.content.success.workflow.message' + ) + }); + }, + error: (error: HttpErrorResponse) => { + patchState(store, { + state: ComponentStatus.LOADED, + error: 'Error firing workflow action' + }); + dotHttpErrorManagerService.handle(error); + } + }) + ); + }) + ) + ) }) ) );