Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(edit-content): apply format to relationships #31048

Merged
merged 11 commits into from
Jan 3, 2025
30 changes: 30 additions & 0 deletions core-web/libs/dotcms-models/src/lib/dot-contentlet.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,33 @@ export interface DotContentletPermissions {
PUBLISH?: string[];
CAN_ADD_CHILDREN?: string[];
}

/**
* The depth of the contentlet.
*
* @enum {string}
* @property {string} ZERO - Without relationships
* @property {string} ONE - Retrieve the id of relationships
* @property {string} TWO - Retrieve relationships
* @property {string} THREE - Retrieve relationships with their relationships
*/
export enum DotContentletDepths {
/**
* Without relationships
*/
ZERO = '0',
/**
* Retrieve the id of relationships
*/
ONE = '1',
/**
* Retrieve relationships
*/
TWO = '2',
/**
* Retrieve relationships with their relationships
*/
THREE = '3'
}

export type DotContentletDepth = `${DotContentletDepths}`;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
DotWorkflowsActionsService,
DotWorkflowService
} from '@dotcms/data-access';
import { DotCMSWorkflowAction } from '@dotcms/dotcms-models';
import { DotCMSWorkflowAction, DotContentletDepths } from '@dotcms/dotcms-models';
import { DotWorkflowActionsComponent } from '@dotcms/ui';
import {
DotFormatDateServiceMock,
Expand Down Expand Up @@ -122,7 +122,10 @@ describe('DotFormComponent', () => {
);
dotWorkflowService.getWorkflowStatus.mockReturnValue(of(MOCK_WORKFLOW_STATUS));

store.initializeExistingContent(MOCK_CONTENTLET_1_OR_2_TABS.inode); // called with the inode of the contentlet
store.initializeExistingContent({
inode: MOCK_CONTENTLET_1_OR_2_TABS.inode,
depth: DotContentletDepths.ONE
}); // called with the inode of the contentlet

spectator.detectChanges();
});
Expand Down Expand Up @@ -200,7 +203,10 @@ describe('DotFormComponent', () => {
);
dotWorkflowService.getWorkflowStatus.mockReturnValue(of(MOCK_WORKFLOW_STATUS));

store.initializeExistingContent(MOCK_CONTENTLET_1_OR_2_TABS.inode); // called with the inode of the contentlet
store.initializeExistingContent({
inode: MOCK_CONTENTLET_1_OR_2_TABS.inode,
depth: DotContentletDepths.ONE
}); // called with the inode of the contentlet
spectator.detectChanges();
});

Expand Down Expand Up @@ -293,7 +299,10 @@ describe('DotFormComponent', () => {
);
dotWorkflowService.getWorkflowStatus.mockReturnValue(of(MOCK_WORKFLOW_STATUS));

store.initializeExistingContent(MOCK_CONTENTLET_1_OR_2_TABS.inode);
store.initializeExistingContent({
inode: MOCK_CONTENTLET_1_OR_2_TABS.inode,
depth: DotContentletDepths.ONE
});
spectator.detectChanges();
});

