Skip to content

Commit

Permalink
Add Reset Workflow Button in Edit Contentlet Sidebar Workflow Section (
Browse files Browse the repository at this point in the history
…#30767)

### Proposed Changes
* Allow resetting a contentlet if the sub-action for that.
This pull request includes several changes to the `core-web` library,
focusing on enhancing the `dot-edit-content-form` and
`dot-edit-content-sidebar-workflow` components. The most important
changes include the addition of new render modes, modifications to form
value handling, and updates to the workflow component structure and
functionality.

### Enhancements to `dot-edit-content-form`:

* Added new render modes to the `DotRenderMode` enum in
`dot-workflows-actions.service.ts`.
* Updated the `changeValue` output type to handle various data types,
including `string[]` and `Date`.
* Introduced a new computed property `$formFields` to filter out certain
fields from the form.
* Refactored the `initializeFormListener` method to handle form value
changes and emit the processed value.
* Simplified the `initializeForm` method to use the new `$formFields`
computed property.

### Updates to `dot-edit-content-sidebar-workflow`:

* Consolidated workflow-related variables into a single `workflow`
object and updated the template to use this object.
[[1]](diffhunk://#diff-1e29fca586a864571ecaaf1794485fbb1b5d6a39bac17bcdcb67e44fb8f8e451L1-R3)
[[2]](diffhunk://#diff-1e29fca586a864571ecaaf1794485fbb1b5d6a39bac17bcdcb67e44fb8f8e451L16-R13)
[[3]](diffhunk://#diff-1e29fca586a864571ecaaf1794485fbb1b5d6a39bac17bcdcb67e44fb8f8e451L26-R58)
[[4]](diffhunk://#diff-1e29fca586a864571ecaaf1794485fbb1b5d6a39bac17bcdcb67e44fb8f8e451L61-R67)
[[5]](diffhunk://#diff-1e29fca586a864571ecaaf1794485fbb1b5d6a39bac17bcdcb67e44fb8f8e451L84-R90)
* Introduced new interfaces `WorkflowSelection` and `DotWorkflowState`
to manage workflow selection and state.
[[1]](diffhunk://#diff-3465f2dbecb29cf855c8f271b85d396eff6ac9c64b1c1440147cdca68392b1d0L10-R27)
[[2]](diffhunk://#diff-3465f2dbecb29cf855c8f271b85d396eff6ac9c64b1c1440147cdca68392b1d0L36-R54)
* Added an output event `onResetWorkflow` to handle workflow resets.
* Updated the workflow component to use the new `workflowSelection` and
`resetWorkflowAction` inputs.

### General improvements:

* Removed the unused `SlicePipe` import from
`dot-edit-content-sidebar.component.ts`.
[[1]](diffhunk://#diff-7de680fdc62a1bf1fd1243d8fcf077fee3a7e08ed213d2821b2eb53b2d9248feL1)
[[2]](diffhunk://#diff-7de680fdc62a1bf1fd1243d8fcf077fee3a7e08ed213d2821b2eb53b2d9248feL44-R43)
* Updated the `dot-edit-content-sidebar.component.html` to use the new
workflow structure and properties.
[[1]](diffhunk://#diff-96315ade336a88f5f2f51d7a64dec588aa9fa44384539792c69e1a3346eaa5d0L3-L13)
[[2]](diffhunk://#diff-96315ade336a88f5f2f51d7a64dec588aa9fa44384539792c69e1a3346eaa5d0L26-R20)

These changes enhance the flexibility and maintainability of the form
and workflow components, ensuring better data handling and user
experience.


### Checklist
- [x] Tests
- [ ] Translations
- [ ] Security Implications Contemplated (add notes if applicable)

### Additional Info
** any additional useful context or info **

### Screenshots
Contenttype with 2 Workflows (1 resettable and 1 not)


https://github.com/user-attachments/assets/e1da7a1e-886c-4270-a98a-4d3ce062a47c

Contenttype with 1 resettable Workflow


https://github.com/user-attachments/assets/7d0be90b-ba6a-4dad-ba2c-75abb3758d0b
  • Loading branch information
oidacra authored Dec 4, 2024
1 parent a20b5e4 commit cc77a5c
Show file tree
Hide file tree
Showing 23 changed files with 1,070 additions and 559 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
} from '@dotcms/dotcms-models';

export enum DotRenderMode {
LOCKED = 'LOCKED',
LISTING = 'LISTING',
ARCHIVED = 'ARCHIVED',
UNPUBLISHED = 'UNPUBLISHED',
PUBLISHED = 'PUBLISHED',
UNLOCKED = 'UNLOCKED',
NEW = 'NEW',
EDITING = 'EDITING'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import {
MOCK_CONTENTLET_1_TAB as MOCK_CONTENTLET_1_OR_2_TABS,
MOCK_CONTENTTYPE_1_TAB,
MOCK_CONTENTTYPE_2_TABS,
MOCK_WORKFLOW_ACTIONS_NEW_ITEMNTTYPE_1_TAB
MOCK_WORKFLOW_ACTIONS_NEW_ITEMNTTYPE_1_TAB,
MOCK_WORKFLOW_STATUS
} from '../../utils/edit-content.mock';
import { MockResizeObserver } from '../../utils/mocks';

Expand All @@ -52,6 +53,7 @@ describe('DotFormComponent', () => {
let workflowActionsService: SpyObject<DotWorkflowsActionsService>;
let workflowActionsFireService: SpyObject<DotWorkflowActionsFireService>;
let dotEditContentService: SpyObject<DotEditContentService>;
let dotWorkflowService: SpyObject<DotWorkflowService>;
let router: SpyObject<Router>;

const createComponent = createComponentFactory({
Expand All @@ -71,6 +73,7 @@ describe('DotFormComponent', () => {
mockProvider(DotWorkflowService),
mockProvider(MessageService),
mockProvider(DotContentletService),

{
provide: ActivatedRoute,
useValue: {
Expand All @@ -95,6 +98,7 @@ describe('DotFormComponent', () => {
workflowActionsService = spectator.inject(DotWorkflowsActionsService);
dotEditContentService = spectator.inject(DotEditContentService);
workflowActionsFireService = spectator.inject(DotWorkflowActionsFireService);
dotWorkflowService = spectator.inject(DotWorkflowService);
router = spectator.inject(Router);
});

Expand All @@ -112,6 +116,7 @@ describe('DotFormComponent', () => {
workflowActionsService.getWorkFlowActions.mockReturnValue(
of(MOCK_SINGLE_WORKFLOW_ACTIONS)
);
dotWorkflowService.getWorkflowStatus.mockReturnValue(of(MOCK_WORKFLOW_STATUS));

store.initializeExistingContent(MOCK_CONTENTLET_1_OR_2_TABS.inode); // called with the inode of the contentlet

Expand Down Expand Up @@ -189,6 +194,7 @@ describe('DotFormComponent', () => {
workflowActionsService.getWorkFlowActions.mockReturnValue(
of(MOCK_SINGLE_WORKFLOW_ACTIONS)
);
dotWorkflowService.getWorkflowStatus.mockReturnValue(of(MOCK_WORKFLOW_STATUS));

store.initializeExistingContent(MOCK_CONTENTLET_1_OR_2_TABS.inode); // called with the inode of the contentlet
spectator.detectChanges();
Expand Down Expand Up @@ -279,6 +285,7 @@ describe('DotFormComponent', () => {
workflowActionsService.getWorkFlowActions.mockReturnValue(
of(MOCK_SINGLE_WORKFLOW_ACTIONS)
);
dotWorkflowService.getWorkflowStatus.mockReturnValue(of(MOCK_WORKFLOW_STATUS));

store.initializeExistingContent(MOCK_CONTENTLET_1_OR_2_TABS.inode);
spectator.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
FLATTENED_FIELD_TYPES
} from '../../models/dot-edit-content-field.constant';
import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum';
import { FormValues } from '../../models/dot-edit-content-form.interface';
import { DotWorkflowActionParams } from '../../models/dot-edit-content.model';
import { getFinalCastedValue, isFilteredType } from '../../utils/functions.util';
import { DotEditContentFieldComponent } from '../dot-edit-content-field/dot-edit-content-field.component';
Expand Down Expand Up @@ -98,7 +99,7 @@ export class DotEditContentFormComponent implements OnInit {
*
* @memberof DotEditContentFormComponent
*/
changeValue = output<Record<string, string>>();
changeValue = output<FormValues>();

/**
* Computed property that retrieves the filtered fields from the store.
Expand All @@ -110,6 +111,10 @@ export class DotEditContentFormComponent implements OnInit {
() => this.$store.contentType()?.fields?.filter(isFilteredType) ?? []
);

$formFields = computed(
() => this.$store.contentType()?.fields?.filter((field) => !isFilteredType(field)) ?? []
);

/**
* FormGroup instance that contains the form controls for the fields in the content type
*
Expand Down Expand Up @@ -158,72 +163,62 @@ export class DotEditContentFormComponent implements OnInit {
}

/**
* Initializes a listener for form value changes.
* When the form value changes, it calls the onFormChange method with the new value.
* The listener is automatically unsubscribed when the component is destroyed.
* Handles form value changes and emits the processed value.
*
* @private
* @param {Record<string, any>} value The raw form value
* @memberof DotEditContentFormComponent
*/
private initializeFormListener() {
this.form.valueChanges.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((value) => {
const processedValue = this.processFormValue(value);
this.changeValue.emit(processedValue);
});
onFormChange(value: Record<string, string>) {
const processedValue = this.processFormValue(value);
this.changeValue.emit(processedValue);
}

/**
* Emits the form value through the `formSubmit` event.
* Initializes a listener for form value changes.
*
* @param {*} value
* @private
* @memberof DotEditContentFormComponent
*/
onFormChange(value) {
this.$filteredFields().forEach(({ variable, fieldType }) => {
if (FLATTENED_FIELD_TYPES.includes(fieldType as FIELD_TYPES)) {
value[variable] = value[variable]?.join(',');
}

if (CALENDAR_FIELD_TYPES.includes(fieldType as FIELD_TYPES)) {
value[variable] = value[variable]
?.toISOString()
.replace(/T|\.\d{3}Z/g, (match: string) => (match === 'T' ? ' ' : '')); // To remove the T and .000Z from the date)
}
private initializeFormListener() {
this.form.valueChanges.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((value) => {
this.onFormChange(value);
});

this.changeValue.emit(value);
}

/**
* Processes the form value, applying specific transformations for certain field types.
*
* @private
* @param {Record<string, any>} value The raw form value
* @param {Record<string, string>} value The raw form value
* @returns {Record<string, string>} The processed form value
* @memberof DotEditContentFormComponent
*/
private processFormValue(
value: Record<string, string | string[] | Date | null | undefined>
): Record<string, string> {
): FormValues {
return Object.fromEntries(
this.$filteredFields().map(({ variable, fieldType }) => {
let fieldValue = value[variable];
Object.entries(value).map(([key, fieldValue]) => {
const field = this.$formFields().find((f) => f.variable === key);

if (!field) {
return [key, fieldValue];
}

if (
Array.isArray(fieldValue) &&
FLATTENED_FIELD_TYPES.includes(fieldType as FIELD_TYPES)
FLATTENED_FIELD_TYPES.includes(field.fieldType as FIELD_TYPES)
) {
fieldValue = fieldValue.join(',');
} else if (
fieldValue instanceof Date &&
CALENDAR_FIELD_TYPES.includes(fieldType as FIELD_TYPES)
CALENDAR_FIELD_TYPES.includes(field.fieldType as FIELD_TYPES)
) {
fieldValue = fieldValue
.toISOString()
.replace(/T|\.\d{3}Z/g, (match) => (match === 'T' ? ' ' : ''));
}

return [variable, fieldValue?.toString() ?? ''];
return [key, fieldValue ?? ''];
})
);
}
Expand All @@ -236,13 +231,15 @@ export class DotEditContentFormComponent implements OnInit {
* @memberof DotEditContentFormComponent
*/
private initializeForm() {
this.form = this.#fb.group({});
this.$store.contentType().fields.forEach((field) => {
if (!isFilteredType(field)) {
const control = this.createFormControl(field);
this.form.addControl(field.variable, control);
}
});
const controls = this.$formFields().reduce(
(acc, field) => ({
...acc,
[field.variable]: this.createFormControl(field)
}),
{}
);

this.form = this.#fb.group(controls);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
@let scheme = $workflow().scheme;
@let task = $workflow().task;

@let workflow = $workflow();
@let isLoading = $isLoading();
@let noWorkflowSelectedYet = $noWorkflowSelectedYet();
@let currentStep = $workflow().step;
@let workflowSelection = $workflowSelection();

<dl class="workflow-list">
<dt class="workflow-column__title" id="workflow-title">{{ 'Workflow' | dm }}</dt>
Expand All @@ -13,7 +10,7 @@
data-testId="workflow-name">
@if (isLoading) {
<p-skeleton />
} @else if (noWorkflowSelectedYet) {
} @else if (workflowSelection.isWorkflowSelected) {
<a
href="#"
class="select-workflow-link"
Expand All @@ -23,33 +20,44 @@
{{ 'edit.content.sidebar.workflow.select.workflow' | dm }}
</a>
} @else {
{{ scheme?.name }}
{{ workflow.scheme?.name }}

<!-- While not step, is consider as New contentlet -->
@if ($showWorkflowDialogIcon()) {
@if ($showWorkflowSelection()) {
<p-button
data-testId="edit-workflow-button"
(click)="showDialog()"
pButton
styleClass="p-button-link"
icon="pi pi-pencil"></p-button>
}

@if (workflow.resetAction) {
<p-button
(click)="onResetWorkflow.emit(workflow.resetAction.id.toString())"
pButton
styleClass="p-button-link"
data-testId="reset-workflow-button"
icon="pi pi-replay"></p-button>
}
}
</dd>

@if (!noWorkflowSelectedYet) {
<dt class="workflow-column__title" id="step-title">{{ 'Step' | dm }}</dt>
@if (!workflowSelection.isWorkflowSelected) {
<dt class="workflow-column__title" id="step-title">
{{ 'Step' | dm }}
</dt>
<dd
class="workflow-column__description"
aria-labelledby="step-title"
data-testId="workflow-step">
@if (isLoading) {
<p-skeleton />
} @else {
{{ currentStep?.name }}
{{ workflow.step?.name }}
}
</dd>

@if (task) {
@if (workflow.task) {
<dt class="workflow-column__title" id="assignee-title">{{ 'Assignee' | dm }}</dt>
<dd
class="workflow-column__description"
Expand All @@ -58,7 +66,7 @@
@if (isLoading) {
<p-skeleton />
} @else {
{{ task.assignedTo }}
{{ workflow.task.assignedTo }}
}
</dd>
}
Expand All @@ -81,7 +89,7 @@
<p-dropdown
id="workflow"
appendTo="body"
[options]="$workflowSchemeOptions()"
[options]="workflowSelection.schemeOptions"
optionLabel="label"
[(ngModel)]="$selectedWorkflow"
placeholder="{{
Expand Down
Loading

0 comments on commit cc77a5c

Please sign in to comment.