Expand All @@ -307,7 +316,10 @@ describe('DotFormComponent', () => {
workflowActionsService.getWorkFlowActions.mockReturnValue(
of(MOCK_SINGLE_WORKFLOW_ACTIONS) // Single workflow actions trigger the show
);
store.initializeExistingContent('inode');
store.initializeExistingContent({
inode: 'inode',
depth: DotContentletDepths.ONE
});
spectator.detectChanges();

const workflowActions = spectator.query(DotWorkflowActionsComponent);
Expand All @@ -320,7 +332,10 @@ describe('DotFormComponent', () => {
of(MOCK_MULTIPLE_WORKFLOW_ACTIONS) // Multiple workflow actions trigger the hide
);

store.initializeExistingContent('inode');
store.initializeExistingContent({
inode: 'inode',
depth: DotContentletDepths.ONE
});
spectator.detectChanges();

const workflowActions = spectator.query(DotWorkflowActionsComponent);
Expand All @@ -334,7 +349,10 @@ describe('DotFormComponent', () => {
workflowActionsService.getWorkFlowActions.mockReturnValue(
of(MOCK_SINGLE_WORKFLOW_ACTIONS)
);
store.initializeExistingContent('inode');
store.initializeExistingContent({
inode: 'inode',
depth: DotContentletDepths.ONE
});
spectator.detectChanges();

const workflowActions = spectator.query(DotWorkflowActionsComponent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { signalStore, withHooks, withState } from '@ngrx/signals';
import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { ComponentStatus } from '@dotcms/dotcms-models';
import { ComponentStatus, DotContentletDepths } from '@dotcms/dotcms-models';
import { withLocales } from '@dotcms/edit-content/feature/edit-content/store/features/locales.feature';

import { withContent } from './features/content.feature';
Expand Down Expand Up @@ -46,7 +46,7 @@ export const DotEditContentStore = signalStore(

// TODO: refactor this when we will use EditContent as sidebar
if (inode) {
store.initializeExistingContent(inode);
store.initializeExistingContent({ inode, depth: DotContentletDepths.TWO });
} else if (contentType) {
store.initializeNewContent(contentType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
DotCMSContentType,
DotCMSWorkflow,
DotCMSWorkflowAction,
DotContentletDepth,
FeaturedFlags,
WorkflowStep
} from '@dotcms/dotcms-models';
Expand Down Expand Up @@ -233,12 +234,12 @@ export function withContent() {
* @returns {Observable<string>} An observable that emits the content's inode when initialization is complete
* @throws Will redirect to /c/content and show error if initialization fails
*/
initializeExistingContent: rxMethod<string>(
initializeExistingContent: rxMethod<{ inode: string; depth: DotContentletDepth }>(
nicobytes marked this conversation as resolved.
Show resolved Hide resolved
pipe(
switchMap((inode: string) => {
switchMap(({ inode, depth }) => {
patchState(store, { state: ComponentStatus.LOADING });

return dotEditContentService.getContentById(inode).pipe(
return dotEditContentService.getContentById(inode, null, depth).pipe(
switchMap((contentlet) => {
const { contentType } = contentlet;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ export class DotEditContentRelationshipFieldComponent implements ControlValueAcc
allowSignalWrites: true
}
);

effect(() => {
if (this.onChange && this.onTouched) {
const value = this.store.formattedRelationship();
this.onChange(value);
this.onTouched();
}
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { TestBed } from '@angular/core/testing';

import { ComponentStatus } from '@dotcms/dotcms-models';

import { RelationshipFieldStore } from './relationship-field.store';

import { RelationshipFieldItem } from '../models/relationship.models';

describe('RelationshipFieldStore', () => {
let store: InstanceType<typeof RelationshipFieldStore>;

const mockData: RelationshipFieldItem[] = [
{ id: '1', title: 'Content 1', language: '1', modDate: new Date().toISOString() },
{ id: '2', title: 'Content 2', language: '1', modDate: new Date().toISOString() },
{ id: '3', title: 'Content 3', language: '1', modDate: new Date().toISOString() }
];

beforeEach(() => {
TestBed.configureTestingModule({
providers: [RelationshipFieldStore]
});

store = TestBed.inject(RelationshipFieldStore);
});

it('should be created', () => {
expect(store).toBeTruthy();
});

describe('Initial State', () => {
it('should have correct initial state', () => {
expect(store.data()).toEqual([]);
expect(store.status()).toBe(ComponentStatus.INIT);
expect(store.selectionMode()).toBeNull();
expect(store.pagination()).toEqual({
offset: 0,
currentPage: 1,
rowsPerPage: 6
});
});
});

describe('State Management', () => {
describe('setData', () => {
it('should set data correctly', () => {
store.setData(mockData);
expect(store.data()).toEqual(mockData);
});
});

describe('setCardinality', () => {
it('should set single selection mode for ONE_TO_ONE relationship', () => {
store.setCardinality(2); // ONE_TO_ONE cardinality
expect(store.selectionMode()).toBe('single');
});

it('should set multiple selection mode for other relationship types', () => {
store.setCardinality(0); // ONE_TO_MANY cardinality
expect(store.selectionMode()).toBe('multiple');
});

it('should throw error for invalid cardinality', () => {
expect(() => store.setCardinality(999)).toThrow('Invalid relationship type');
});
});

describe('addData', () => {
it('should add new unique data to existing data', () => {
const initialData = [mockData[0]];
const newData = [mockData[1], mockData[2]];

store.setData(initialData);
store.addData(newData);

expect(store.data()).toEqual([...initialData, ...newData]);
});

it('should not add duplicate data', () => {
const initialData = [mockData[0]];
const newData = [mockData[0], mockData[1]];

store.setData(initialData);
store.addData(newData);

expect(store.data()).toEqual([mockData[0], mockData[1]]);
});
});

describe('pagination', () => {
it('should handle next page correctly', () => {
store.nextPage();
expect(store.pagination()).toEqual({
offset: 6,
currentPage: 2,
rowsPerPage: 6
});
});

it('should handle previous page correctly', () => {
store.nextPage();
store.previousPage();
expect(store.pagination()).toEqual({
offset: 0,
currentPage: 1,
rowsPerPage: 6
});
});
});
});

describe('Computed Properties', () => {
describe('totalPages', () => {
it('should compute total pages correctly', () => {
store.setData(mockData);
expect(store.totalPages()).toBe(1);
});

it('should handle empty data', () => {
expect(store.totalPages()).toBe(0);
});
});

describe('isDisabledCreateNewContent', () => {
it('should disable for single mode with one item', () => {
store.setCardinality(2); // ONE_TO_ONE
store.setData([mockData[0]]);
expect(store.isDisabledCreateNewContent()).toBe(true);
});

it('should not disable for single mode with no items', () => {
store.setCardinality(2); // ONE_TO_ONE
expect(store.isDisabledCreateNewContent()).toBe(false);
});

it('should not disable for multiple mode regardless of items', () => {
store.setCardinality(0); // ONE_TO_MANY
store.setData(mockData);
expect(store.isDisabledCreateNewContent()).toBe(false);
});
});

describe('formattedRelationship', () => {
it('should format relationship IDs correctly', () => {
store.setData(mockData);
expect(store.formattedRelationship()).toBe('1,2,3');
});

it('should handle empty data', () => {
expect(store.formattedRelationship()).toBe('');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@ import {
patchState,
signalStore,
withComputed,
withHooks,
withMethods,
withState
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe } from 'rxjs';

import { computed } from '@angular/core';

import { tap } from 'rxjs/operators';

import { ComponentStatus } from '@dotcms/dotcms-models';

import { RELATIONSHIP_OPTIONS } from '../dot-edit-content-relationship-field.constants';
Expand Down Expand Up @@ -52,7 +47,15 @@ export const RelationshipFieldStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed((state) => ({
/**
* Computes the total number of pages based on the number of items and the rows per page.
* @returns {number} The total number of pages.
*/
totalPages: computed(() => Math.ceil(state.data().length / state.pagination().rowsPerPage)),
/**
* Checks if the create new content button is disabled based on the selection mode and the number of items.
* @returns {boolean} True if the button is disabled, false otherwise.
*/
isDisabledCreateNewContent: computed(() => {
const totalItems = state.data().length;
const selectionMode = state.selectionMode();
Expand All @@ -62,6 +65,15 @@ export const RelationshipFieldStore = signalStore(
}

return false;
}),
/**
* Formats the relationship field data into a string of IDs.
* @returns {string} A string of IDs separated by commas.
*/
formattedRelationship: computed(() => {
const data = state.data();

return data.map((item) => item.id).join(',');
})
})),
withMethods((store) => {
Expand Down Expand Up @@ -115,13 +127,6 @@ export const RelationshipFieldStore = signalStore(
data: store.data().filter((item) => item.id !== id)
});
},
/**
* Loads the data for the relationship field by fetching content from the service.
* It updates the state with the loaded data and sets the status to LOADED.
*/
loadData: rxMethod<void>(
pipe(tap(() => patchState(store, { status: ComponentStatus.LOADED })))
),
/**
* Advances the pagination to the next page and updates the state accordingly.
*/
Expand All @@ -148,9 +153,4 @@ export const RelationshipFieldStore = signalStore(
}
};
}),
withHooks({
onInit: (store) => {
store.loadData();
}
})
);
Loading
Loading