From f325a9481c89a5fc8a1878598510d163cec25c14 Mon Sep 17 00:00:00 2001 From: Rafael Velazco Date: Wed, 28 Aug 2024 10:30:48 -0400 Subject: [PATCH 1/5] feat(SDK): Enable Inline editing in text fields for React SDK (#29620) ### Proposed changes - Develop an inline editing component for our React SDK ### Video https://github.com/user-attachments/assets/c045e382-e4ab-474f-bf52-5b4dd6c73553 --- core-web/libs/sdk/angular/package.json | 9 +- .../dot-editable-text.component.ts | 12 +- .../lib/components/dot-editable-text/utils.ts | 6 +- core-web/libs/sdk/client/package.json | 2 +- core-web/libs/sdk/experiments/package.json | 4 +- core-web/libs/sdk/react/package.json | 7 +- core-web/libs/sdk/react/src/index.ts | 1 + .../DotEditableText/DotEditableText.spec.tsx | 232 ++++++++++++++++++ .../DotEditableText/DotEditableText.tsx | 141 +++++++++++ .../lib/components/DotEditableText/utils.ts | 82 +++++++ .../react/src/lib/mocks/mockPageContext.tsx | 27 +- .../libs/sdk/react/src/lib/models/index.ts | 41 ++++ core-web/package.json | 1 + core-web/yarn.lock | 15 +- examples/nextjs/jsconfig.json | 2 +- examples/nextjs/package.json | 1 + .../src/components/content-types/banner.js | 12 +- examples/nextjs/src/components/my-page.js | 1 + 18 files changed, 574 insertions(+), 22 deletions(-) create mode 100644 core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx create mode 100644 core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx create mode 100644 core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts diff --git a/core-web/libs/sdk/angular/package.json b/core-web/libs/sdk/angular/package.json index 477fcedcdf0d..e8d0e48b69ab 100644 --- a/core-web/libs/sdk/angular/package.json +++ b/core-web/libs/sdk/angular/package.json @@ -1,14 +1,17 @@ { "name": "@dotcms/angular", - "version": "0.0.1-alpha.32", + "version": "0.0.1-alpha.33", + "dependencies": { + "@tinymce/tinymce-angular": "^8.0.0" + }, "peerDependencies": { "@angular/common": "^17.1.0", "@angular/core": "^17.1.0", "@angular/router": "^17.1.0", - "@dotcms/client": "0.0.1-alpha.32", - "@tinymce/tinymce-angular": "^8.0.0", + "@dotcms/client": "0.0.1-alpha.33", "rxjs": "^7.8.0" }, + "allowedNonPeerDependencies": ["@tinymce/tinymce-angular"], "description": "Official Angular Components library to render a dotCMS page.", "repository": { "type": "git", diff --git a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts index 67bdc3d2db94..0e50404fb0f2 100644 --- a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts +++ b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts @@ -22,7 +22,7 @@ import { postMessageToEditor } from '@dotcms/client'; -import { TINYMCE_CONFIG, TINYMCE_FORMAT, TINYMCE_MODE } from './utils'; +import { TINYMCE_CONFIG, DOT_EDITABLE_TEXT_FORMAT, DOT_EDITABLE_TEXT_MODE } from './utils'; import { DotCMSContentlet } from '../../models'; @@ -56,17 +56,17 @@ export class DotEditableTextComponent implements OnInit, OnChanges { /** * Represents the mode of the editor which can be `plain`, `minimal`, or `full` * - * @type {TINYMCE_MODE} + * @type {DOT_EDITABLE_TEXT_MODE} * @memberof DotEditableTextComponent */ - @Input() mode: TINYMCE_MODE = 'plain'; + @Input() mode: DOT_EDITABLE_TEXT_MODE = 'plain'; /** * Represents the format of the editor which can be `text` or `html` * - * @type {TINYMCE_FORMAT} + * @type {DOT_EDITABLE_TEXT_FORMAT} * @memberof DotEditableTextComponent */ - @Input() format: TINYMCE_FORMAT = 'text'; + @Input() format: DOT_EDITABLE_TEXT_FORMAT = 'text'; /** * Represents the `contentlet` that can be inline edited * @@ -126,7 +126,7 @@ export class DotEditableTextComponent implements OnInit, OnChanges { * @memberof DotEditableTextComponent */ get onNumberOfPages() { - return this.contentlet['onNumberOfPages']; + return this.contentlet['onNumberOfPages'] || 1; } /** diff --git a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts index c090da24553e..bff68d2c9ad4 100644 --- a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts +++ b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts @@ -1,8 +1,8 @@ import { EditorComponent } from '@tinymce/tinymce-angular'; -export type TINYMCE_MODE = 'minimal' | 'full' | 'plain'; +export type DOT_EDITABLE_TEXT_MODE = 'minimal' | 'full' | 'plain'; -export type TINYMCE_FORMAT = 'html' | 'text'; +export type DOT_EDITABLE_TEXT_FORMAT = 'html' | 'text'; const DEFAULT_TINYMCE_CONFIG: EditorComponent['init'] = { menubar: false, @@ -17,7 +17,7 @@ const DEFAULT_TINYMCE_CONFIG: EditorComponent['init'] = { }; export const TINYMCE_CONFIG: { - [key in TINYMCE_MODE]: EditorComponent['init']; + [key in DOT_EDITABLE_TEXT_MODE]: EditorComponent['init']; } = { minimal: { ...DEFAULT_TINYMCE_CONFIG, diff --git a/core-web/libs/sdk/client/package.json b/core-web/libs/sdk/client/package.json index 3c049aa9e858..b3c43998a5f5 100644 --- a/core-web/libs/sdk/client/package.json +++ b/core-web/libs/sdk/client/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/client", - "version": "0.0.1-alpha.32", + "version": "0.0.1-alpha.33", "description": "Official JavaScript library for interacting with DotCMS REST APIs.", "repository": { "type": "git", diff --git a/core-web/libs/sdk/experiments/package.json b/core-web/libs/sdk/experiments/package.json index 2eed7e32c417..aaf274d22c49 100644 --- a/core-web/libs/sdk/experiments/package.json +++ b/core-web/libs/sdk/experiments/package.json @@ -1,6 +1,6 @@ { "name": "@dotcms/experiments", - "version": "0.0.1-alpha.32", + "version": "0.0.1-alpha.33", "description": "Official JavaScript library to use Experiments with DotCMS.", "repository": { "type": "git", @@ -25,6 +25,6 @@ "peerDependencies": { "react": ">=18", "react-dom": ">=18", - "@dotcms/client": "0.0.1-alpha.32" + "@dotcms/client": "0.0.1-alpha.33" } } diff --git a/core-web/libs/sdk/react/package.json b/core-web/libs/sdk/react/package.json index 598add55b570..1382b0b55f24 100644 --- a/core-web/libs/sdk/react/package.json +++ b/core-web/libs/sdk/react/package.json @@ -1,10 +1,13 @@ { "name": "@dotcms/react", - "version": "0.0.1-alpha.32", + "version": "0.0.1-alpha.33", + "dependencies": { + "@tinymce/tinymce-react": "^5.1.1" + }, "peerDependencies": { "react": ">=18", "react-dom": ">=18", - "@dotcms/client": "0.0.1-alpha.32" + "@dotcms/client": "0.0.1-alpha.33" }, "description": "Official React Components library to render a dotCMS page.", "repository": { diff --git a/core-web/libs/sdk/react/src/index.ts b/core-web/libs/sdk/react/src/index.ts index 0e77739be956..53c09f8a1ede 100644 --- a/core-web/libs/sdk/react/src/index.ts +++ b/core-web/libs/sdk/react/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/components/DotcmsLayout/DotcmsLayout'; +export * from './lib/components/DotEditableText/DotEditableText'; export * from './lib/components/PageProvider/PageProvider'; export * from './lib/components/Row/Row'; export * from './lib/hooks/useDotcmsPageContext'; diff --git a/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx new file mode 100644 index 000000000000..0aaa79b19335 --- /dev/null +++ b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx @@ -0,0 +1,232 @@ +import { expect } from '@jest/globals'; +import { fireEvent, render, screen } from '@testing-library/react'; +import * as tinymceReact from '@tinymce/tinymce-react'; + +import * as dotcmsClient from '@dotcms/client'; + +import { DotEditableText } from './DotEditableText'; + +import { dotcmsContentletMock } from '../../mocks/mockPageContext'; + +const { CUSTOMER_ACTIONS, postMessageToEditor } = dotcmsClient; + +// Define mockEditor before using it in jest.mock +const TINYMCE_EDITOR_MOCK = { + focus: () => { + /* */ + }, + getContent: (_data: string) => '', + isDirty: () => false, + hasFocus: () => false, + setContent: () => { + /* */ + } +}; + +jest.mock('@tinymce/tinymce-react', () => ({ + Editor: jest.fn(({ onInit, onMouseDown, onFocusOut }) => { + onInit({}, TINYMCE_EDITOR_MOCK); + + return
; + }) +})); + +// Mock @dotcms/client module +jest.mock('@dotcms/client', () => ({ + ...jest.requireActual('@dotcms/client'), + isInsideEditor: jest.fn().mockImplementation(() => true), + postMessageToEditor: jest.fn(), + DotCmsClient: { + dotcmsUrl: 'http://localhost:8080' + } +})); + +const mockedDotcmsClient = dotcmsClient as jest.Mocked; +const { Editor } = tinymceReact as jest.Mocked; + +describe('DotEditableText', () => { + describe('Outside editor', () => { + beforeEach(() => { + mockedDotcmsClient.isInsideEditor.mockReturnValue(false); + render(); + }); + + it('should render the content', () => { + const editor = screen.queryByTestId('tinymce-editor'); + expect(editor).toBeNull(); + expect(screen.getByText(dotcmsContentletMock['title'])).not.toBeNull(); + }); + }); + + describe('Inside editor', () => { + let rerenderFn: (ui: React.ReactNode) => void; + + beforeEach(() => { + mockedDotcmsClient.isInsideEditor.mockReturnValue(true); + const { rerender } = render( + + ); + rerenderFn = rerender; + }); + + it('should pass the correct props to the Editor component', () => { + const editor = screen.getByTestId('tinymce-editor'); + expect(editor).not.toBeNull(); + + expect(Editor).toHaveBeenCalledWith( + { + tinymceScriptSrc: 'http://localhost:8080/ext/tinymcev7/tinymce.min.js', + inline: true, + init: { + inline: true, + menubar: false, + plugins: '', + powerpaste_html_import: 'clean', + powerpaste_word_import: 'clean', + suffix: '.min', + toolbar: '', + valid_styles: { + '*': 'font-size,font-family,color,text-decoration,text-align' + } + }, + initialValue: dotcmsContentletMock.title, + onMouseDown: expect.any(Function), + onFocusOut: expect.any(Function), + onInit: expect.any(Function) + }, + {} + ); + }); + + describe('DotEditableText events', () => { + let focusSpy: jest.SpyInstance; + + describe('Window Message', () => { + beforeEach(() => { + focusSpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'focus'); + }); + + it("should focus on the editor when the message is 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS'", () => { + window.dispatchEvent( + new MessageEvent('message', { + data: { + name: 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS', + payload: { + oldInode: dotcmsContentletMock['inode'], + inode: '456' + } + } + }) + ); + expect(focusSpy).toHaveBeenCalled(); + }); + + it("should not focus on the editor when the message is not 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS'", () => { + window.dispatchEvent( + new MessageEvent('message', { + data: { name: 'ANOTHER_EVENT' } + }) + ); + expect(focusSpy).not.toHaveBeenCalled(); + }); + }); + + describe('mousedown', () => { + const event = new MouseEvent('mousedown', { + bubbles: true + }); + const mutiplePagesContentlet = { + ...dotcmsContentletMock, + onNumberOfPages: 2 + }; + + it('should postMessage the UVE if the content is in multiple pages', () => { + rerenderFn( + + ); + const editorElem = screen.getByTestId('tinymce-editor'); + fireEvent(editorElem, event); + + const payload = { + dataset: { + fieldName: 'title', + inode: mutiplePagesContentlet.inode, + language: mutiplePagesContentlet.languageId + } + }; + expect(postMessageToEditor).toHaveBeenCalledWith({ + action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING, + payload + }); + }); + + it('should not postMessage the UVE if the content is in a single page', () => { + const editorElem = screen.getByTestId('tinymce-editor'); + fireEvent(editorElem, event); + expect(postMessageToEditor).not.toHaveBeenCalled(); + }); + }); + + describe('onFocusOut', () => { + let isDirtySpy: jest.SpyInstance; + let getContentSpy: jest.SpyInstance; + + const event = new FocusEvent('focusout', { + bubbles: true + }); + + beforeEach(() => { + isDirtySpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'isDirty'); + getContentSpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'getContent'); + }); + + it('should not postMessage the UVE if the editor is not dirty', () => { + mockedDotcmsClient.isInsideEditor.mockReturnValue(false); + const editorElem = screen.getByTestId('tinymce-editor'); + fireEvent(editorElem, event); + expect(isDirtySpy).toHaveBeenCalled(); + expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' }); + expect(postMessageToEditor).not.toHaveBeenCalled(); + }); + + it('should not postMessage the UVE if the content did not change', () => { + isDirtySpy.mockReturnValue(true); + getContentSpy.mockReturnValue(dotcmsContentletMock.title); + + const editorElem = screen.getByTestId('tinymce-editor'); + fireEvent(editorElem, event); + + expect(isDirtySpy).toHaveBeenCalled(); + expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' }); + expect(postMessageToEditor).not.toHaveBeenCalled(); + }); + + it('should postMessage the UVE if the content changed', () => { + isDirtySpy.mockReturnValue(true); + getContentSpy.mockReturnValue('New content'); + + const editorElem = screen.getByTestId('tinymce-editor'); + fireEvent(editorElem, event); + + const postMessageData = { + action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING, + payload: { + content: 'New content', + dataset: { + inode: dotcmsContentletMock.inode, + langId: dotcmsContentletMock.languageId, + fieldName: 'title' + } + } + }; + + expect(isDirtySpy).toHaveBeenCalled(); + expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' }); + expect(postMessageToEditor).toHaveBeenCalledWith(postMessageData); + }); + }); + }); + }); + + afterEach(() => jest.clearAllMocks()); // Clear all mocks to avoid side effects from other tests +}); diff --git a/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx new file mode 100644 index 000000000000..5799e525fe15 --- /dev/null +++ b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx @@ -0,0 +1,141 @@ +import { Editor } from '@tinymce/tinymce-react'; +import { useEffect, useRef, useState } from 'react'; + +import { + isInsideEditor as isInsideEditorFn, + postMessageToEditor, + CUSTOMER_ACTIONS, + DotCmsClient +} from '@dotcms/client'; + +import { DotEditableTextProps, TINYMCE_CONFIG } from './utils'; + +const MCE_URL = '/ext/tinymcev7/tinymce.min.js'; + +export function DotEditableText({ + mode = 'plain', + format = 'text', + contentlet, + fieldName = '' +}: Readonly) { + const editorRef = useRef(null); + const [scriptSrc, setScriptSrc] = useState(''); + const [isInsideEditor, setIsInsideEditor] = useState(false); + const [content, setContent] = useState(contentlet?.[fieldName] || ''); + + useEffect(() => { + setIsInsideEditor(isInsideEditorFn()); + + if (!contentlet || !fieldName) { + console.error('DotEditableText: contentlet or fieldName is missing'); + console.error('Ensure that all needed props are passed to view and edit the content'); + + return; + } + + if (!isInsideEditorFn()) { + return; + } + + const createURL = new URL(MCE_URL, DotCmsClient.dotcmsUrl); + setScriptSrc(createURL.toString()); + + const content = contentlet?.[fieldName] || ''; + editorRef.current?.setContent(content, { format }); + setContent(content); + }, [format, fieldName, contentlet?.[fieldName]]); + + useEffect(() => { + if (!isInsideEditorFn()) { + return; + } + + const onMessage = ({ data }: MessageEvent) => { + const { name, payload } = data; + if (name !== 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS') { + return; + } + + const { oldInode, inode } = payload; + const currentInode = contentlet.inode; + const shouldFocus = currentInode === oldInode || currentInode === inode; + + if (shouldFocus) { + editorRef.current?.focus(); + } + }; + + window.addEventListener('message', onMessage); + + return () => { + window.removeEventListener('message', onMessage); + }; + }, [contentlet.inode]); + + const onMouseDown = (event: MouseEvent) => { + const { onNumberOfPages = 1 } = contentlet; + const { inode, languageId: language } = contentlet; + + if (onNumberOfPages <= 1 || editorRef.current?.hasFocus()) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + + postMessageToEditor({ + action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING, + payload: { + dataset: { + inode, + language, + fieldName + } + } + }); + }; + + const onFocusOut = () => { + const editedContent = editorRef.current?.getContent({ format: format }) || ''; + const { inode, languageId: langId } = contentlet; + + if (!editorRef.current?.isDirty() || !didContentChange(editedContent)) { + return; + } + + postMessageToEditor({ + action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING, + payload: { + content: editedContent, + dataset: { + inode, + langId, + fieldName + } + } + }); + }; + + const didContentChange = (editedContent: string) => { + return content !== editedContent; + }; + + if (!isInsideEditor) { + // We can let the user pass the Child Component and create a root to get the HTML for the editor + return ; + } + + return ( + (editorRef.current = editor)} + init={TINYMCE_CONFIG[mode]} + initialValue={content} + onMouseDown={onMouseDown} + onFocusOut={onFocusOut} + /> + ); +} + +export default DotEditableText; diff --git a/core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts b/core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts new file mode 100644 index 000000000000..dc097d6276f8 --- /dev/null +++ b/core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts @@ -0,0 +1,82 @@ +import { IAllProps } from '@tinymce/tinymce-react'; + +import { DotCMSContentlet } from '../../models'; + +export type DOT_EDITABLE_TEXT_FORMAT = 'html' | 'text'; + +export type DOT_EDITABLE_TEXT_MODE = 'minimal' | 'full' | 'plain'; + +export interface DotEditableTextProps { + /** + * Represents the field name of the `contentlet` that can be edited + * + * @memberof DotEditableTextProps + */ + fieldName: string; + /** + * Represents the format of the editor which can be `text` or `html` + * + * @type {DOT_EDITABLE_TEXT_FORMAT} + * @memberof DotEditableTextProps + */ + format?: DOT_EDITABLE_TEXT_FORMAT; + /** + * Represents the mode of the editor which can be `plain`, `minimal`, or `full` + * + * @type {DOT_EDITABLE_TEXT_MODE} + * @memberof DotEditableTextProps + */ + mode?: DOT_EDITABLE_TEXT_MODE; + /** + * Represents the `contentlet` that can be inline edited + * + * @type {DotCMSContentlet} + * @memberof DotEditableTextProps + */ + contentlet: DotCMSContentlet; +} + +const DEFAULT_TINYMCE_CONFIG: IAllProps['init'] = { + inline: true, + menubar: false, + powerpaste_html_import: 'clean', + powerpaste_word_import: 'clean', + suffix: '.min', + valid_styles: { + '*': 'font-size,font-family,color,text-decoration,text-align' + } +}; + +export const TINYMCE_CONFIG: { + [key in DOT_EDITABLE_TEXT_MODE]: IAllProps['init']; +} = { + full: { + ...DEFAULT_TINYMCE_CONFIG, + plugins: 'link lists autolink charmap', + toolbar: [ + 'styleselect undo redo | bold italic underline | forecolor backcolor | alignleft aligncenter alignright alignfull | numlist bullist outdent indent | hr charmap removeformat | link' + ], + style_formats: [ + { title: 'Paragraph', format: 'p' }, + { title: 'Header 1', format: 'h1' }, + { title: 'Header 2', format: 'h2' }, + { title: 'Header 3', format: 'h3' }, + { title: 'Header 4', format: 'h4' }, + { title: 'Header 5', format: 'h5' }, + { title: 'Header 6', format: 'h6' }, + { title: 'Pre', format: 'pre' }, + { title: 'Code', format: 'code' } + ] + }, + plain: { + ...DEFAULT_TINYMCE_CONFIG, + plugins: '', + toolbar: '' + }, + minimal: { + ...DEFAULT_TINYMCE_CONFIG, + plugins: 'link autolink', + toolbar: 'bold italic underline | link', + valid_elements: 'strong,em,span[style],a[href]' + } +}; diff --git a/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx b/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx index f00c83e03b1b..e6b1526de3af 100644 --- a/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx +++ b/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx @@ -1,5 +1,30 @@ import { PageProvider } from '../components/PageProvider/PageProvider'; -import { DotCMSPageContext } from '../models'; +import { DotCMSPageContext, DotCMSContentlet } from '../models'; + +export const dotcmsContentletMock: DotCMSContentlet = { + archived: false, + baseType: '', + contentType: '', + folder: '', + hasTitleImage: false, + host: '', + hostName: '', + identifier: '', + inode: '', + languageId: 1, + live: false, + locked: false, + modDate: '', + modUser: '', + modUserName: '', + owner: '', + sortOrder: 1, + stInode: '', + title: 'This is my editable title', + titleImage: '', + url: '', + working: false +}; export const mockPageContext: DotCMSPageContext = { pageAsset: { diff --git a/core-web/libs/sdk/react/src/lib/models/index.ts b/core-web/libs/sdk/react/src/lib/models/index.ts index 42fc4275b389..4378ffa8156a 100644 --- a/core-web/libs/sdk/react/src/lib/models/index.ts +++ b/core-web/libs/sdk/react/src/lib/models/index.ts @@ -87,3 +87,44 @@ export interface DotCMSPageContext { }; isInsideEditor: boolean; } + +export interface DotCMSContentlet { + archived: boolean; + binaryContentAsset?: string; + deleted?: boolean; + baseType: string; + binary?: string; + binaryVersion?: string; + file?: string; + contentType: string; + hasLiveVersion?: boolean; + folder: string; + hasTitleImage: boolean; + hostName: string; + host: string; + inode: string; + identifier: string; + languageId: number; + image?: string; + locked: boolean; + language?: string; + mimeType?: string; + modUser: string; + modDate: string; + live: boolean; + sortOrder: number; + owner: string; + title: string; + stInode: string; + titleImage: string; + modUserName: string; + text?: string; + working: boolean; + url: string; + contentTypeIcon?: string; + body?: string; + variant?: string; + __icon__?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; // This is a catch-all for any other custom properties that might be on the contentlet. +} diff --git a/core-web/package.json b/core-web/package.json index 21e1af628fc8..e428d83ca991 100644 --- a/core-web/package.json +++ b/core-web/package.json @@ -74,6 +74,7 @@ "@nx/angular": "19.5.6", "@tarekraafat/autocomplete.js": "^10.2.6", "@tinymce/tinymce-angular": "^7.0.0", + "@tinymce/tinymce-react": "^5.1.1", "@tiptap/core": "^2.0.0-beta.218", "@tiptap/extension-bubble-menu": "^2.0.0-beta.218", "@tiptap/extension-character-count": "^2.0.0-beta.218", diff --git a/core-web/yarn.lock b/core-web/yarn.lock index 11e90672221e..3a1fd349659f 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -5177,6 +5177,14 @@ tinymce "^6.0.0 || ^5.5.0" tslib "^2.3.0" +"@tinymce/tinymce-react@^5.1.1": + version "5.1.1" + resolved "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-5.1.1.tgz#3b8555ceaccfa6bb8bb03c3b0c8baaccde138bde" + integrity sha512-DQ0wpvnf/9z8RsOEAmrWZ1DN1PKqcQHfU+DpM3llLze7FHmxVtzuN8O+FYh0oAAF4stzAXwiCIVacfqjMwRieQ== + dependencies: + prop-types "^15.6.2" + tinymce "^7.0.0 || ^6.0.0 || ^5.5.1" + "@tiptap/core@^2.0.0-beta.218", "@tiptap/core@^2.5.8": version "2.5.8" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.5.8.tgz#58de366b0d2acb0a6e67a4780de64d619ebd90fa" @@ -18590,7 +18598,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -21206,6 +21214,11 @@ tiny-relative-date@^1.3.0: resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.8.4.tgz#53e1313ebfe5524b24c0fa45937d51fda058632d" integrity sha512-okoJyxuPv1gzASxQDNgQbnUXOdAIyoOSXcXcZZu7tiW0PSKEdf3SdASxPBupRj+64/E3elHwVRnzSdo82Emqbg== +"tinymce@^7.0.0 || ^6.0.0 || ^5.5.1": + version "7.3.0" + resolved "https://registry.npmjs.org/tinymce/-/tinymce-7.3.0.tgz#929b5d6d151b7d5e6f2efdaa802484834c42872c" + integrity sha512-Ls4PgYlpk73XAxBSBqbVmSl8Mb3DuNfgF01GZ0lY6/MOEVRl3IL+VxC1Oe6165e8WqbqVsxO3Qj/PmoYNvQKGQ== + tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" diff --git a/examples/nextjs/jsconfig.json b/examples/nextjs/jsconfig.json index 611be824c455..0d856efda589 100644 --- a/examples/nextjs/jsconfig.json +++ b/examples/nextjs/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "paths": { - "@/*": ["./src/*"], + "@/*": ["./src/*"] } } } diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index d24ace61533d..6e1663951154 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@tinymce/tinymce-react": "^5.1.1", "@dotcms/client": "0.0.1-alpha.32", "@dotcms/react": "0.0.1-alpha.32", "@dotcms/experiments": "0.0.1-alpha.32", diff --git a/examples/nextjs/src/components/content-types/banner.js b/examples/nextjs/src/components/content-types/banner.js index fa4ac476e5e3..b323ba96ff1c 100644 --- a/examples/nextjs/src/components/content-types/banner.js +++ b/examples/nextjs/src/components/content-types/banner.js @@ -1,7 +1,10 @@ import Image from "next/image"; import Link from "next/link"; +import { DotEditableText } from "@dotcms/react"; + +function Banner(contentlet) { + const { title, caption, image, link, buttonText } = contentlet; -function Banner({ title, image, caption, buttonText, link }) { return (
{image && ( @@ -13,7 +16,12 @@ function Banner({ title, image, caption, buttonText, link }) { /> )}
-

{title}

+

+ +

{caption}

Date: Wed, 28 Aug 2024 09:53:58 -0500 Subject: [PATCH 2/5] #29255 also set siteName to CT (#29769) The siteName is also needed to be set to the CT because otherwise the host it gets looked up by siteName first if it does not exist on the receiver it will fallback to defaultHost, which is *not* the desired behavior here, which is fallback to system_host. --- .../publishing/remote/handler/ContentTypeHandler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ContentTypeHandler.java b/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ContentTypeHandler.java index 4c1ab2516dba..4b48956914ad 100644 --- a/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ContentTypeHandler.java +++ b/dotCMS/src/enterprise/java/com/dotcms/enterprise/publishing/remote/handler/ContentTypeHandler.java @@ -301,11 +301,14 @@ private ContentType saveOrUpdateContentType(final ContentTypeWrapper contentType .toJavaOptional() .orElse(Host.SYSTEM_HOST); + final Host host = APILocator.getHostAPI().find(hostId, APILocator.systemUser(), false); // update host so that it works locally final ContentType typeToSave = hostId.equals(contentTypeIn.host()) ? contentTypeIn - : ContentTypeBuilder.builder(contentTypeIn).from(contentTypeIn).host(hostId).build(); + : ContentTypeBuilder.builder(contentTypeIn).from(contentTypeIn) + .host(host.getIdentifier()) + .siteName(host.getHostname()).build(); final List deferredFields = fields.stream() From 4f4cc27fe07f156678a0fd041bc665435806404a Mon Sep 17 00:00:00 2001 From: Neehakethi-dotcms <139247809+Neehakethi@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:24:58 -0500 Subject: [PATCH 3/5] issue-29667 : Update default value to 4 (#29773) CLEANUP_BACKUP_FILES_OLDER_THAN_DAYS: is set to 3 Now it is changed to 4 --- .../main/java/com/dotmarketing/quartz/job/BinaryCleanupJob.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/quartz/job/BinaryCleanupJob.java b/dotCMS/src/main/java/com/dotmarketing/quartz/job/BinaryCleanupJob.java index 3732b9195cd6..107264bcfe6e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/quartz/job/BinaryCleanupJob.java +++ b/dotCMS/src/main/java/com/dotmarketing/quartz/job/BinaryCleanupJob.java @@ -90,7 +90,7 @@ void cleanUpTmpUploadedFiles() { * Deletes from /assets/bundles */ void cleanUpOldBundles() { - final int days = Config.getIntProperty(CLEANUP_BUNDLES_OLDER_THAN_DAYS, 3); + final int days = Config.getIntProperty(CLEANUP_BUNDLES_OLDER_THAN_DAYS, 4); if (days < 1) { return; } From b8fe837cdfffeaadeec36767bcb4ab23bea03d93 Mon Sep 17 00:00:00 2001 From: "daniel.solis" <2894221+dsolistorres@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:30:26 -0600 Subject: [PATCH 4/5] fix(encoding) : set default character encoding for requests (#29766) Closes #29392 ### Proposed Changes * Upgrade `web.xml` deployment descriptor to use Servlet 4 specification * With Servlet 4, these 2 elements can be added to the `web.xml` configuration file: ``` UTF-8 UTF-8 ``` To set. UTF-8 as the default character encoding for requests and responses. --- dotCMS/src/main/webapp/WEB-INF/web.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/webapp/WEB-INF/web.xml b/dotCMS/src/main/webapp/WEB-INF/web.xml index ce55de25dd3d..5b0174fb06fb 100644 --- a/dotCMS/src/main/webapp/WEB-INF/web.xml +++ b/dotCMS/src/main/webapp/WEB-INF/web.xml @@ -1,10 +1,13 @@ + http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd" + version="4.0" metadata-complete="true"> dotCMS + UTF-8 + UTF-8 + From f58464fd728c610ac534e3bb0603e365b3428bd2 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Monroy Date: Wed, 28 Aug 2024 12:46:13 -0400 Subject: [PATCH 5/5] Epic about PrimeNG Upgrade (#29614) ### Parent Issue https://github.com/dotCMS/core/issues/29058 ### Objective This is a PR to collect small PRs to upgrade from PrimeNG v15 to v17. ### PRs associated - [x] https://github.com/dotCMS/core/pull/29057 - [x] https://github.com/dotCMS/core/pull/29624 - [x] https://github.com/dotCMS/core/pull/29562 - [x] https://github.com/dotCMS/core/pull/29650 - [x] https://github.com/dotCMS/core/pull/29658 - [x] https://github.com/dotCMS/core/pull/29674 - [x] https://github.com/dotCMS/core/pull/29704 - [x] https://github.com/dotCMS/core/pull/29706 - [x] https://github.com/dotCMS/core/pull/29737 --------- Co-authored-by: Arcadio Quintero Co-authored-by: spbolton Co-authored-by: Victor Alfaro Co-authored-by: Humberto Morera <31667212+hmoreras@users.noreply.github.com> --- core-web/README.MD | 1 - ...s-configuration-detail-form.component.html | 6 +- ...onfiguration-detail-form.component.spec.ts | 310 ++++--- .../dot-apps-card.component.spec.ts | 2 +- .../store/dot-categories-list-store.ts | 4 +- .../container-list.component.spec.ts | 11 +- .../dot-add-variable.component.html | 48 +- .../dot-container-code.component.spec.ts | 11 +- .../dot-palette-contentlets.component.spec.ts | 2 +- ...it-page-state-controller.component.spec.ts | 15 +- .../dot-edit-page-toolbar.component.html | 18 +- ...dit-page-view-as-controller.component.html | 8 +- ...-page-view-as-controller.component.spec.ts | 21 +- ...t-page-workflows-actions.component.spec.ts | 8 +- .../dot-edit-page-nav.component.spec.ts | 4 +- ...age-state-controller-seo.component.spec.ts | 11 +- ...page-view-as-controller-seo.component.html | 6 +- .../dot-pages-listing-panel.component.spec.ts | 6 +- .../dot-pages-listing-panel.component.ts | 59 +- .../dot-pages-store/dot-pages.store.spec.ts | 5 +- .../dot-template-list.component.spec.ts | 25 +- .../categories-property.component.html | 8 +- .../dot-edit-relationships.component.html | 2 +- .../dot-new-relationships.component.html | 4 +- .../form/content-types-form.component.html | 2 - .../content-types-layout.component.spec.ts | 5 +- .../dot-content-types-edit.component.spec.ts | 5 +- .../dot-content-types.component.ts | 4 +- .../dot-alert-confirm.spec.ts | 22 +- .../dot-autocomplete-tags.component.html | 2 +- .../dot-autocomplete-tags.component.spec.ts | 58 +- .../dot-autocomplete-tags.component.ts | 8 +- .../dot-page-selector.component.spec.ts | 20 +- .../dot-page-selector.component.ts | 18 +- .../dot-site-selector.component.html | 2 +- ...lows-actions-selector-field.component.html | 3 +- ...s-actions-selector-field.component.spec.ts | 3 +- .../searchable-dropdown.component.html | 69 +- .../searchable-dropdown.component.scss | 4 +- .../searchable-dropdown.component.spec.ts | 16 +- .../searchable-dropdown.component.stories.ts | 107 +-- .../searchable-dropdown.component.ts | 20 +- .../dot-create-persona-form.component.spec.ts | 11 +- .../dot-container-selector.component.html | 2 +- .../dot-crumbtrail.component.html | 1 - .../dot-crumbtrail.component.scss | 52 +- .../dot-crumbtrail.component.ts | 22 +- ...ot-persona-selected-item.component.spec.ts | 4 +- ...dot-persona-selector-option.component.html | 8 +- .../dot-persona-selector.component.html | 38 +- .../dot-persona-selector.component.spec.ts | 2 +- .../dot-persona-selector.component.ts | 2 +- .../dot-portlet-toolbar.component.spec.ts | 2 +- .../dot-portlet-toolbar.component.ts | 4 +- ...dot-theme-selector-dropdown.component.html | 65 +- .../dot-login-as/dot-login-as.component.html | 2 +- .../dot-toolbar-user.component.html | 10 +- .../dot-toolbar-user.component.spec.ts | 16 +- .../dot-login.component.spec.ts | 15 +- .../menu/DotCollapseBreadcrumb.stories.ts | 27 +- .../dotcms/menu/DotCrumbtrail.stories.ts | 10 +- .../stories/primeng/button/Button.stories.ts | 12 +- .../src/stories/primeng/button/templates.ts | 67 +- .../stories/primeng/data/DataView.stories.ts | 89 +- .../src/stories/primeng/data/Tree.stories.ts | 20 +- .../primeng/form/AutoComplete.stories.ts | 6 +- .../stories/primeng/form/Checkbox.stories.ts | 21 +- .../stories/primeng/form/Dropdown.stories.ts | 17 +- .../primeng/form/TreeSelect.stories.ts | 10 +- .../src/stories/primeng/menu/Menu.stories.ts | 50 +- .../src/stories/utils/tree-node-files.ts | 99 +- core-web/karma.conf.js | 2 +- .../dot-block-editor.component.stories.ts | 8 +- core-web/libs/block-editor/tsconfig.json | 3 - .../dot-autocomplete-tags.component.ts | 8 +- .../src/lib/dot-action-menu-item.model.ts | 15 +- .../dotcms-theme/components/_breadcrumb.scss | 77 +- .../dotcms-theme/components/_paginator.scss | 1 - .../dotcms-theme/components/_tree.scss | 326 ++++--- .../components/buttons/_button.scss | 29 + .../components/buttons/_splitbutton.scss | 2 + .../components/form/_autocomplete.scss | 5 + .../components/form/_calendar.scss | 25 +- .../components/form/_dropdown.scss | 6 + .../components/form/_treeselect.scss | 1 + ...tegory-field-search-list.component.spec.ts | 16 +- ...-category-field-selected.component.spec.ts | 21 +- .../dot-category-field-selected.component.ts | 5 +- ...edit-content-checkbox-field.component.html | 8 - ...t-content-checkbox-field.component.spec.ts | 7 +- ...t-edit-content-checkbox-field.component.ts | 37 +- .../componentes/treeselect.component.css | 95 -- .../componentes/treeselect.component.ts | 868 ------------------ ...t-content-host-folder-field.component.html | 2 +- ...t-content-host-folder-field.component.scss | 0 ...ontent-host-folder-field.component.spec.ts | 21 +- ...dit-content-host-folder-field.component.ts | 26 +- ...-content-multi-select-field.component.html | 5 - ...-content-multi-select-field.component.scss | 0 ...ntent-multi-select-field.component.spec.ts | 11 +- ...it-content-multi-select-field.component.ts | 28 +- ...t-edit-content-select-field.component.html | 6 - ...dit-content-select-field.component.spec.ts | 4 +- ...dot-edit-content-select-field.component.ts | 35 +- .../dot-edit-content-tag-field.component.html | 12 - .../dot-edit-content-tag-field.component.scss | 0 ...t-edit-content-tag-field.component.spec.ts | 35 +- .../dot-edit-content-tag-field.component.ts | 49 +- .../lib/services/dot-edit-content.service.ts | 4 +- core-web/libs/edit-content/src/test-setup.ts | 19 +- ...xperiments-configuration.component.spec.ts | 7 +- ...ot-experiments-configuration-store.spec.ts | 13 +- .../store/dot-locales-list.store.spec.ts | 1 - ...-ema-palette-contentlets.component.spec.ts | 26 +- .../edit-ema-palette-contentlets.component.ts | 4 +- .../edit-ema-editor.component.html | 1 - .../edit-ema/portlet/src/test-setup.ts | 19 +- ...ot-content-compare-table.component.spec.ts | 28 +- .../dot-results-seo-tool.component.spec.ts | 2 +- .../dot-select-seo-tool.component.spec.ts | 2 +- .../portlets/edit-ema/ui/src/test-setup.ts | 25 +- .../add-style-classes-dialog.component.html | 2 +- ...add-style-classes-dialog.component.spec.ts | 21 +- ...-style-classes-dialog.component.stories.ts | 5 +- .../add-style-classes-dialog.component.ts | 12 +- .../template-builder-box.component.html | 1 - ...late-builder-theme-selector.component.html | 60 +- ...e-builder-theme-selector.component.spec.ts | 7 +- .../libs/template-builder/src/test-setup.ts | 19 +- .../dot-action-menu-button.component.ts | 8 +- .../dot-workflow-actions.component.html | 11 +- .../dot-workflow-actions.component.spec.ts | 6 +- .../dot-workflow-actions.component.ts | 4 +- .../dot-select-item.directive.spec.ts | 14 +- .../dot-select-item.directive.ts | 11 +- .../dot-container-options.directive.spec.ts | 44 +- ...dot-container-options.directive.stories.ts | 13 +- .../dot-container-options.directive.ts | 34 +- .../mock-containers-dropdown.component.ts | 9 - .../dot-site-selector.directive.spec.ts | 14 +- .../src/lib/dot-rendered-page-state.mock.ts | 20 +- .../src/lib/dot-workflows-actions.mock.ts | 3 - core-web/package.json | 6 +- core-web/yarn.lock | 18 +- 144 files changed, 1565 insertions(+), 2374 deletions(-) delete mode 100644 core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.css delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.scss delete mode 100644 core-web/libs/ui/src/lib/dot-container-options/mock-containers-dropdown.component.ts diff --git a/core-web/README.MD b/core-web/README.MD index 4015f1d3efb3..0f98f8c27c9f 100644 --- a/core-web/README.MD +++ b/core-web/README.MD @@ -46,4 +46,3 @@ Use `nx affected:test` to execute the unit tests affected by recent changes. ### Understanding Your Workspace Run `nx dep-graph` to see a diagram of your projects' dependencies. This will help you better understand the structure and relationships in your monorepo. - diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.html index eb7c734574eb..82d09f4f3944 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.html @@ -3,8 +3,8 @@ + [pTooltip]="field.warnings.length ? field.warnings.join('. ') : ''" + size="18" /> @@ -77,7 +77,7 @@ [ngClass]="{ required: field.required }" - [options]="field.options"> + [options]="field.options" /> {{ field.hint }} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.spec.ts index ab89ca508587..18555c04d988 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.spec.ts @@ -1,15 +1,14 @@ +import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator'; import { MarkdownService } from 'ngx-markdown'; import { CommonModule } from '@angular/common'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component } from '@angular/core'; import { FormGroupDirective, ReactiveFormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; import { ButtonModule } from 'primeng/button'; import { CheckboxModule } from 'primeng/checkbox'; -import { DropdownModule } from 'primeng/dropdown'; +import { Dropdown, DropdownModule } from 'primeng/dropdown'; import { InputTextModule } from 'primeng/inputtext'; import { InputTextareaModule } from 'primeng/inputtextarea'; import { TooltipModule } from 'primeng/tooltip'; @@ -114,203 +113,228 @@ const formState = { class MockMarkdownComponent {} describe('DotAppsConfigurationDetailFormComponent', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - ButtonModule, - CommonModule, - CheckboxModule, - DropdownModule, - DotIconModule, - InputTextareaModule, - InputTextModule, - ReactiveFormsModule, - TooltipModule, - DotFieldRequiredDirective - ], - declarations: [DotAppsConfigurationDetailFormComponent, MockMarkdownComponent], - providers: [MarkdownService, FormGroupDirective] - }); + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: DotAppsConfigurationDetailFormComponent, + imports: [ + HttpClientTestingModule, + ButtonModule, + CommonModule, + CheckboxModule, + DropdownModule, + DotIconModule, + InputTextareaModule, + InputTextModule, + ReactiveFormsModule, + TooltipModule, + DotFieldRequiredDirective + ], + providers: [MarkdownService, FormGroupDirective], + declarations: [MockMarkdownComponent] }); describe('Without warnings', () => { - let component: DotAppsConfigurationDetailFormComponent; - let fixture: ComponentFixture; - let de: DebugElement; - beforeEach(() => { - fixture = TestBed.createComponent(DotAppsConfigurationDetailFormComponent); - de = fixture.debugElement; - component = de.componentInstance; - component.formFields = secrets; - spyOn(component.data, 'emit'); - spyOn(component.valid, 'emit'); - fixture.detectChanges(); + spectator = createComponent({ + props: { + formFields: secrets + } + }); + spectator.detectChanges(); }); it('should load form components', () => { - expect(de.queryAll(By.css('.field')).length).toBe(secrets.length); + const fields = spectator.queryAll('.field'); + expect(fields.length).toBe(secrets.length); }); it('should not have warning icon', () => { - expect(de.query(By.css('dot-icon'))).toBeFalsy(); + const element = spectator.query('dot-icon'); + expect(element).toBeFalsy(); }); it('should focus the first form field after view init', async () => { - fixture.detectChanges(); - await fixture.whenStable(); - const firstFormField = fixture.nativeElement.querySelector( - `#${component.formFields[0].name}` - ); + spectator.detectComponentChanges(); + await spectator.fixture.whenStable(); + + const field = spectator.component.formFields[0]; + const firstFormField = spectator.query(`#${field.name}`); spyOn(firstFormField, 'focus'); - component.ngAfterViewInit(); + spectator.component.ngAfterViewInit(); expect(firstFormField.focus).toHaveBeenCalled(); expect(document.activeElement).toEqual(firstFormField); }); it('should load Label, Textarea & Hint with right attributes', () => { - const row = de.query(By.css('[data-testid="name"]')); - expect(row.query(By.css('markdown'))).toBeTruthy(); - expect(row.query(By.css('label')).nativeElement.textContent.trim()).toBe( - secrets[0].label - ); - expect( - row - .query(By.css('label')) - .nativeElement.classList.contains('p-label-input-required') - ).toBeTruthy(); - expect(row.query(By.css('textarea')).nativeElement.attributes.id.value).toBe( - secrets[0].name - ); - expect(row.query(By.css('textarea')).nativeElement.attributes.autoResize.value).toBe( - 'autoResize' - ); - expect(row.query(By.css('textarea')).nativeElement.value).toBe(secrets[0].value); - expect(row.query(By.css('.p-field-hint')).nativeElement.textContent).toBe( - secrets[0].hint - ); + const row = spectator.query(byTestId('name')); + + const markdownElement = row.querySelector('markdown'); + expect(markdownElement).toBeTruthy(); + + const field = secrets[0]; + + const labelElement = row.querySelector('label'); + expect(labelElement.textContent.trim()).toBe(field.label); + expect(labelElement.classList).toContain('p-label-input-required'); + + const textareaElement = row.querySelector('textarea'); + expect(textareaElement.getAttribute('id')).toBe(field.name); + expect(textareaElement.getAttribute('autoResize')).toBe('autoResize'); + expect(textareaElement.value).toBe(field.value); + + const hintElement = row.querySelector('.p-field-hint'); + expect(hintElement.textContent).toBe(field.hint); }); it('should load Checkbox & Hint with right attributes', () => { - const row = de.query(By.css('[data-testid="enabled"]')); - expect(row.query(By.css('markdown'))).toBeTruthy(); - expect(row.query(By.css('p-checkbox')).nativeElement.attributes.id.value).toBe( - secrets[2].name - ); - expect(row.query(By.css('p-checkbox')).componentInstance.label).toBe(secrets[2].label); - expect(row.query(By.css('input')).nativeElement.value).toBe(secrets[2].value); - expect(row.query(By.css('.p-field-hint')).nativeElement.textContent).toBe( - secrets[2].hint - ); + const row = spectator.query(byTestId('enabled')); + + const markdownElement = row.querySelector('markdown'); + expect(markdownElement).toBeTruthy(); + + const field = secrets[2]; + + const checkboxElement = row.querySelector('p-checkbox'); + expect(checkboxElement.getAttribute('id')).toBe(field.name); + + const labelElement = checkboxElement.querySelector('label'); + expect(labelElement.textContent).toContain(field.label); + + const inputElement = row.querySelector('input'); + expect(inputElement.value).toBe(field.value); + + const hintElement = row.querySelector('.p-field-hint'); + expect(hintElement.textContent).toBe(field.hint); }); it('should load Label, Select & Hint with right attributes', () => { - const row = de.query(By.css('[data-testid="select"]')); - expect(row.query(By.css('markdown'))).toBeTruthy(); - expect(row.query(By.css('label')).nativeElement.textContent.trim()).toBe( - secrets[3].label - ); - expect(row.query(By.css('p-dropdown')).nativeElement.id).toBe(secrets[3].name); - expect(row.query(By.css('p-dropdown')).componentInstance.options).toBe( - secrets[3].options - ); - expect(row.query(By.css('p-dropdown')).componentInstance.value).toBe( - secrets[3].options[0].value - ); - expect(row.query(By.css('.p-field-hint')).nativeElement.textContent).toBe( - secrets[3].hint - ); + const row = spectator.query(byTestId('select')); + + const markdownElement = row.querySelector('markdown'); + expect(markdownElement).toBeTruthy(); + + const field = secrets[3]; + + const labelElement = row.querySelector('label'); + expect(labelElement.textContent.trim()).toBe(field.label); + + const dropdownComponent = spectator.query(Dropdown); + expect(dropdownComponent.id).toBe(field.name); + expect(dropdownComponent.options).toBe(field.options); + expect(dropdownComponent.value).toBe(field.value); + + const hintElement = row.querySelector('.p-field-hint'); + expect(hintElement.textContent).toBe(field.hint); }); it('should load Label, Button & Hint with right attributes', () => { - const row = de.query(By.css('[data-testid="integration"]')); - expect(row.query(By.css('label')).nativeElement.textContent.trim()).toBe( - secrets[4].label - ); - expect(row.query(By.css('button')).nativeElement.id).toBe(secrets[4].name); - expect(row.query(By.css('.form__group-hint')).nativeElement.textContent).toBe( - secrets[4].hint - ); + const row = spectator.query(byTestId('integration')); + + const field = secrets[4]; + + const labelElement = row.querySelector('label'); + expect(labelElement.textContent.trim()).toBe(field.label); + + const buttonElement = row.querySelector('button'); + expect(buttonElement.id).toBe(field.name); + + const hintElement = row.querySelector('.form__group-hint'); + expect(hintElement.textContent).toBe(field.hint); }); it('should Button be disabled when no configured app', () => { - const row = de.query(By.css('[data-testid="integration"]')); - const button = row.query(By.css('button')).nativeElement; - expect(button.disabled).toBe(true); + const row = spectator.query(byTestId('integration')); + const buttonElement = row.querySelector('button'); + expect(buttonElement.disabled).toBe(true); }); it('should Button open link on new tab when clicked on a configured app', () => { - component.appConfigured = true; - fixture.detectChanges(); + spectator.setInput('appConfigured', true); + spectator.detectChanges(); + + const field = secrets[4]; + const openMock = jasmine.createSpy(); window.open = openMock; - const row = de.query(By.css('[data-testid="integration"]')); - const button = row.query(By.css('button')).nativeElement; + const row = spectator.query(byTestId('integration')); + const buttonElement = row.querySelector('button'); - button.click(); - expect(openMock).toHaveBeenCalledWith(secrets[4].value, '_blank'); + buttonElement.click(); + expect(openMock).toHaveBeenCalledWith(field.value, '_blank'); }); it('should emit form state when loaded', async () => { - fixture.detectChanges(); - await fixture.whenStable(); - expect(component.data.emit).toHaveBeenCalledWith(formState); - expect(component.valid.emit).toHaveBeenCalledWith(true); + spectator.detectChanges(); + await spectator.fixture.whenStable(); + + spectator.output('data').subscribe((data) => { + expect(data).toEqual(formState); + }); + + spectator.output('valid').subscribe((data) => { + expect(data).toEqual(true); + }); }); it('should emit form state when value changed', () => { - component.myFormGroup.get('name').setValue('Test2'); - component.myFormGroup.get('password').setValue('Password2'); - component.myFormGroup.get('enabled').setValue('false'); - expect(component.data.emit).toHaveBeenCalledTimes(3); - expect(component.valid.emit).toHaveBeenCalledTimes(3); + const spyDataOutput = spyOn(spectator.component.data, 'emit'); + const spyValidOutput = spyOn(spectator.component.valid, 'emit'); + + spectator.component.myFormGroup.get('name').setValue('Test2'); + spectator.component.myFormGroup.get('password').setValue('Password2'); + spectator.component.myFormGroup.get('enabled').setValue('false'); + + expect(spyDataOutput).toHaveBeenCalledTimes(3); + expect(spyValidOutput).toHaveBeenCalledTimes(3); }); it('should emit form state disabled when required field empty', () => { - component.myFormGroup.get('name').setValue(''); - expect(component.valid.emit).toHaveBeenCalledWith(false); + const spyValidOutput = spyOn(spectator.component.valid, 'emit'); + + spectator.component.myFormGroup.get('name').setValue(''); + expect(spyValidOutput).toHaveBeenCalledWith(false); }); }); describe('With warnings', () => { - let component: DotAppsConfigurationDetailFormComponent; - let fixture: ComponentFixture; - let de: DebugElement; - beforeEach(() => { - fixture = TestBed.createComponent(DotAppsConfigurationDetailFormComponent); - de = fixture.debugElement; - component = de.componentInstance; - component.formFields = secrets.map((item, i) => { - if (i < 3) { - return { - ...item, - warnings: [`error ${i}`] - }; - } + spectator = createComponent({ + props: { + formFields: secrets.map((item, i) => { + if (i < 3) { + return { + ...item, + warnings: [`error ${i}`] + }; + } - return item; + return item; + }) + } }); - fixture.detectChanges(); + spectator.detectChanges(); }); it('should have warning icons', () => { - const warningIcons = de.queryAll(By.css('dot-icon')); - expect(warningIcons[0].attributes['name']).toBe('warning'); - expect(warningIcons[0].attributes['size']).toBe('18'); - expect(warningIcons[0].attributes['ng-reflect-text']).toBe( - component.formFields[0].warnings[0] + const warningIcons = spectator.queryAll('dot-icon'); + const formFields = spectator.component.formFields; + + expect(warningIcons[0].getAttribute('name')).toBe('warning'); + expect(warningIcons[0].getAttribute('size')).toBe('18'); + expect(warningIcons[0].getAttribute('ng-reflect-content')).toBe( + formFields[0].warnings[0] ); - expect(warningIcons[1].attributes['name']).toBe('warning'); - expect(warningIcons[1].attributes['size']).toBe('18'); - expect(warningIcons[1].attributes['ng-reflect-text']).toBe( - component.formFields[1].warnings[0] + + expect(warningIcons[1].getAttribute('name')).toBe('warning'); + expect(warningIcons[1].getAttribute('size')).toBe('18'); + expect(warningIcons[1].getAttribute('ng-reflect-content')).toBe( + formFields[1].warnings[0] ); - expect(warningIcons[2].attributes['name']).toBe('warning'); - expect(warningIcons[2].attributes['size']).toBe('18'); - expect(warningIcons[2].attributes['ng-reflect-text']).toBe( - component.formFields[2].warnings[0] + + expect(warningIcons[2].getAttribute('name')).toBe('warning'); + expect(warningIcons[2].getAttribute('size')).toBe('18'); + expect(warningIcons[2].getAttribute('ng-reflect-content')).toBe( + formFields[2].warnings[0] ); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-card/dot-apps-card.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-card/dot-apps-card.component.spec.ts index 3876d77c1bb9..30f346a9adee 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-card/dot-apps-card.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-card/dot-apps-card.component.spec.ts @@ -128,7 +128,7 @@ describe('DotAppsCardComponent', () => { expect(warningIcon).toBeTruthy(); expect(warningIcon.attributes['name']).toBe('warning'); expect(warningIcon.attributes['size']).toBe('18'); - expect(warningIcon.attributes['ng-reflect-text']).toBe( + expect(warningIcon.attributes['ng-reflect-content']).toBe( `${component.app.sitesWithWarnings} ${messageServiceMock.get( 'apps.invalid.configurations' )}` diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts index a5304036297a..87602ddf7d49 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts @@ -9,7 +9,7 @@ import { map, take } from 'rxjs/operators'; import { DotCategoriesService } from '@dotcms/app/api/services/dot-categories/dot-categories.service'; import { DotMessageService, OrderDirection } from '@dotcms/data-access'; -import { DotActionMenuItem, DotCategory } from '@dotcms/dotcms-models'; +import { DotActionMenuItem, DotCategory, DotMenuItemCommandEvent } from '@dotcms/dotcms-models'; import { DataTableColumn } from '@models/data-table'; export interface DotCategoriesListState { @@ -188,7 +188,7 @@ export class DotCategoriesListStore extends ComponentStore { + command: (event: DotMenuItemCommandEvent) => { this.getChildrenCategories({ sortOrder: 1, filters: { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts index f90cd8a13a10..a9b9bdc193a3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { of } from 'rxjs'; import { CommonModule } from '@angular/common'; @@ -6,6 +7,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, Input, Output } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; import { ConfirmationService, SelectItem } from 'primeng/api'; @@ -294,7 +296,8 @@ describe('ContainerListComponent', () => { HttpClientTestingModule, InputTextModule, MenuModule, - TableModule + TableModule, + BrowserAnimationsModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); @@ -388,7 +391,9 @@ describe('ContainerListComponent', () => { comp.handleActionMenuOpen({} as MouseEvent); - menu.model[0].command(); + menu.model[0].command({ + originalEvent: createFakeEvent('click') + }); expect(dotContainersService.publish).toHaveBeenCalledWith([ '123Published', '123Unpublish', @@ -464,7 +469,7 @@ describe('ContainerListComponent', () => { it('should fetch containers with offset when table emits onPage', () => { spyOn(store, 'getContainersWithOffset'); - table.onPage.emit({ first: 10 }); + table.onPage.emit({ first: 10, rows: 10 }); expect(store.getContainersWithOffset).toHaveBeenCalledWith(10); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.html index 93476281a0b9..fb63b528fe21 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-add-variable/dot-add-variable.component.html @@ -1,22 +1,26 @@ - - -
-
-

- {{ field?.name }} -

- - {{ field?.fieldTypeLabel }} - -
-
- -
-
-
-
+@if (vm$ | async; as vm) { + + + @for (field of data; track $index) { +
+
+

+ {{ field?.name }} +

+ + {{ field?.fieldTypeLabel }} + +
+
+ +
+
+ } +
+
+} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-container-code.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-container-code.component.spec.ts index bd04645a2f5f..3a5107de9007 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-container-code.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-code/dot-container-code.component.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; @@ -247,7 +248,7 @@ describe('DotContentEditorComponent', () => { }); it('should have add content type', fakeAsync(() => { - menu.model[0].command(); + menu.model[0].command({ originalEvent: createFakeEvent('click') }); hostFixture.detectChanges(); const contentTypes = de.queryAll(By.css('p-tabpanel')); const code = de.query(By.css(`[data-testid="${mockContentTypes[0].id}"]`)); @@ -277,8 +278,8 @@ describe('DotContentEditorComponent', () => { })); xit('should have remove content type and focus on another content type', fakeAsync(() => { - menu.model[0].command(); - menu.model[1].command(); + menu.model[0].command({ originalEvent: createFakeEvent('click') }); + menu.model[1].command({ originalEvent: createFakeEvent('click') }); hostFixture.detectChanges(); const code = de.query(By.css(`[data-testid="${mockContentTypes[0].id}"]`)); code.triggerEventHandler('monacoInit', { @@ -313,8 +314,8 @@ describe('DotContentEditorComponent', () => { })); it('should have select content type and focus on field', fakeAsync(() => { - menu.model[0].command(); - menu.model[1].command(); + menu.model[0].command({ originalEvent: createFakeEvent('click') }); + menu.model[1].command({ originalEvent: createFakeEvent('click') }); hostFixture.detectChanges(); const code = de.query(By.css(`[data-testid="${mockContentTypes[0].id}"]`)); code.triggerEventHandler('monacoInit', { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-contentlets/dot-palette-contentlets.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-contentlets/dot-palette-contentlets.component.spec.ts index cc552d0c16c4..121d68181ddd 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-contentlets/dot-palette-contentlets.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-contentlets/dot-palette-contentlets.component.spec.ts @@ -183,7 +183,7 @@ describe('DotPaletteContentletsComponent', () => { expect(paginatorContainer.componentInstance.rows).toBe(25); expect(paginatorContainer.componentInstance.totalRecords).toBe(30); expect(paginatorContainer.componentInstance.showFirstLastIcon).toBe(false); - expect(paginatorContainer.componentInstance.pageLinkSize).toBe('2'); + expect(paginatorContainer.componentInstance.pageLinkSize).toBe(2); paginatorContainer.componentInstance.onPageChange.emit({ first: 25 }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts index 73a239b6aae9..c9aa90ab19e4 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { createFakeEvent } from '@ngneat/spectator'; import { of } from 'rxjs'; import { CommonModule } from '@angular/common'; @@ -229,10 +230,10 @@ describe('DotEditPageStateControllerComponent', () => { expect(lockerDe.classes.warn).toBe(true, 'warn class'); expect(lockerDe.attributes.appendTo).toBe('target'); - expect(lockerDe.attributes['ng-reflect-text']).toBe('Page locked by Some One'); + expect(lockerDe.attributes['ng-reflect-content']).toBe('Page locked by Some One'); expect(lockerDe.attributes['ng-reflect-tooltip-position']).toBe('top'); - expect(locker.modelValue).toBe(false, 'checked'); - expect(locker.disabled).toBe(false, 'disabled'); + expect(locker.modelValue).toBe(false); + expect(locker.disabled).toBe(false); }); it('should have lock info', () => { @@ -578,14 +579,18 @@ describe('DotEditPageStateControllerComponent', () => { }); it("should change the mode when the user clicks on the 'Edit' option", () => { - component.menuItems[0].command(); + component.menuItems[0].command({ + originalEvent: createFakeEvent('click') + }); expect(component.modeChange.emit).toHaveBeenCalledWith(DotPageMode.EDIT); }); it("should call editContentlet when clicking on the 'ContentType Content' option", () => { spyOn(editContentletService, 'edit'); - component.menuItems[1].command(); + component.menuItems[1].command({ + originalEvent: createFakeEvent('click') + }); expect(editContentletService.edit).toHaveBeenCalledWith({ data: { inode: '123' diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.html index e7490b039458..242b571cca44 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.html @@ -14,13 +14,13 @@ + class="dot-variant-header flex gap-3" />
- +

{{ variant.experimentName }}

@@ -31,21 +31,21 @@

{{ variant.experimentName }}

+ [variant]="variant" /> + class="dot-edit__what-changed-button" />
+ class="flex w-full gap-2" />
@@ -54,7 +54,7 @@

{{ variant.experimentName }}

[apiLink]="apiLink" [title]="pageState.page.title" [url]="pageState.page.pageURI" - class="flex gap-2"> + class="flex gap-2" /> {{ variant.experimentName }} - + {{ variant.experimentName }} role="button"> science - + diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.html index 4b0f811e3c51..a07996a842da 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.html @@ -5,7 +5,7 @@ [value]="pageState.viewAs.device" appendTo="body" tooltipPosition="bottom" - tooltipStyleClass="dot-device-selector__dialog"> + tooltipStyleClass="dot-device-selector__dialog" /> + tooltipStyleClass="dot-language-selector__dialog" /> + [pageState]="pageState" /> + [value]="pageState.viewAs.language" /> diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.spec.ts index 9bc317e71c11..d4725530618c 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-view-as-controller/dot-edit-page-view-as-controller.component.spec.ts @@ -230,18 +230,13 @@ describe('DotEditPageViewAsControllerComponent', () => { personaSelector.selected.emit(mockDotPersona); expect(component.changePersonaHandler).toHaveBeenCalledWith(mockDotPersona); - // expect(component.changeViewAs.emit).toHaveBeenCalledWith({ - // language: mockDotLanguage, - // persona: mockDotPersona, - // mode: 'PREVIEW' - // }); }); it('should have Device selector with tooltip', () => { const deviceSelectorDe = de.query(By.css('dot-device-selector')); expect(deviceSelector).not.toBeNull(); expect(deviceSelectorDe.attributes.appendTo).toBe('body'); - expect(deviceSelectorDe.attributes['ng-reflect-text']).toBe('Default Device'); + expect(deviceSelectorDe.attributes['ng-reflect-content']).toBe('Default Device'); expect(deviceSelectorDe.attributes['ng-reflect-tooltip-position']).toBe('bottom'); }); @@ -250,11 +245,6 @@ describe('DotEditPageViewAsControllerComponent', () => { deviceSelector.selected.emit(mockDotDevices[0]); expect(component.changeDeviceHandler).toHaveBeenCalledWith(mockDotDevices[0]); - // expect(component.changeViewAs.emit).toHaveBeenCalledWith({ - // language: mockDotLanguage, - // device: mockDotDevices[0], - // mode: 'PREVIEW' - // }); }); it('should have Language selector', () => { @@ -276,10 +266,6 @@ describe('DotEditPageViewAsControllerComponent', () => { languageSelector.selected.emit(testlanguage); expect(component.changeLanguageHandler).toHaveBeenCalledWith(testlanguage); - // expect(component.changeViewAs.emit).toHaveBeenCalledWith({ - // language: testlanguage, - // mode: 'PREVIEW' - // }); }); it('should propagate the values to the selector components on init', () => { @@ -291,12 +277,7 @@ describe('DotEditPageViewAsControllerComponent', () => { }) ); fixtureHost.detectChanges(); - - // expect(languageSelector.value).toEqual(mockDotPersona); expect(deviceSelector.value).toEqual(mockDotEditPageViewAs.device); - - // expect(personaSelector.value).toEqual(mockDotEditPageViewAs.persona); - // expect(personaSelector.pageId).toEqual(mockDotRenderedPage.page.identifier); }); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-workflows-actions/dot-edit-page-workflows-actions.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-workflows-actions/dot-edit-page-workflows-actions.component.spec.ts index 5ed8ca1181dc..3d0bde922472 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-workflows-actions/dot-edit-page-workflows-actions.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-workflows-actions/dot-edit-page-workflows-actions.component.spec.ts @@ -58,7 +58,7 @@ import { DotEditPageWorkflowsActionsComponent } from './dot-edit-page-workflows- @Component({ selector: 'dot-test-host-component', template: ` - + ` }) class TestHostComponent { @@ -201,7 +201,7 @@ describe('DotEditPageWorkflowsActionsComponent', () => { }); fixture.detectChanges(); - splitButtons = de.queryAll(By.css('.p-menuitem-link')); + splitButtons = de.queryAll(By.css('.p-menuitem-content')); firstButton = splitButtons[0].nativeElement; secondButton = splitButtons[1].nativeElement; thirdButton = splitButtons[2].nativeElement; @@ -279,14 +279,16 @@ describe('DotEditPageWorkflowsActionsComponent', () => { }); }); - it('should fire actions on click in the menu items', () => { + it('should fire actions on click on secondButton', () => { secondButton.click(); expect(dotWorkflowActionsFireService.fireTo).toHaveBeenCalledWith({ actionId: mockWorkflowsActions[1].id, inode: component.page.workingInode, data: undefined }); + }); + it('should fire actions on click on thirdButton', () => { thirdButton.click(); expect(dotWorkflowActionsFireService.fireTo).toHaveBeenCalledWith({ actionId: mockWorkflowsActions[2].id, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts index 9eb96b106701..829c82fc263f 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts @@ -245,7 +245,7 @@ describe('DotEditPageNavComponent', () => { expect(menuListItems[1].nativeElement.classList).toContain( 'edit-page-nav__item--disabled' ); - expect(menuListItems[1].nativeElement.getAttribute('ng-reflect-text')).toBe( + expect(menuListItems[1].nativeElement.getAttribute('ng-reflect-content')).toBe( 'Can’t edit advanced template' ); }); @@ -289,7 +289,7 @@ describe('DotEditPageNavComponent', () => { const label = item.query(By.css('.edit-page-nav__item-text')); expect(label.nativeElement.textContent.trim()).toBe(labels[index]); - expect(item.nativeElement.getAttribute('ng-reflect-text')).toBe( + expect(item.nativeElement.getAttribute('ng-reflect-content')).toBe( 'Enterprise only' ); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts index 6eebc18be85c..b616a6c4965c 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { createFakeEvent } from '@ngneat/spectator'; import { of } from 'rxjs'; import { CommonModule, DecimalPipe } from '@angular/common'; @@ -239,7 +240,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { expect(lockerDe.classes.warn).toBe(true, 'warn class'); expect(lockerDe.attributes.appendTo).toBe('target'); - expect(lockerContainerDe.attributes['ng-reflect-text']).toBe( + expect(lockerContainerDe.attributes['ng-reflect-content']).toBe( 'Page locked by Some One' ); expect(lockerContainerDe.attributes['ng-reflect-tooltip-position']).toBe('bottom'); @@ -635,14 +636,18 @@ describe('DotEditPageStateControllerSeoComponent', () => { }); it("should change the mode when the user clicks on the 'Edit' option", () => { - component.menuItems[0].command(); + component.menuItems[0].command({ + originalEvent: createFakeEvent('click') + }); expect(component.modeChange.emit).toHaveBeenCalledWith(DotPageMode.EDIT); }); it("should call editContentlet when clicking on the 'ContentType Content' option", () => { spyOn(editContentletService, 'edit'); - component.menuItems[1].command(); + component.menuItems[1].command({ + originalEvent: createFakeEvent('click') + }); expect(editContentletService.edit).toHaveBeenCalledWith({ data: { inode: '123' diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-view-as-controller-seo/dot-edit-page-view-as-controller-seo.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-view-as-controller-seo/dot-edit-page-view-as-controller-seo.component.html index ba790fd952d4..c5fd439ee9f2 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-view-as-controller-seo/dot-edit-page-view-as-controller-seo.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-view-as-controller-seo/dot-edit-page-view-as-controller-seo.component.html @@ -12,14 +12,14 @@ (delete)="deletePersonalization($event)" (selected)="changePersonaHandler($event)" [disabled]="(dotPageStateService.haveContent$ | async) === false" - [pageState]="pageState"> + [pageState]="pageState" /> + class="flex dot-edit__what-changed-button" /> @@ -27,7 +27,7 @@ (selected)="changeLanguageHandler($event)" [pageId]="pageState.page.identifier" [readonly]="!!variant" - [value]="pageState.viewAs.language"> + [value]="pageState.viewAs.language" /> { }); it('should set table with params', () => { - const elem = de.query(By.css('p-table')).componentInstance; - expect(elem.loading).toBe(undefined); + const elem: Table = de.query(By.css('p-table')).componentInstance; + expect(elem.loading).toBe(false); expect(elem.lazy).toBe(true); expect(elem.selectionMode).toBe('single'); expect(elem.sortField).toEqual('modDate'); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.ts index af5e9165a932..101745f6b6c4 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.ts @@ -1,4 +1,4 @@ -import { Observable, Subject } from 'rxjs'; +import { Observable } from 'rxjs'; import { AfterViewInit, @@ -7,16 +7,16 @@ import { HostListener, inject, OnDestroy, - OnInit, Output, ViewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { LazyLoadEvent } from 'primeng/api'; import { ContextMenu } from 'primeng/contextmenu'; import { Table } from 'primeng/table'; -import { filter, takeUntil } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { DotMessageService } from '@dotcms/data-access'; @@ -28,7 +28,7 @@ import { DotActionsMenuEventParams } from '../dot-pages.component'; templateUrl: './dot-pages-listing-panel.component.html', styleUrls: ['./dot-pages-listing-panel.component.scss'] }) -export class DotPagesListingPanelComponent implements OnInit, OnDestroy, AfterViewInit { +export class DotPagesListingPanelComponent implements OnDestroy, AfterViewInit { readonly store = inject(DotPageStore); readonly #dotMessageService = inject(DotMessageService); @@ -37,59 +37,41 @@ export class DotPagesListingPanelComponent implements OnInit, OnDestroy, AfterVi @Output() goToUrl = new EventEmitter(); @Output() showActionsMenu = new EventEmitter(); @Output() pageChange = new EventEmitter(); - - private domIdMenuAttached = ''; - private destroy$ = new Subject(); - private scrollElement?: HTMLElement; vm$: Observable = this.store.vm$; - dotStateLabels = { archived: this.#dotMessageService.get('Archived'), published: this.#dotMessageService.get('Published'), revision: this.#dotMessageService.get('Revision'), draft: this.#dotMessageService.get('Draft') }; + #domIdMenuAttached = ''; + #scrollElement?: HTMLElement; - ngOnInit() { + constructor() { this.store.actionMenuDomId$ .pipe( - takeUntil(this.destroy$), + takeUntilDestroyed(), filter((actionMenuDomId) => !!actionMenuDomId) ) .subscribe((actionMenuDomId: string) => { if (actionMenuDomId.includes('tableRow')) { - this.cm.show(); - this.domIdMenuAttached = actionMenuDomId; + this.cm.show(new Event('click')); + this.#domIdMenuAttached = actionMenuDomId; // To hide when the menu is opened } else this.cm.hide(); }); } ngAfterViewInit(): void { - this.scrollElement = document.querySelector('dot-pages'); + this.#scrollElement = document.querySelector('dot-pages'); - this.scrollElement?.addEventListener('scroll', () => { + this.#scrollElement?.addEventListener('scroll', () => { this.closeContextMenu(); }); } ngOnDestroy(): void { - this.destroy$.next(true); - this.destroy$.complete(); - this.scrollElement?.removeAllListeners('scroll'); - } - - /** - * Closes the context menu when the user clicks outside of it - * - * @memberof DotPagesListingPanelComponent - */ - @HostListener('window:click') - private closeContextMenu(): void { - if (this.domIdMenuAttached.includes('tableRow')) { - this.cm.hide(); - this.store.clearMenuActions(); - } + this.#scrollElement?.removeAllListeners('scroll'); } /** @@ -126,7 +108,7 @@ export class DotPagesListingPanelComponent implements OnInit, OnDestroy, AfterVi * @memberof DotPagesComponent */ closedActionsContextMenu() { - this.domIdMenuAttached = ''; + this.#domIdMenuAttached = ''; } /** @@ -178,4 +160,17 @@ export class DotPagesListingPanelComponent implements OnInit, OnDestroy, AfterVi this.store.getPages({ offset: 0 }); this.store.setSessionStorageFilterParams(); } + + /** + * Closes the context menu when the user clicks outside of it + * + * @memberof DotPagesListingPanelComponent + */ + @HostListener('window:click') + private closeContextMenu(): void { + if (this.#domIdMenuAttached.includes('tableRow')) { + this.cm.hide(); + this.store.clearMenuActions(); + } + } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts index 227ac7dcb772..85e9973d4739 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { Observable, of, throwError } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; @@ -639,7 +640,7 @@ describe('DotPageStore', () => { expect(menuActions[7].label).toEqual('contenttypes.content.push_publish'); - menuActions[7].command(); + menuActions[7].command({ originalEvent: createFakeEvent('click') }); expect(dotPushPublishDialogService.open).toHaveBeenCalledWith({ assetIdentifier: item.identifier, @@ -800,7 +801,7 @@ describe('DotPageStore', () => { const publishAction = menuAction.find( (action) => action.label === mockPublishAction.name ); - publishAction.command(); + publishAction.command({ originalEvent: createFakeEvent('click') }); expect(dotHttpErrorManagerService.handle).toHaveBeenCalledWith(error, true); done(); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts index a3842fc8cadd..e89fcafd0b61 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-list/dot-template-list.component.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { createFakeEvent } from '@ngneat/spectator'; import { of, Subject } from 'rxjs'; import { CommonModule } from '@angular/common'; @@ -874,7 +875,7 @@ describe('DotTemplateListComponent', () => { it('should execute Publish action', () => { spyOn(dotTemplatesService, 'publish').and.returnValue(of(mockBulkResponseSuccess)); - menu.model[0].command(); + menu.model[0].command({ originalEvent: createFakeEvent('click') }); expect(dotTemplatesService.publish).toHaveBeenCalledWith([ '123Published', '123Locked' @@ -882,14 +883,14 @@ describe('DotTemplateListComponent', () => { checkNotificationAndReLoadOfPage('Templates published'); }); it('should execute Push Publish action', () => { - menu.model[1].command(); + menu.model[1].command({ originalEvent: createFakeEvent('click') }); expect(dotPushPublishDialogService.open).toHaveBeenCalledWith({ assetIdentifier: '123Published,123Locked', title: 'Push Publish' }); }); it('should execute Add To Bundle action', () => { - menu.model[2].command(); + menu.model[2].command({ originalEvent: createFakeEvent('click') }); fixture.detectChanges(); const addToBundleDialog: DotAddToBundleComponent = fixture.debugElement.query( By.css('dot-add-to-bundle') @@ -900,7 +901,7 @@ describe('DotTemplateListComponent', () => { spyOn(dotTemplatesService, 'unPublish').and.returnValue( of(mockBulkResponseSuccess) ); - menu.model[3].command(); + menu.model[3].command({ originalEvent: createFakeEvent('click') }); expect(dotTemplatesService.unPublish).toHaveBeenCalledWith([ '123Published', '123Locked' @@ -909,7 +910,7 @@ describe('DotTemplateListComponent', () => { }); it('should execute Archive action', () => { spyOn(dotTemplatesService, 'archive').and.returnValue(of(mockBulkResponseSuccess)); - menu.model[4].command(); + menu.model[4].command({ originalEvent: createFakeEvent('click') }); expect(dotTemplatesService.archive).toHaveBeenCalledWith([ '123Published', '123Locked' @@ -920,7 +921,7 @@ describe('DotTemplateListComponent', () => { spyOn(dotTemplatesService, 'unArchive').and.returnValue( of(mockBulkResponseSuccess) ); - menu.model[5].command(); + menu.model[5].command({ originalEvent: createFakeEvent('click') }); expect(dotTemplatesService.unArchive).toHaveBeenCalledWith([ '123Published', '123Locked' @@ -932,7 +933,7 @@ describe('DotTemplateListComponent', () => { spyOn(dotAlertConfirmService, 'confirm').and.callFake((conf) => { conf.accept(); }); - menu.model[6].command(); + menu.model[6].command({ originalEvent: createFakeEvent('click') }); expect(dotTemplatesService.delete).toHaveBeenCalledWith([ '123Published', '123Locked' @@ -952,26 +953,26 @@ describe('DotTemplateListComponent', () => { describe('error', () => { it('should fire exception on publish', () => { spyOn(dotTemplatesService, 'publish').and.returnValue(of(mockBulkResponseFail)); - menu.model[0].command(); + menu.model[0].command({ originalEvent: createFakeEvent('click') }); checkOpenOfDialogService('Templates published'); }); it('should fire exception on unPublish', () => { spyOn(dotTemplatesService, 'unPublish').and.returnValue( of(mockBulkResponseFail) ); - menu.model[3].command(); + menu.model[3].command({ originalEvent: createFakeEvent('click') }); checkOpenOfDialogService('Template unpublished'); }); it('should fire exception on archive', () => { spyOn(dotTemplatesService, 'archive').and.returnValue(of(mockBulkResponseFail)); - menu.model[4].command(); + menu.model[4].command({ originalEvent: createFakeEvent('click') }); checkOpenOfDialogService('Template archived'); }); it('should fire exception on unArchive', () => { spyOn(dotTemplatesService, 'unArchive').and.returnValue( of(mockBulkResponseFail) ); - menu.model[5].command(); + menu.model[5].command({ originalEvent: createFakeEvent('click') }); checkOpenOfDialogService('Template unarchived'); }); it('should fire exception on delete', () => { @@ -979,7 +980,7 @@ describe('DotTemplateListComponent', () => { spyOn(dotAlertConfirmService, 'confirm').and.callFake((conf) => { conf.accept(); }); - menu.model[6].command(); + menu.model[6].command({ originalEvent: createFakeEvent('click') }); checkOpenOfDialogService('Template deleted'); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/categories-property/categories-property.component.html b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/categories-property/categories-property.component.html index 2ed9223a1a10..ae2d7dfcddfa 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/categories-property/categories-property.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/categories-property/categories-property.component.html @@ -11,12 +11,10 @@ id="categories" #searchableDropdown labelPropertyName="categoryName" - placeholder="{{ placeholder }}" - width="100%"> + [placeholder]="placeholder" + width="100%" /> + [message]="'contenttypes.field.properties.category.error.required' | dm" />
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-edit-relationship/dot-edit-relationships.component.html b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-edit-relationship/dot-edit-relationships.component.html index 2c922e34ba03..60fa6381ad26 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-edit-relationship/dot-edit-relationships.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-edit-relationship/dot-edit-relationships.component.html @@ -8,4 +8,4 @@ [totalRecords]="dotPaginatorService.totalRecords" [placeholder]="'contenttypes.field.properties.relationship.existing.placeholder' | dm" width="100%" - labelPropertyName="label"> + labelPropertyName="label" /> diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-new-relationships/dot-new-relationships.component.html b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-new-relationships/dot-new-relationships.component.html index d3c971b7b802..87180fcec9bf 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-new-relationships/dot-new-relationships.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-new-relationships/dot-new-relationships.component.html @@ -17,7 +17,7 @@ [totalRecords]="paginatorService.totalRecords" id="contentType" labelPropertyName="name" - width="100%"> + width="100%" />
@@ -25,5 +25,5 @@ (switch)="cardinalityChanged($event)" [value]="cardinality" [disabled]="editing" - id="cardinality"> + id="cardinality" />
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html index df158c37fe53..ff4a6a4a0ad3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html @@ -97,7 +97,6 @@ [options]="dateVarOptions" [tabindex]="7" [placeholder]="'contenttypes.form.date.field.placeholder' | dm" - [autoDisplayFirst]="false" [showClear]="true" id="content-type-form-publish-date-field" appendTo="body" @@ -111,7 +110,6 @@ { it('should set actions correctly', () => { const addRow: MenuItem = splitButton.componentInstance.model[0]; const addTabDivider: MenuItem = splitButton.componentInstance.model[1]; - addRow.command(); + addRow.command({ originalEvent: createFakeEvent('click') }); expect(dotEventsService.notify).toHaveBeenCalledWith('add-row'); - addTabDivider.command(); + addTabDivider.command({ originalEvent: createFakeEvent('click') }); expect(dotEventsService.notify).toHaveBeenCalledWith('add-tab-divider'); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts index 4dc67ec2d627..f9454c4694d5 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { createFakeEvent } from '@ngneat/spectator'; import { of, throwError } from 'rxjs'; import { Location } from '@angular/common'; @@ -513,11 +514,11 @@ describe('DotContentTypesEditComponent', () => { const dotEventsService = fixture.debugElement.injector.get(DotEventsService); spyOn(dotEventsService, 'notify'); - comp.contentTypeActions[0].command(); + comp.contentTypeActions[0].command({ originalEvent: createFakeEvent('click') }); expect(comp.contentTypeActions[0].label).toBe('Add rows'); expect(dotEventsService.notify).toHaveBeenCalledWith('add-row'); - comp.contentTypeActions[1].command(); + comp.contentTypeActions[1].command({ originalEvent: createFakeEvent('click') }); expect(comp.contentTypeActions[1].label).toBe('Add tab'); expect(dotEventsService.notify).toHaveBeenCalledWith('add-tab-divider'); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts index 72dd6815e8c1..e9d3bd4e0ab5 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts @@ -175,7 +175,7 @@ export class DotContentTypesPortletComponent implements OnInit, OnDestroy { { menuItem: { label: this.dotMessageService.get('contenttypes.action.delete'), - command: (item) => this.removeConfirmation(item), + command: (item: DotCMSContentType) => this.removeConfirmation(item), icon: 'pi pi-trash' }, shouldShow: (item) => !item.fixed && !item.defaultType @@ -210,7 +210,7 @@ export class DotContentTypesPortletComponent implements OnInit, OnDestroy { actions.push({ menuItem: { label: this.dotMessageService.get('contenttypes.content.push_publish'), - command: (item) => this.pushPublishContentType(item) + command: (item: DotCMSContentType) => this.pushPublishContentType(item) } }); } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-alert-confirm/dot-alert-confirm.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-alert-confirm/dot-alert-confirm.spec.ts index fdb56630df5a..72cbe2cb623b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-alert-confirm/dot-alert-confirm.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-alert-confirm/dot-alert-confirm.spec.ts @@ -3,6 +3,8 @@ import { ComponentFixture, fakeAsync, tick, waitForAsync } from '@angular/core/t import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Dialog } from 'primeng/dialog'; + import { DOTTestBed } from '@dotcms/app/test/dot-test-bed'; import { DotAlertConfirmService } from '@dotcms/data-access'; import { LoginService } from '@dotcms/dotcms-js'; @@ -68,8 +70,8 @@ describe('DotAlertConfirmComponent', () => { fixture.detectChanges(); const confirm = de.query(By.css('p-confirmDialog')).componentInstance; - expect(confirm.style).toEqual({ width: '400px' }, 'width'); - expect(confirm.closable).toBe(false, 'closable'); + expect(confirm.style).toEqual({ width: '400px' }); + expect(confirm.closable).toBe(false); }); it('should bind correctly to buttons', fakeAsync(() => { @@ -159,14 +161,14 @@ describe('DotAlertConfirmComponent', () => { }); fixture.detectChanges(); - const dialog = de.query(By.css('p-dialog')).componentInstance; - - expect(dialog.closable).toBe(false, 'closable'); - expect(dialog.draggable).toBe(false, 'draggable'); - expect(dialog.header).toBe('Header Test', 'header'); - expect(dialog.modal).toBe('modal', 'modal'); - expect(dialog.visible).toBe(true, 'visible'); - expect(dialog.style).toEqual({ width: '400px' }, 'width'); + const dialog: Dialog = de.query(By.css('p-dialog')).componentInstance; + + expect(dialog.closable).toBe(false); + expect(dialog.draggable).toBe(false); + expect(dialog.header).toBe('Header Test'); + expect(dialog.modal).toBe(true); + expect(dialog.visible).toBe(true); + expect(dialog.style).toEqual({ width: '400px' }); }); it('should add message', () => { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.html b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.html index b18c1cbbe3f7..9c52084f67b0 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.html @@ -10,7 +10,7 @@ [placeholder]="placeholder" [disabled]="disabled" #autoComplete - field="label" + optionLabel="label" dataKey="label" inputStyleClass="ui-inputtext " appendTo="body" /> diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.spec.ts index d103f961ddaa..b8005fd8116e 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { createFakeEvent } from '@ngneat/spectator'; import { Observable, of } from 'rxjs'; import { DebugElement } from '@angular/core'; @@ -74,7 +75,7 @@ describe('DotAutocompleteTagsComponent', () => { }); it('should set all properties correctly', () => { - expect(autoComplete.field).toEqual('label'); + expect(autoComplete.optionLabel).toEqual('label'); expect(autoComplete.dataKey).toEqual('label'); expect(autoComplete.multiple).toBe(true); expect(autoComplete.placeholder).toEqual('Custom Placeholder'); @@ -102,27 +103,40 @@ describe('DotAutocompleteTagsComponent', () => { }); describe('onKeyUp', () => { - const enterEvent = { key: 'Enter', currentTarget: { value: 'enterEvent' } }; - const newEnterEvent = { key: 'Enter', currentTarget: { value: 'newTag' } }; - const backspaceEvent = { key: 'Backspace' }; - const qEvent = { key: 'q', currentTarget: { value: 'qEvent' } }; + const enterEvent = { + key: 'Enter', + currentTarget: { value: 'enterEvent' } + } as unknown as KeyboardEvent; + const newEnterEvent = { + key: 'Enter', + currentTarget: { value: 'newTag' } + } as unknown as KeyboardEvent; + const backspaceEvent = { key: 'Backspace' } as unknown as KeyboardEvent; + const qEvent = { + key: 'q', + currentTarget: { value: 'qEvent' } + } as unknown as KeyboardEvent; beforeEach(() => { // }); it('should NOT add the tag because user dint hit enter', () => { - autoComplete.onKeyup({ ...qEvent }); + autoComplete.onKeyUp.emit(qEvent); expect(component.value.length).toEqual(2); }); it('should NOT add the tag because label is just white spaces', () => { - autoComplete.onKeyup({ key: 'Enter', currentTarget: { value: ' ' } }); + const mockEvent = { + key: 'Enter', + currentTarget: { value: ' ' } + } as unknown as KeyboardEvent; + autoComplete.onKeyUp.emit(mockEvent); expect(component.value.length).toEqual(2); }); it('should NOT add the tag because is duplicate if the user hit enter', () => { - autoComplete.onKeyup({ ...enterEvent }); + autoComplete.onKeyUp.emit({ ...enterEvent }); expect(component.value[1].label).toEqual(preLoadedTags[1].label); expect(component.value.length).toEqual(2); }); @@ -130,11 +144,11 @@ describe('DotAutocompleteTagsComponent', () => { it('should call checkForTag if user hit enter should add the tag and clear input value', () => { spyOn(component, 'checkForTag').and.callThrough(); spyOn(autoComplete, 'hide').and.callThrough(); - autoComplete.onKeyup(newEnterEvent); + autoComplete.onKeyUp.emit(newEnterEvent); - expect(component.checkForTag).toHaveBeenCalledWith(newEnterEvent); + expect(component.checkForTag).toHaveBeenCalledWith(newEnterEvent); expect(component.value[0].label).toEqual('newTag'); - expect(newEnterEvent.currentTarget.value).toBeNull(); + // expect(newEnterEvent.currentTarget.value).toBeNull(); expect(component.propagateChange).toHaveBeenCalledWith( 'newTag,enterEvent,Dotcms' ); @@ -142,15 +156,18 @@ describe('DotAutocompleteTagsComponent', () => { }); it('should put back last deleted item by the p-autoComplete', () => { - autoComplete.onUnselect.emit({ label: qEvent.currentTarget.value }); - autoComplete.onKeyup({ ...backspaceEvent }); + autoComplete.onUnselect.emit({ + originalEvent: createFakeEvent('click'), + value: { label: 'qEvent' } + }); + autoComplete.onKeyUp.emit({ ...backspaceEvent }); expect(component.value.length).toEqual(3); - expect(component.value[2].label).toEqual(qEvent.currentTarget.value); + expect(component.value[2].label).toEqual('qEvent'); }); it('should not do nothing on backspace if there is not a previous deleted element', () => { component.value = []; - autoComplete.onKeyup({ ...backspaceEvent }); + autoComplete.onKeyUp.emit({ ...backspaceEvent }); expect(component.value.length).toEqual(0); }); }); @@ -163,9 +180,16 @@ describe('DotAutocompleteTagsComponent', () => { siteName: '', persona: null }); - autoComplete.completeMethod.emit({ query: 'test' }); + const fakeEvent = createFakeEvent('click'); + autoComplete.completeMethod.emit({ + originalEvent: fakeEvent, + query: 'test' + }); - expect(component.filterTags).toHaveBeenCalledWith({ query: 'test' }); + expect(component.filterTags).toHaveBeenCalledWith({ + query: 'test', + originalEvent: fakeEvent + }); expect(component.filteredOptions.length).toBe(1); }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.ts index 4ae4446d5d16..7b39f9903cf1 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component.ts @@ -1,7 +1,7 @@ import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { AutoComplete } from 'primeng/autocomplete'; +import { AutoComplete, AutoCompleteUnselectEvent } from 'primeng/autocomplete'; import { take } from 'rxjs/operators'; @@ -94,9 +94,11 @@ export class DotAutocompleteTagsComponent implements OnInit, ControlValueAccesso * * @memberof DotAutocompleteTagsComponent */ - removeItem(tag: DotTag): void { + removeItem(event: AutoCompleteUnselectEvent): void { this.propagateChange(this.getStringifyLabels()); - this.lastDeletedTag = tag; + if (event?.value) { + this.lastDeletedTag = event.value; + } } /** * Set the function to be called when the control receives a change event. diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.spec.ts index 8f40241c63ad..9b8ec4fbd1c9 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; @@ -10,6 +11,7 @@ import { UntypedFormGroup } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AutoCompleteModule } from 'primeng/autocomplete'; @@ -161,7 +163,8 @@ describe('DotPageSelectorComponent', () => { AutoCompleteModule, FormsModule, CommonModule, - ReactiveFormsModule + ReactiveFormsModule, + BrowserAnimationsModule ], providers: [ { provide: DotPageSelectorService, useClass: MockDotPageSelectorService }, @@ -283,7 +286,10 @@ describe('DotPageSelectorComponent', () => { it('should show message of permissions', () => { spyOn(dotPageSelectorService, 'getFolders').and.callThrough(); autocomplete.triggerEventHandler('completeMethod', searchFolderObj); - autocomplete.triggerEventHandler('onSelect', expectedFolderMap[1]); + autocomplete.triggerEventHandler('onSelect', { + originalEvent: createFakeEvent('onSelect'), + value: expectedFolderMap[1] + }); hostFixture.detectChanges(); const message = de.query(By.css('[data-testId="message"]')); expect(message.nativeNode.textContent).toEqual('Folder Permissions'); @@ -314,7 +320,10 @@ describe('DotPageSelectorComponent', () => { it('should emit selected page and propagate changes', () => { spyOn(dotPageSelectorService, 'getPages').and.callThrough(); autocomplete.triggerEventHandler('completeMethod', searchPageObj); - autocomplete.triggerEventHandler('onSelect', expectedPagesMap[0]); + autocomplete.triggerEventHandler('onSelect', { + originalEvent: createFakeEvent('onSelect'), + value: expectedPagesMap[0] + }); expect(component.selected.emit).toHaveBeenCalledWith( expectedPagesMap[0].payload as DotPageAsset ); @@ -345,7 +354,10 @@ describe('DotPageSelectorComponent', () => { const folder = expectedFolderMap[0].payload; spyOn(dotPageSelectorService, 'getFolders').and.callThrough(); autocomplete.triggerEventHandler('completeMethod', searchFolderObj); - autocomplete.triggerEventHandler('onSelect', expectedFolderMap[0]); + autocomplete.triggerEventHandler('onSelect', { + originalEvent: createFakeEvent('onSelect'), + value: expectedFolderMap[0] + }); expect(component.selected.emit).toHaveBeenCalledWith( `//${folder.hostName}${folder.path}` ); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.ts index c9b5e30d6740..bdaa3f54e956 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-page-selector/dot-page-selector.component.ts @@ -3,7 +3,7 @@ import { Observable, of, Subject } from 'rxjs'; import { Component, EventEmitter, forwardRef, Input, Output, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { AutoComplete } from 'primeng/autocomplete'; +import { AutoComplete, AutoCompleteSelectEvent } from 'primeng/autocomplete'; import { switchMap, take } from 'rxjs/operators'; @@ -22,6 +22,7 @@ const NO_SPECIAL_CHAR = /^[a-zA-Z0-9._/-]*$/g; const REPLACE_SPECIAL_CHAR = /[^a-zA-Z0-9._/-]/g; const NO_SPECIAL_CHAR_WHITE_SPACE = /^[a-zA-Z0-9._/-\s]*$/g; const REPLACE_SPECIAL_CHAR_WHITE_SPACE = /[^a-zA-Z0-9._/-\s]/g; + enum SearchType { SITE = 'site', FOLDER = 'folder', @@ -109,17 +110,22 @@ export class DotPageSelectorComponent implements ControlValueAccessor { * @param DotPageAsset item * @memberof DotPageSelectorComponent */ - onSelect(item: DotPageSelectorItem): void { + onSelect(event: AutoCompleteSelectEvent): void { + const { originalEvent, value } = event; + if (this.searchType === 'site') { - const site: Site = item.payload; + const site: Site = value.payload; this.currentHost = site; - this.autoComplete.completeMethod.emit({ query: `//${site.hostname}/` }); + this.autoComplete.completeMethod.emit({ + query: `//${site.hostname}/`, + originalEvent + }); } else if (this.searchType === 'page') { - const page: DotPageAsset = item.payload; + const page: DotPageAsset = value.payload; this.selected.emit(page); this.propagateChange(page.identifier); } else if (this.searchType === 'folder') { - this.handleFolderSelection(item.payload); + this.handleFolderSelection(value.payload); } this.resetResults(); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-site-selector/dot-site-selector.component.html b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-site-selector/dot-site-selector.component.html index 9121252783f1..57709a2a97e6 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-site-selector/dot-site-selector.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-site-selector/dot-site-selector.component.html @@ -14,7 +14,7 @@ [width]="width" #searchableDropdown labelPropertyName="hostname" - cssClassDataList="site_selector__data-list"> + cssClassDataList="site_selector__data-list" /> {{ currentSite?.hostname }} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.html b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.html index 1f9ddf324d7c..8997661483f8 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.html @@ -2,7 +2,6 @@ *ngIf="actions$ | async as actions" (onChange)="handleChange($event)" [(ngModel)]="value" - [autoDisplayFirst]="false" [disabled]="disabled || actions.length === 0" [group]="true" [options]="actions" @@ -10,4 +9,4 @@ [showClear]="true" [style]="{ width: '100%' }" #dropdown - appendTo="body"> + appendTo="body" /> diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.spec.ts index d3efce4ea78d..0b78b6ffe3b2 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.component.spec.ts @@ -147,9 +147,8 @@ describe('DotWorkflowsActionsSelectorFieldComponent', () => { it('should have basics', () => { expect(dropdown.appendTo).toBe('body'); expect(dropdown.group).toBe(true); - expect(dropdown.placeholder).toBe('Select an action'); + expect(dropdown.placeholder()).toBe('Select an action'); expect(dropdown.style).toEqual({ width: '100%' }); - expect(dropdown.autoDisplayFirst).toBe(false); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.html b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.html index 751a2ad7d876..d9649f80f901 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.html @@ -1,12 +1,16 @@ - - - - {{ item.label }} - + + @for (item of data; track $index) { + + @if (item[getValueLabelPropertyName()] === valueString) { + + } + {{ item.label }} + + } @@ -23,18 +27,19 @@ - + @if (label) { + + } -
- -
+ @if (action) { +
+ +
+ } - + + [ngTemplateOutlet]="externalItemListTemplate || defaultListTemplate">
diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.scss index a42e1e8284d2..092b8edc5e8b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.scss +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.scss @@ -51,8 +51,8 @@ ::ng-deep { .p-overlaypanel.paginator { - .p-dataview-content { - margin-bottom: $spacing-9; + .p-paginator { + justify-content: center; } .p-paginator-bottom { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts index ec804eb416fc..c9d53a406805 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts @@ -37,7 +37,7 @@ import { SEARCHABLE_NGFACES_MODULES } from '../searchable-dropdown.module'; [valuePropertyName]="valuePropertyName" [overlayWidth]="overlayWidth" [width]="width" - [disabled]="disabled"> + [disabled]="disabled" /> ` }) class HostTestComponent { @@ -399,12 +399,14 @@ describe('SearchableDropdownComponent', () => { [width]="width" #searchableDropdown cssClassDataList="site_selector__data-list"> - -
- {{ data.label }} -
+ + @for (item of data; track $index) { +
+ {{ item.label }} +
+ }
({ + label: faker.lorem.words(3), + value: faker.lorem.slug(2) +}); + +const data = faker.helpers.multiple(generateFakeOption, { count: 10 }); const meta: Meta = { title: 'DotCMS/Searchable Dropdown', @@ -89,6 +51,16 @@ const meta: Meta = { ] }) ], + args: { + rows: 4, + pageLinkSize: 2, + placeholder: 'Select something', + labelPropertyName: 'label', + width: '300px', + cssClass: '', + data: [...data], + action: action('action') + }, argTypes: { width: { name: 'width', @@ -110,31 +82,32 @@ export default meta; type Story = StoryObj; -export const Default: Story = { - args: { - rows: 4, - pageLinkSize: 2, - placeholder: 'Select something', - labelPropertyName: 'label', - width: '300px', - cssClass: '', - data: [...data] - } +export const Default: Story = {}; + +export const CustomTemplate: Story = { + render: (args) => ({ + props: args, + template: ` + + + @for(item of data; track $index) { +
+

{{ item.label }} --

+
+ } +
+
` + }) }; -export const Secondary: Story = { - args: { - ...Default.args, - cssClass: 'd-secondary' - }, +export const CustomSelectedTemplate: Story = { render: (args) => ({ props: args, template: ` - -
-

{{ item.label }} --

-
-
- ` + + +

--Choose--

+
+
` }) }; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts index 9e8473977218..afa62e994286 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts @@ -21,7 +21,7 @@ import { import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { PrimeTemplate } from 'primeng/api'; -import { DataView } from 'primeng/dataview'; +import { DataView, DataViewLazyLoadEvent } from 'primeng/dataview'; import { OverlayPanel } from 'primeng/overlaypanel'; import { debounceTime, tap } from 'rxjs/operators'; @@ -50,7 +50,7 @@ export class SearchableDropdownComponent @Input() data: Record[]; - @Input() action: (action: unknown) => void; + @Input() action: (event: Event) => void; @Input() labelPropertyName: string | string[]; @@ -142,7 +142,7 @@ export class SearchableDropdownComponent value: unknown; overlayPanelMinHeight: string; options: unknown[]; - label: string; + label: string | null = null; externalSelectTemplate: TemplateRef; selectedOptionIndex = 0; @@ -200,7 +200,7 @@ export class SearchableDropdownComponent ngAfterContentInit() { this.totalRecords = this.totalRecords || this.data?.length; this.templates.forEach((item: PrimeTemplate) => { - if (item.getType() === 'listItem') { + if (item.getType() === 'list') { this.externalItemListTemplate = item.template; } else if (item.getType() === 'select') { this.externalSelectTemplate = item.template; @@ -260,13 +260,17 @@ export class SearchableDropdownComponent * @param {PaginationEvent} event * @memberof SearchableDropdownComponent */ - paginate(event: PaginationEvent): void { - const paginationEvent = Object.assign({}, event); + paginate(event: DataViewLazyLoadEvent): void { + const paginationEvent = { + first: event.first, + rows: event.rows, + filter: '' + }; if (this.searchInput) { paginationEvent.filter = this.searchInput.nativeElement.value; } - this.pageChange.emit(paginationEvent); + this.pageChange.emit(paginationEvent as PaginationEvent); } /** @@ -422,7 +426,7 @@ export class SearchableDropdownComponent this.setLabel(); } - private getValueLabelPropertyName(): string { + public getValueLabelPropertyName(): string { return Array.isArray(this.labelPropertyName) ? this.labelPropertyName[0] : this.labelPropertyName; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.spec.ts index c1ace11c910d..0051844dd317 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.spec.ts @@ -5,7 +5,7 @@ import { UntypedFormBuilder } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { FileUploadModule } from 'primeng/fileupload'; +import { FileUpload, FileUploadModule } from 'primeng/fileupload'; import { DotAutocompleteTagsComponent } from '@components/_common/dot-autocomplete-tags/dot-autocomplete-tags.component'; import { DotAutocompleteTagsModule } from '@components/_common/dot-autocomplete-tags/dot-autocomplete-tags.module'; @@ -146,11 +146,12 @@ describe('DotCreatePersonaFormComponent', () => { it('should set the p-fileUpload with the correctly attributes', () => { const fileUpload: DebugElement = fixture.debugElement.query(By.css('p-fileUpload')); + const componentInstance: FileUpload = fileUpload.componentInstance; - expect(fileUpload.componentInstance.url).toEqual('/api/v1/temp'); - expect(fileUpload.componentInstance.accept).toEqual('image/*,.webp'); - expect(fileUpload.componentInstance.auto).toEqual('true'); - expect(fileUpload.componentInstance.mode).toEqual('basic'); + expect(componentInstance.url).toEqual('/api/v1/temp'); + expect(componentInstance.accept).toEqual('image/*,.webp'); + expect(componentInstance.auto).toEqual(true); + expect(componentInstance.mode).toEqual('basic'); }); it('should emit isValid to false when the file upload starts', () => { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.html index 799287e8d586..3890db3fa79f 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.html @@ -13,4 +13,4 @@ placeholder="{{ 'editpage.container.add.label' | dm }}" persistentPlaceholder="true" width="fit-content" - overlayWidth="440px">
+ overlayWidth="440px" /> diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.html deleted file mode 100644 index 5571e023509b..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.scss index 567652908b22..91ff4efb9c73 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.scss +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.scss @@ -1,35 +1,35 @@ @use "variables" as *; :host ::ng-deep .p-breadcrumb { - ul li { - &:first-child { - font-size: $font-size-lmd; - a span.p-menuitem-text { - color: $black; - font-size: $font-size-lmd; - font-weight: $font-weight-regular-bold; + .p-breadcrumb-list { + li { + &:first-child { + font-size: $font-size-lg; + a span.p-menuitem-text { + color: $black; + font-size: $font-size-lg; + font-weight: $font-weight-regular-bold; + } } - } - - .p-menuitem-link[href] { - .p-menuitem-text { - font-size: $font-size-lmd; + .p-menuitem-link[href] { + .p-menuitem-text { + font-size: $font-size-lg; + } } - } - - &:last-child a { - pointer-events: none; - } - &:nth-child(2) { - &::before { - content: ""; - width: 1px; - height: 20px; - background: $color-palette-gray-500; - display: block; + &:last-child a { + pointer-events: none; } - .p-icon-wrapper { - display: none; + &:nth-child(2) { + &::before { + content: ""; + width: 1px; + height: 20px; + background: $color-palette-gray-500; + display: block; + } + .p-icon-wrapper { + display: none; + } } } } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.ts index 3aaf7e4b0e51..66890b390130 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-crumbtrail/dot-crumbtrail.component.ts @@ -1,19 +1,15 @@ -import { Observable } from 'rxjs'; +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; -import { Component, OnInit } from '@angular/core'; - -import { DotCrumb, DotCrumbtrailService } from './service/dot-crumbtrail.service'; +import { DotCrumbtrailService } from './service/dot-crumbtrail.service'; @Component({ selector: 'dot-crumbtrail', - templateUrl: './dot-crumbtrail.component.html', + template: '', styleUrls: ['./dot-crumbtrail.component.scss'] }) -export class DotCrumbtrailComponent implements OnInit { - crumb: Observable; - - constructor(private crumbTrailService: DotCrumbtrailService) {} - - ngOnInit() { - this.crumb = this.crumbTrailService.crumbTrail$; - } +export class DotCrumbtrailComponent { + readonly #crumbTrailService = inject(DotCrumbtrailService); + $model = toSignal(this.#crumbTrailService.crumbTrail$, { + initialValue: [] + }); } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selected-item/dot-persona-selected-item.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selected-item/dot-persona-selected-item.component.spec.ts index f7821b392a88..0601e4b7bb05 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selected-item/dot-persona-selected-item.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selected-item/dot-persona-selected-item.component.spec.ts @@ -89,7 +89,7 @@ describe('DotPersonaSelectedItemComponent', () => { it('should set properties to null when enable', () => { container = de.query(By.css('.dot-persona-selector__container')).nativeElement; expect(container.getAttribute('ng-reflect-tooltip-position')).toEqual(null); - expect(container.getAttribute('ng-reflect-text')).toEqual(null); + expect(container.getAttribute('ng-reflect-content')).toEqual(null); }); it('should set properties correctly when disable', () => { @@ -97,7 +97,7 @@ describe('DotPersonaSelectedItemComponent', () => { fixture.detectChanges(); container = de.query(By.css('.dot-persona-selector__container')).nativeElement; expect(container.getAttribute('ng-reflect-tooltip-position')).toEqual('bottom'); - expect(container.getAttribute('ng-reflect-text')).toEqual('Add content...'); + expect(container.getAttribute('ng-reflect-content')).toEqual('Add content...'); }); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector-option/dot-persona-selector-option.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector-option/dot-persona-selector-option.component.html index f00190e229d0..7b38fbbca652 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector-option/dot-persona-selector-option.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector-option/dot-persona-selector-option.component.html @@ -3,12 +3,8 @@ [text]="persona.name" [image]="persona.photo" pBadge - dotAvatar> - + dotAvatar /> + + + @for (personaData of data; track $index) { + + } + - - - + tooltipPosition="bottom" />
- + diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts index e084aeb17a54..3740b85b7bc1 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.spec.ts @@ -183,7 +183,7 @@ describe('DotPersonaSelectorComponent', () => { it('should set dot-persona-selected-item with right attributes', () => { const personaSelectedItemDe = de.query(By.css('dot-persona-selected-item')); expect(personaSelectedItemDe.attributes.appendTo).toBe('target'); - expect(personaSelectedItemDe.attributes['ng-reflect-text']).toBe('Default Visitor'); + expect(personaSelectedItemDe.attributes['ng-reflect-content']).toBe('Default Visitor'); expect(personaSelectedItemDe.attributes['ng-reflect-tooltip-position']).toBe('bottom'); }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.ts index 43f1550e1e29..1a39273ed3c5 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-persona-selector/dot-persona-selector.component.ts @@ -113,7 +113,7 @@ export class DotPersonaSelectorComponent implements OnInit { * @memberof DotPersonaSelectorComponent */ personaChange(persona: DotPersona): void { - if (!this.value || this.value.identifier !== persona.identifier) { + if (!this.value || this.value?.identifier !== persona.identifier) { this.selected.emit(persona); } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.spec.ts index 607e212e21c2..e6400165a0ae 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.spec.ts @@ -115,7 +115,7 @@ describe('DotPortletToolbarComponent', () => { actionsPrimaryButton.triggerEventHandler('click', {}); expect(actionsPrimaryButton.nativeElement.textContent).toBe('Save'); - expect(spy).toHaveBeenCalledWith({}); + expect(spy).toHaveBeenCalled(); const actionsMenu = de.query(By.css('[data-testId="actionsMenu"]')); expect(actionsMenu).toBeNull(); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.ts index b0f121eb4edb..d1184d27edec 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-portlet-base/components/dot-portlet-toolbar/dot-portlet-toolbar.component.ts @@ -37,9 +37,9 @@ export class DotPortletToolbarComponent { * @param {MouseEvent} $event * @memberof DotPortletToolbarComponent */ - onPrimaryClick($event: MouseEvent): void { + onPrimaryClick($event: Event): void { try { - this.actions.primary[0].command($event); + this.actions.primary[0].command({ originalEvent: $event }); } catch (error) { console.error(error); } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-theme-selector-dropdown/dot-theme-selector-dropdown.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-theme-selector-dropdown/dot-theme-selector-dropdown.component.html index 1de2c9d339f4..771d6e6cd2cf 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-theme-selector-dropdown/dot-theme-selector-dropdown.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-theme-selector-dropdown/dot-theme-selector-dropdown.component.html @@ -1,32 +1,3 @@ - - - - - -
- {{ item.label.charAt(0) }} -
-
- - {{ item.label }} - - {{ 'Last-Updated' | dm }}: {{ item.modDate | date: 'MM/dd/yy' }} - - -
-
-
+ + @for (item of data; track $index) { + + + + +
+ {{ item.label.charAt(0) }} +
+
+ + {{ item.label }} + + {{ 'Last-Updated' | dm }}: {{ item.modDate | date: 'MM/dd/yy' }} + + +
+ } +
+ + width="100%" /> diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-login-as/dot-login-as.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-login-as/dot-login-as.component.html index 539f1cd9119d..d528aa1f9654 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-login-as/dot-login-as.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-login-as/dot-login-as.component.html @@ -27,7 +27,7 @@ placeholder="{{ 'loginas.select.loginas.user' | dm }}" formControlName="loginAsUser" labelPropertyName="fullName" - overlayWidth="300px"> + overlayWidth="300px" />
+ dotGravatar /> + styleClass="toolbar-user__menu" /> @if (vm.showMyAccount) { - + } @if (vm.showLoginAs) { - + } } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts index 8a4b430b1e81..9851fb7c54b9 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/components/dot-toolbar-user/dot-toolbar-user.component.spec.ts @@ -138,9 +138,11 @@ describe('DotToolbarUserComponent', () => { avatarComponent.click(); fixture.detectChanges(); - const logoutLink = de.query(By.css('#dot-toolbar-user-link-logout')); + const logoutItem = de.query(By.css('#dot-toolbar-user-link-logout')); + const logoutLink = logoutItem.query(By.css('a')); + expect(logoutLink.attributes.href).toBe('/dotAdmin/logout?r=1466424490000'); - expect(logoutLink.parent.classes['toolbar-user__logout']).toBe(true); + expect(logoutItem.classes['toolbar-user__logout']).toBe(true); }); it('should have correct target in logout link', () => { fixture.detectChanges(); @@ -149,7 +151,7 @@ describe('DotToolbarUserComponent', () => { avatarComponent.click(); fixture.detectChanges(); - const logoutLink = de.query(By.css('#dot-toolbar-user-link-logout')); + const logoutLink = de.query(By.css('#dot-toolbar-user-link-logout a')); expect(logoutLink.attributes.target).toBe('_self'); }); @@ -168,12 +170,8 @@ describe('DotToolbarUserComponent', () => { avatarComponent.click(); fixture.detectChanges(); - const logoutAsLink = de.query(By.css('#dot-toolbar-user-link-logout-as')); - logoutAsLink.triggerEventHandler('click', { - preventDefault: () => { - // - } - }); + const logoutAsLink = de.query(By.css('#dot-toolbar-user-link-logout-as a')).nativeElement; + logoutAsLink.click(); await fixture.whenStable(); expect(loginService.logoutAs).toHaveBeenCalledTimes(1); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-component/dot-login.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-component/dot-login.component.spec.ts index 360a85e23813..fac353f1f760 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-component/dot-login.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-component/dot-login.component.spec.ts @@ -12,7 +12,7 @@ import { ActivatedRoute, Params, RouterLink } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { ButtonModule } from 'primeng/button'; -import { Checkbox, CheckboxModule } from 'primeng/checkbox'; +import { CheckboxModule } from 'primeng/checkbox'; import { Dropdown, DropdownModule } from 'primeng/dropdown'; import { DotLoadingIndicatorModule } from '@components/_common/iframe/dot-loading-indicator/dot-loading-indicator.module'; @@ -191,7 +191,7 @@ describe('DotLoginComponent', () => { expect(dotFormatDateService.setLang).toHaveBeenCalledWith('en_US'); }); - it('should disable fields while waiting login response', () => { + it('should disable fields while waiting login response', async () => { component.loginForm.setValue(credentials); spyOn(dotRouterService, 'goToMain'); spyOn(loginService, 'loginUser').and.returnValue( @@ -202,21 +202,20 @@ describe('DotLoginComponent', () => { ); signInButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + await fixture.whenStable(); + const languageDropdown: Dropdown = de.query( By.css('[data-testId="language"]') ).componentInstance; const emailInput = de.query(By.css('[data-testId="userNameInput"]')); const passwordInput = de.query(By.css('[data-testId="password"]')); - const rememberCheckBox: Checkbox = de.query( - By.css('[data-testId="rememberMe"]') - ).componentInstance; - - fixture.detectChanges(); + const rememberCheckBox = component.loginForm.get('rememberMe'); expect(languageDropdown.disabled).toBeTruthy(); expect(emailInput.nativeElement.disabled).toBeTruthy(); expect(passwordInput.nativeElement.disabled).toBeTruthy(); - expect(rememberCheckBox.disabled).toBeTruthy(); + expect(rememberCheckBox.disable).toBeTruthy(); }); it('should keep submit button disabled until the form is valid', () => { diff --git a/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCollapseBreadcrumb.stories.ts b/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCollapseBreadcrumb.stories.ts index 4c36fa02b062..03277de86afe 100644 --- a/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCollapseBreadcrumb.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCollapseBreadcrumb.stories.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ +import { action } from '@storybook/addon-actions'; import { Meta, StoryObj, @@ -11,11 +11,14 @@ import { import { importProvidersFrom } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MenuItem } from 'primeng/api'; import { ToastModule } from 'primeng/toast'; import { DotCollapseBreadcrumbComponent } from '@dotcms/ui'; -const meta: Meta = { +type Args = DotCollapseBreadcrumbComponent & { model: MenuItem[]; maxItems: number }; + +const meta: Meta = { title: 'DotCMS/Menu/DotCollapseBreadcrumb', component: DotCollapseBreadcrumbComponent, decorators: [ @@ -40,10 +43,10 @@ const meta: Meta = { } }, argTypes: { - $model: { + model: { description: 'Menu items to display' }, - $maxItems: { + maxItems: { description: 'Max items to display', control: { type: 'number' } } @@ -56,17 +59,17 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { - $maxItems: 4, - $model: [ - { label: 'Electronics', command: console.log }, - { label: 'Computer', command: console.log }, - { label: 'Accessories', command: console.log }, - { label: 'Keyboard', command: console.log }, - { label: 'Wireless', command: console.log } + maxItems: 4, + model: [ + { label: 'Electronics', command: action('command') }, + { label: 'Computer', command: action('command') }, + { label: 'Accessories', command: action('command') }, + { label: 'Keyboard', command: action('command') }, + { label: 'Wireless', command: action('command') } ] } }; diff --git a/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCrumbtrail.stories.ts b/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCrumbtrail.stories.ts index d75db1784964..c54e60512bf1 100644 --- a/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCrumbtrail.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/dotcms/menu/DotCrumbtrail.stories.ts @@ -41,7 +41,6 @@ const meta: Meta = { ) ], parameters: { - layout: 'centered', docs: { description: { component: @@ -49,11 +48,10 @@ const meta: Meta = { } } }, - render: () => { - return { - template: `` - }; - } + render: (args) => ({ + props: args, + template: `` + }) }; export default meta; diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/button/Button.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/button/Button.stories.ts index 70a4dbb8b9fc..34e467167845 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/button/Button.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/button/Button.stories.ts @@ -2,13 +2,7 @@ import { Meta, StoryObj } from '@storybook/angular'; import { Button } from 'primeng/button'; -type Args = Button & { - size: string; - severity: string; - rounded: string; -}; - -const meta: Meta = { +const meta: Meta = { title: 'PrimeNG/Button', component: Button, args: { @@ -46,9 +40,9 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const Main: Story = { +export const Default: Story = { render: (args) => { const argsWithClasses = ['size', 'severity', 'type', 'rounded']; const parts = []; diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/button/templates.ts b/core-web/apps/dotcms-ui/src/stories/primeng/button/templates.ts index 7c2dfadc12df..3538f96657f2 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/button/templates.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/button/templates.ts @@ -848,145 +848,164 @@ export const BasicSplitButtonTemplate = `
- +
- +
diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/data/DataView.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/data/DataView.stories.ts index f0a7f62d1cbb..b087c28bcd98 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/data/DataView.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/data/DataView.stories.ts @@ -12,14 +12,19 @@ type Data = { category: string; }; -const data: Data[] = Array.from({ length: 100 }, () => ({ +const generateFakeProduct = (): Data => ({ id: faker.commerce.isbn(), name: faker.commerce.productName(), description: faker.commerce.productDescription(), - image: faker.image.url(), + image: faker.image.url({ + width: 200, + height: 200 + }), price: faker.commerce.price(), category: faker.commerce.department() -})); +}); + +const data = faker.helpers.multiple(generateFakeProduct, { count: 50 }); const meta: Meta = { title: 'PrimeNG/Data/DataView', @@ -35,14 +40,53 @@ const meta: Meta = { } } }, - render: (args) => { - return { - props: { ...args }, - template: ` - - -
-
+ args: { + value: [...data], + rows: 3, + paginator: true, + layout: 'list' + }, + render: (args) => ({ + props: { ...args }, + template: ` + + + @for(item of products; track item.id){ +
+
+
+ +
+
+ {{ item.category }} +
{{ item.name }}
+
+
+
+ } +
+
` + }) +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Grid: Story = { + args: { + layout: 'grid', + rows: 6 + }, + render: (args) => ({ + props: { ...args }, + template: ` + + +
+ @for(item of products; track item.id){ +
@@ -52,21 +96,10 @@ const meta: Meta = {
{{ item.name }}
-
-
-
-
` - }; - } -}; -export default meta; - -type Story = StoryObj; - -export const Primary: Story = { - args: { - value: [...data], - rows: 3, - paginator: true - } +
+ } +
+
+
` + }) }; diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/data/Tree.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/data/Tree.stories.ts index ec47b99e355b..3c2e743fe036 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/data/Tree.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/data/Tree.stories.ts @@ -10,7 +10,7 @@ import { FormsModule } from '@angular/forms'; import { Tree, TreeModule } from 'primeng/tree'; -import { files } from '../../utils/tree-node-files'; +import { generateFakeTree } from '../../utils/tree-node-files'; const meta: Meta = { title: 'PrimeNG/Data/Tree', @@ -28,18 +28,15 @@ const meta: Meta = { layout: 'centered', docs: { description: { - component: - 'Tree is used to display hierarchical data: https://www.primefaces.org/primeng-v15-lts/tree' + component: 'Tree is used to display hierarchical data: https://primeng.org/tree' } } }, args: { - value: [...files] + value: [...generateFakeTree()] }, render: (args) => ({ - props: { - ...args - }, + props: args, template: `` }) }; @@ -49,15 +46,22 @@ type Story = StoryObj; export const Default: Story = {}; +export const Checkbox: Story = { + args: { + selectionMode: 'checkbox' + } +}; + export const VirtualScroll: Story = { args: { + value: [...generateFakeTree(1000)], virtualScroll: true, virtualScrollItemSize: 30, virtualScrollOptions: { autoSize: true, style: { width: '200px', - height: '200px' + height: '300px' } } } diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/form/AutoComplete.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/form/AutoComplete.stories.ts index 8b8ac578ea92..ec161edddc9a 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/form/AutoComplete.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/form/AutoComplete.stories.ts @@ -1,3 +1,4 @@ +import { action } from '@storybook/addon-actions'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { UPDATE_STORY_ARGS } from '@storybook/core-events'; @@ -99,7 +100,6 @@ export default meta; type Story = StoryObj; -// First arguments is the Args from Meta, the second one is the whole storybook context export const Main: Story = { render: (args, { id }) => { return { @@ -122,10 +122,12 @@ export const Main: Story = { suggestions: filtered // This way the reference of suggestions change in runtime and primeng finish its change detection lifecycle } }); - } + }, + onSelect: action('onSelect') }, template: ` ({ props: args, template: ` - - ` + @for(city of cities; track $index){ + + }` }) }; export default meta; diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/form/Dropdown.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/form/Dropdown.stories.ts index 3a8b1142cc4d..dca68f6546fd 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/form/Dropdown.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/form/Dropdown.stories.ts @@ -66,7 +66,7 @@ export default meta; type Story = StoryObj; -export const Primary: Story = { +export const Default: Story = { parameters: { docs: { source: { @@ -76,3 +76,18 @@ export const Primary: Story = { } } }; + +export const CustomTemplate: Story = { + render: (args) => ({ + props: args, + template: ` + + + --{{ selected.label }}-- + + + **{{ item.label }}** + + ` + }) +}; diff --git a/core-web/apps/dotcms-ui/src/stories/primeng/form/TreeSelect.stories.ts b/core-web/apps/dotcms-ui/src/stories/primeng/form/TreeSelect.stories.ts index 684276130ffc..d0425369cc0c 100644 --- a/core-web/apps/dotcms-ui/src/stories/primeng/form/TreeSelect.stories.ts +++ b/core-web/apps/dotcms-ui/src/stories/primeng/form/TreeSelect.stories.ts @@ -12,7 +12,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TreeSelectModule, TreeSelect } from 'primeng/treeselect'; -import { files } from '../../utils/tree-node-files'; +import { generateFakeTree } from '../../utils/tree-node-files'; type ExtraArgs = { invalid: boolean; @@ -37,21 +37,19 @@ const meta: Meta = { docs: { description: { component: - 'TreeSelect is a form component to choose from hierarchical data.: https://www.primefaces.org/primeng-v15-lts/treeselect' + 'TreeSelect is a form component to choose from hierarchical data.: https://primeng.org/treeselect' } } }, args: { placeholder: 'Select Item', - options: [...files], + options: [...generateFakeTree()], showClear: true, invalid: false, selectionMode: 'single' }, render: (args) => ({ - props: { - ...args - }, + props: args, template: ` { - console.log('update'); - } + command: () => action('update') }, { label: 'Delete', icon: 'pi pi-times', - command: () => { - console.log('delete'); - } + command: () => action('delete') } ] } @@ -45,7 +41,8 @@ const meta: Meta = { decorators: [ moduleMetadata({ imports: [MenuModule, BrowserAnimationsModule, ButtonModule] - }) + }), + componentWrapperDecorator((story) => `
${story}
`) ], args: { items: [...items] @@ -58,14 +55,43 @@ type Story = StoryObj; export const Basic: Story = { render: (args) => ({ props: args, - template: `` + template: `` }) }; export const Overlay: Story = { render: (args) => ({ props: args, - template: ` - ` + template: ` + + ` + }) +}; + +export const WithCustomLabels: Story = { + args: { + items: [ + { + id: 'custom-label', + label: ` +

My custom label

`, + escape: false, + target: '_self' + }, + { separator: true }, + { + id: 'my-account', + label: 'my-account', + icon: 'pi pi-user', + visible: true, + command: () => action('my-account') + } + ] + }, + render: (args) => ({ + props: args, + template: ` + + ` }) }; diff --git a/core-web/apps/dotcms-ui/src/stories/utils/tree-node-files.ts b/core-web/apps/dotcms-ui/src/stories/utils/tree-node-files.ts index 3d398ad6cf93..464502c57ed4 100644 --- a/core-web/apps/dotcms-ui/src/stories/utils/tree-node-files.ts +++ b/core-web/apps/dotcms-ui/src/stories/utils/tree-node-files.ts @@ -1,82 +1,21 @@ +import { faker } from '@faker-js/faker'; + import { TreeNode } from 'primeng/api'; -export const files: TreeNode[] = [ - { - label: 'Documents', - data: 'Documents Folder', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { - label: 'Work', - data: 'Work Folder', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { - label: 'Expenses.doc', - icon: 'pi pi-file', - data: 'Expenses Document' - }, - { label: 'Resume.doc', icon: 'pi pi-file', data: 'Resume Document' } - ] - }, - { - label: 'Home', - data: 'Home Folder', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { - label: 'Invoices.txt', - icon: 'pi pi-file', - data: 'Invoices for this month' - } - ] - } - ] - }, - { - label: 'Pictures', - data: 'Pictures Folder', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { label: 'barcelona.jpg', icon: 'pi pi-image', data: 'Barcelona Photo' }, - { label: 'logo.jpg', icon: 'pi pi-image', data: 'PrimeFaces Logo' }, - { label: 'primeui.png', icon: 'pi pi-image', data: 'PrimeUI Logo' } - ] - }, - { - label: 'Movies', - data: 'Movies Folder', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { - label: 'Al Pacino', - data: 'Pacino Movies', - children: [ - { label: 'Scarface', icon: 'pi pi-video', data: 'Scarface Movie' }, - { label: 'Serpico', icon: 'pi pi-video', data: 'Serpico Movie' } - ] - }, - { - label: 'Robert De Niro', - data: 'De Niro Movies', - children: [ - { - label: 'Goodfellas', - icon: 'pi pi-video', - data: 'Goodfellas Movie' - }, - { - label: 'Untouchables', - icon: 'pi pi-video', - data: 'Untouchables Movie' - } - ] - } - ] - } -]; +export const generateFakeChildrenNode = (): TreeNode => ({ + key: faker.string.uuid(), + label: faker.lorem.word(), + icon: 'pi pi-file' +}); + +export const generateFakeNode = (): TreeNode => ({ + key: faker.string.uuid(), + label: faker.lorem.word(), + expandedIcon: 'pi pi-folder-open', + collapsedIcon: 'pi pi-folder', + children: faker.helpers.multiple(generateFakeChildrenNode, { + count: faker.number.int({ max: 5, min: 1 }) + }) +}); + +export const generateFakeTree = (count = 10) => faker.helpers.multiple(generateFakeNode, { count }); diff --git a/core-web/karma.conf.js b/core-web/karma.conf.js index 1a98cbf72910..1f86084604fb 100644 --- a/core-web/karma.conf.js +++ b/core-web/karma.conf.js @@ -16,7 +16,7 @@ module.exports = () => { require('@angular-devkit/build-angular/plugins/karma') ], client: { - clearContext: false, // leave Jasmine Spec Runner output visible in browser + clearContext: true, // leave Jasmine Spec Runner output visible in browser captureConsole: true }, coverageReporter: { diff --git a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts index 8a03dd7d95be..5678f4132243 100644 --- a/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts +++ b/core-web/libs/block-editor/src/lib/components/dot-block-editor/dot-block-editor.component.stories.ts @@ -10,19 +10,17 @@ import { OrderListModule } from 'primeng/orderlist'; import { debounceTime, delay, tap } from 'rxjs/operators'; +import { BlockEditorModule, DotBlockEditorComponent } from '@dotcms/block-editor'; import { + DotAiService, DotContentSearchService, DotMessageService, DotPropertiesService, DotUploadFileService, - FileStatus, - DotAiService + FileStatus } from '@dotcms/data-access'; import { DotSpinnerModule } from '@dotcms/ui'; -import { DotBlockEditorComponent } from './dot-block-editor.component'; - -import { BlockEditorModule } from '../../block-editor.module'; import { AssetFormComponent, BubbleLinkFormComponent, diff --git a/core-web/libs/block-editor/tsconfig.json b/core-web/libs/block-editor/tsconfig.json index b38aac055b5c..bd44055f0945 100644 --- a/core-web/libs/block-editor/tsconfig.json +++ b/core-web/libs/block-editor/tsconfig.json @@ -8,9 +8,6 @@ }, { "path": "./tsconfig.spec.json" - }, - { - "path": "./.storybook/tsconfig.json" } ], "compilerOptions": { diff --git a/core-web/libs/dot-rules/src/lib/components/dot-autocomplete-tags/dot-autocomplete-tags.component.ts b/core-web/libs/dot-rules/src/lib/components/dot-autocomplete-tags/dot-autocomplete-tags.component.ts index 932adbeded3e..a7a6ff705661 100644 --- a/core-web/libs/dot-rules/src/lib/components/dot-autocomplete-tags/dot-autocomplete-tags.component.ts +++ b/core-web/libs/dot-rules/src/lib/components/dot-autocomplete-tags/dot-autocomplete-tags.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { SelectItem } from 'primeng/api'; +import { AutoCompleteSelectEvent, AutoCompleteUnselectEvent } from 'primeng/autocomplete'; @Component({ selector: 'dot-autocomplete-tags', @@ -37,13 +38,14 @@ export class DotAutocompleteTagsComponent implements OnInit { event.currentTarget.value = null; } } - addItem(item: any) { + addItem(event: AutoCompleteSelectEvent) { + const { value } = event; this.value.splice(-1, 1); - this.value.push(item.value); + this.value.push(value); this.onChange.emit(this.value); } - removeItem(_item: any) { + removeItem(_event: AutoCompleteUnselectEvent) { this.onChange.emit(this.value); } } diff --git a/core-web/libs/dotcms-models/src/lib/dot-action-menu-item.model.ts b/core-web/libs/dotcms-models/src/lib/dot-action-menu-item.model.ts index e1fae8cdc786..cd7b43bb9fe6 100644 --- a/core-web/libs/dotcms-models/src/lib/dot-action-menu-item.model.ts +++ b/core-web/libs/dotcms-models/src/lib/dot-action-menu-item.model.ts @@ -1,6 +1,15 @@ -import { MenuItem } from 'primeng/api'; +import { MenuItem, MenuItemCommandEvent } from 'primeng/api'; -export interface DotActionMenuItem { +export interface CustomMenuItem extends Omit { + command?(event?: T): void; +} + +export interface DotActionMenuItem { shouldShow?: (x?: Record) => boolean; - menuItem: MenuItem; + menuItem: CustomMenuItem; +} + +export interface DotMenuItemCommandEvent extends MenuItemCommandEvent { + inode: string; + categoryName: string; } diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_breadcrumb.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_breadcrumb.scss index 8829a9aa4e08..73e717e8a0f3 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_breadcrumb.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_breadcrumb.scss @@ -1,38 +1,47 @@ @use "variables" as *; -.p-breadcrumb ul li .p-menuitem-link { - transition: none; - border-radius: $border-radius-xs; -} - -.p-breadcrumb ul li .p-menuitem-link:focus { - outline: 0 none; - outline-offset: 0; - box-shadow: none; -} - -.p-breadcrumb ul li .p-menuitem-link[href] .p-menuitem-text { - font-size: $font-size-default; - - &:hover { - color: $color-palette-primary; - text-decoration: underline; +.p-breadcrumb { + border: 0 none; + padding: $spacing-3 0; + + .p-breadcrumb-list { + li { + .p-menuitem-link { + transition: none; + .p-menuitem-text { + color: $color-palette-gray-700; + font-size: $font-size-default; + cursor: pointer; + &[href] { + color: $black; + &:hover { + color: $color-palette-primary; + text-decoration: underline; + } + } + } + .p-menuitem-icon { + color: $black; + } + &:focus { + outline: 0 none; + outline-offset: 0; + box-shadow: none; + } + } + &.p-menuitem-separator { + margin: 0 $spacing-1 0 $spacing-1; + color: $black; + } + + &:last-child { + .p-menuitem-text { + color: $color-palette-gray-700; + } + .p-menuitem-icon { + color: $color-palette-gray-700; + } + } + } } } - -.p-breadcrumb ul li .p-menuitem-link .p-menuitem-text { - color: $color-palette-gray-700; -} - -.p-breadcrumb ul li .p-menuitem-link .p-menuitem-icon { - color: $black; -} - -.p-breadcrumb ul li.p-breadcrumb-chevron { - margin: 0 $spacing-1 0 $spacing-1; - color: $black; -} - -.p-breadcrumb ul li:last-child .p-menuitem-icon { - color: $black; -} diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_paginator.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_paginator.scss index 860637b62711..a27c61c3da5b 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_paginator.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_paginator.scss @@ -7,7 +7,6 @@ border-width: 0; padding: 0.375rem $spacing-2; border-radius: $border-radius-xs; - justify-content: flex-end; } .p-paginator .p-paginator-first, diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tree.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tree.scss index 6c43a1fb7242..1461a513433d 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tree.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tree.scss @@ -6,170 +6,166 @@ color: $black; padding: $spacing-2; border-radius: $border-radius-xs; -} - -.p-tree .p-tree-container .p-treenode { - padding: $spacing-0; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content { - border-radius: $border-radius-xs; - transition: none; - padding: 0.571rem; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content .p-tree-toggler { - margin-right: $spacing-1; - width: $spacing-5; - height: $spacing-5; - color: $black; - border: 0 none; - background: transparent; - border-radius: 50%; - transition: - background-color 0.2s, - color 0.2s, - box-shadow 0.2s; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content .p-tree-toggler:enabled:hover { - color: $text-color-hover; - border-color: transparent; - background: $bg-hover; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content .p-tree-toggler:focus { - outline: 0 none; - outline-offset: 0; - box-shadow: none; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content .p-treenode-icon { - margin-right: $spacing-1; - color: $black; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content .p-checkbox { - margin-right: $spacing-1; -} - -.p-tree - .p-tree-container - .p-treenode - .p-treenode-content - .p-checkbox - .p-indeterminate - .p-checkbox-icon { - color: $black; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content:focus { - outline: 0 none; - outline-offset: 0; - box-shadow: none; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content.p-highlight { - background: $bg-highlight; - color: $text-color-highlight; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content.p-highlight .p-tree-toggler, -.p-tree .p-tree-container .p-treenode .p-treenode-content.p-highlight .p-treenode-icon { - color: $text-color-highlight; -} - -.p-tree - .p-tree-container - .p-treenode - .p-treenode-content.p-treenode-selectable:not(.p-highlight):hover { - background: $bg-hover; - color: $text-color-hover; -} - -.p-tree .p-tree-container .p-treenode .p-treenode-content.p-treenode-dragover { - background: rgba(0, 0, 0, 0.04); - color: $black; -} - -.p-tree .p-tree-filter-container { - margin-bottom: $spacing-1; -} - -.p-tree .p-tree-filter-container .p-tree-filter { - width: 100%; - padding-right: $spacing-6; -} - -.p-tree .p-tree-filter-container .p-tree-filter-icon { - right: $spacing-2; - color: $black; -} - -.p-tree .p-treenode-children { - padding: 0 0 0 $spacing-3; -} - -.p-tree .p-tree-loading-icon { - font-size: $icon-xl; -} - -.p-tree .p-treenode-droppoint.p-treenode-droppoint-active { - background-color: rgba(50, 65, 145, 0.12); -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content { - border-radius: $border-radius-xs; - border: 1px solid $input-border-color; - background-color: $white; - color: $black; - padding: 0.571rem; - transition: none; -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content.p-highlight { - background-color: $bg-highlight; - color: $text-color-highlight; -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content.p-highlight .p-treenode-icon { - color: $text-color-highlight; -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content .p-tree-toggler { - margin-right: $spacing-1; -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content .p-treenode-icon { - color: $black; - margin-right: $spacing-1; -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content .p-checkbox { - margin-right: $spacing-1; -} - -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content .p-checkbox .p-checkbox-icon { - color: $color-palette-primary; -} - -.p-tree.p-tree-horizontal - .p-treenode - .p-treenode-content - .p-treenode-label:not(.p-highlight):hover { - background-color: inherit; - color: inherit; -} - -.p-tree.p-tree-horizontal - .p-treenode - .p-treenode-content.p-treenode-selectable:not(.p-highlight):hover { - background: $bg-hover; - color: $text-color-hover; -} -.p-tree.p-tree-horizontal .p-treenode .p-treenode-content:focus { - outline: 0 none; - outline-offset: 0; - box-shadow: none; + .p-tree-container { + .p-treenode { + padding: 0; + margin: 0; + outline: 0; + + .p-treenode-content { + border-radius: $border-radius-xs; + transition: none; + padding: $spacing-0 $spacing-1; + + .p-tree-toggler { + margin-right: $spacing-1; + width: $spacing-5; + height: $spacing-5; + color: $black; + border: 0 none; + background: transparent; + border-radius: 50%; + transition: + background-color 0.2s, + color 0.2s, + box-shadow 0.2s; + + &:enabled:hover { + color: $text-color-hover; + border-color: transparent; + background: $bg-hover; + } + + &:focus { + outline: 0 none; + outline-offset: 0; + box-shadow: none; + } + } + + .p-treenode-icon { + margin-right: $spacing-1; + color: $black; + } + + .p-checkbox { + margin-right: $spacing-1; + + .p-indeterminate { + .p-checkbox-icon { + color: $black; + } + } + } + + &:focus { + outline: 0 none; + outline-offset: 0; + box-shadow: none; + } + + &.p-highlight { + background: $bg-highlight; + color: $text-color-highlight; + + .p-tree-toggler, + .p-treenode-icon { + color: $text-color-highlight; + } + } + + &.p-treenode-selectable:not(.p-highlight):hover { + background: $bg-hover; + color: $text-color-hover; + } + + &.p-treenode-dragover { + background: rgba(0, 0, 0, 0.04); + color: $black; + } + } + } + + .p-tree-filter-container { + margin-bottom: $spacing-1; + + .p-tree-filter { + width: 100%; + padding-right: $spacing-6; + } + + .p-tree-filter-icon { + right: $spacing-2; + color: $black; + } + } + + .p-treenode-children { + padding: 0 0 0 $spacing-3; + } + + .p-tree-loading-icon { + font-size: $icon-xl; + } + + .p-treenode-droppoint.p-treenode-droppoint-active { + background-color: rgba(50, 65, 145, 0.12); + } + } + + &.p-tree-horizontal { + .p-treenode { + .p-treenode-content { + border-radius: $border-radius-xs; + border: 1px solid $input-border-color; + background-color: $white; + color: $black; + padding: 0.571rem; + transition: none; + + &.p-highlight { + background-color: $bg-highlight; + color: $text-color-highlight; + + .p-treenode-icon { + color: $text-color-highlight; + } + } + + .p-tree-toggler { + margin-right: $spacing-1; + } + + .p-treenode-icon { + color: $black; + margin-right: $spacing-1; + } + + .p-checkbox { + margin-right: $spacing-1; + + .p-checkbox-icon { + color: $color-palette-primary; + } + } + + .p-treenode-label:not(.p-highlight):hover { + background-color: inherit; + color: inherit; + } + + &.p-treenode-selectable:not(.p-highlight):hover { + background: $bg-hover; + color: $text-color-hover; + } + + &:focus { + outline: 0 none; + outline-offset: 0; + box-shadow: none; + } + } + } + } } diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_button.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_button.scss index 69ff804b13a0..cf8164cecc3b 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_button.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_button.scss @@ -157,3 +157,32 @@ a.p-button.p-button-text { .p-button-rounded { border-radius: 50%; } + +.p-buttonset { + .p-button { + margin: 0; + + &:not(:last-child) { + border-right: 0 none; + } + + &:not(:first-of-type):not(:last-of-type) { + border-radius: 0; + } + + &:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:last-of-type { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &:focus { + position: relative; + z-index: 1; + } + } +} diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_splitbutton.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_splitbutton.scss index 1fed8e73fd86..53a2dcbba897 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_splitbutton.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/buttons/_splitbutton.scss @@ -67,6 +67,7 @@ .p-splitbutton-defaultbutton:enabled, .p-splitbutton-menubutton:enabled { @extend #outlined-primary-severity; + color: $color-palette-primary; } &.p-button-sm { @@ -84,6 +85,7 @@ .p-splitbutton-defaultbutton:enabled, .p-splitbutton-menubutton:enabled { @extend #outlined-secondary-severity; + color: $color-palette-secondary; } &.p-button-sm { diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss index 7f28756409ef..d12594558230 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss @@ -27,6 +27,7 @@ p-autocomplete.p-inputwrapper-focus { .p-autocomplete.p-component.p-autocomplete-dd.p-autocomplete-multiple:has(.p-icon-wrapper) { grid-template-columns: max-content 1fr auto; } + .p-autocomplete { @extend #form-field-extend; height: auto; @@ -142,6 +143,10 @@ p-autocomplete.ng-dirty.ng-invalid > .p-autocomplete > .p-inputtext { &.p-highlight { @extend #field-panel-item-highlight; } + + &.p-focus { + background-color: $color-palette-primary-300; + } } } } diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_calendar.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_calendar.scss index 40a86fb1e770..89f2bfa73b5a 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_calendar.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_calendar.scss @@ -42,6 +42,8 @@ height: auto; aspect-ratio: 1/1; background-color: $color-palette-gray-200; + color: $color-palette-primary; + border-radius: 0 $border-radius-sm $border-radius-sm 0; .pi { width: $icon-lg-box; @@ -139,6 +141,7 @@ height: $field-height-md; width: $field-height-md; border-radius: 50%; + color: $color-palette-primary-500; @extend #outlined-primary-severity; } @@ -156,6 +159,7 @@ display: flex; gap: $spacing-1; } + td, th { border-spacing: 0; @@ -190,6 +194,15 @@ gap: $spacing-0; border-top: 1px solid $color-palette-gray-300; padding: $spacing-3; + + button { + width: $icon-lg-box; + height: auto; + aspect-ratio: 1/1; + font-size: $icon-lg; + color: $color-palette-primary-500; + } + span { font-size: $font-size-md; font-weight: $font-weight-regular-bold; @@ -199,15 +212,8 @@ justify-content: center; align-content: center; flex-wrap: wrap; - - &.pi { - width: $icon-lg-box; - height: auto; - aspect-ratio: 1/1; - font-size: $icon-lg; - color: $color-palette-primary-500; - } } + button { border-radius: $border-radius-sm; @@ -243,10 +249,11 @@ p-calendar.p-calendar-clearable { .p-calendar-clear-icon { margin-top: -0.438rem; - color: $color-palette-gray-500; + color: $color-palette-primary; width: $icon-lg-box; font-size: $icon-lg; right: 44px; + &:hover { color: $color-palette-primary-600; } diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_dropdown.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_dropdown.scss index 659d9177f978..5aad1eb60e87 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_dropdown.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_dropdown.scss @@ -41,6 +41,12 @@ p-dropdown.ng-dirty.ng-invalid > .p-dropdown { .p-dropdown-label { padding-right: $spacing-1; @include truncate-text; + + &:focus, + &:enabled:focus { + outline: 0 none; + box-shadow: none; + } } &:has(.p-dropdown-clear-icon) { diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_treeselect.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_treeselect.scss index a3f71f762826..ae7e37647d59 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_treeselect.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_treeselect.scss @@ -37,6 +37,7 @@ .p-treeselect-label { padding: $spacing-1 $spacing-2; + line-height: $spacing-4; transition: background-color $basic-speed, border-color $basic-speed, diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts index 2a57e3a7ae44..b6fb0dedc535 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { Table, TableModule } from 'primeng/table'; @@ -113,7 +114,10 @@ describe('DotCategoryFieldSearchListComponent', () => { const itemCheckedSpy = jest.spyOn(spectator.component.itemChecked, 'emit'); spectator.detectChanges(); - spectator.triggerEventHandler(Table, 'onHeaderCheckboxToggle', { checked: true }); + spectator.triggerEventHandler(Table, 'onHeaderCheckboxToggle', { + originalEvent: createFakeEvent('click'), + checked: true + }); expect(itemCheckedSpy).toHaveBeenCalledWith(CATEGORY_MOCK_TRANSFORMED); }); @@ -121,8 +125,14 @@ describe('DotCategoryFieldSearchListComponent', () => { it('should emit $removeItem event with all keys when header checkbox is unselected', () => { const removeItemSpy = jest.spyOn(spectator.component.removeItem, 'emit'); spectator.detectChanges(); - spectator.triggerEventHandler(Table, 'onHeaderCheckboxToggle', { checked: true }); - spectator.triggerEventHandler(Table, 'onHeaderCheckboxToggle', { checked: false }); + spectator.triggerEventHandler(Table, 'onHeaderCheckboxToggle', { + originalEvent: createFakeEvent('click'), + checked: true + }); + spectator.triggerEventHandler(Table, 'onHeaderCheckboxToggle', { + originalEvent: createFakeEvent('click'), + checked: false + }); const allKeys = CATEGORY_MOCK_TRANSFORMED.map((category) => category.key); expect(removeItemSpy).toHaveBeenCalledWith(allKeys); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.spec.ts index 29f9ea3e0ad8..c3bbfda63de4 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.spec.ts @@ -1,7 +1,7 @@ -import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; +import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { DotMessageService } from '@dotcms/data-access'; -import { DotMessagePipe } from '@dotcms/utils-testing'; +import { DotMessagePipe, MockDotMessageService } from '@dotcms/utils-testing'; import { DotCategoryFieldSelectedComponent } from './dot-category-field-selected.component'; @@ -12,7 +12,14 @@ describe('DotCategoryFieldSelectedComponent', () => { const createComponent = createComponentFactory({ component: DotCategoryFieldSelectedComponent, imports: [DotMessagePipe], - providers: [mockProvider(DotMessageService)] + providers: [ + { + provide: DotMessageService, + useValue: new MockDotMessageService({ + 'edit.content.category-field.category.root-name': 'Root' + }) + } + ] }); beforeEach(() => { @@ -34,10 +41,10 @@ describe('DotCategoryFieldSelectedComponent', () => { const title = item.querySelector('[data-testId="category-title"]'); const path = item.querySelector('[data-testId="category-path"]'); - expect(title).toContainText(CATEGORY_MOCK_TRANSFORMED[index].value); - expect(path?.getAttribute('ng-reflect-text')).toBe( - CATEGORY_MOCK_TRANSFORMED[index].path - ); + const category = CATEGORY_MOCK_TRANSFORMED[index]; + + expect(title).toContainText(category.value); + expect(path).toContainText(category.path); }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.ts index e27dbe389480..f6471a407603 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-selected/dot-category-field-selected.component.ts @@ -1,5 +1,5 @@ import { animate, state, style, transition, trigger } from '@angular/animations'; -import { ChangeDetectionStrategy, Component, EventEmitter, input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { ChipModule } from 'primeng/chip'; @@ -53,6 +53,5 @@ export class DotCategoryFieldSelectedComponent { * Represents an EventEmitter used for removing items. Emit the key * of the category */ - @Output() - removeItem = new EventEmitter(); + removeItem = output(); } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.html deleted file mode 100644 index da908d6f2fc9..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.html +++ /dev/null @@ -1,8 +0,0 @@ -@for (option of options; track $index) { - -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.spec.ts index e4ed8fd15aac..d6511edfd9c5 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.spec.ts @@ -99,10 +99,9 @@ describe('DotEditContentCheckboxFieldComponent', () => { spectator.detectComponentChanges(); spectator.queryAll(Checkbox).forEach((checkbox) => { - expect(spectator.query(`label[for="${checkbox.inputId}"]`)).toBeTruthy(); - expect(spectator.query(`label[for="${checkbox.inputId}"]`).textContent).toEqual( - checkbox.label - ); + const selector = `label[for="${checkbox.inputId}"]`; + expect(spectator.query(selector)).toBeTruthy(); + expect(spectator.query(selector).textContent).toEqual(` ${checkbox.label}`); }); }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.ts index c8a345e94948..6d3e8eefad89 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-checkbox-field/dot-edit-content-checkbox-field.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; import { ControlContainer, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CheckboxModule } from 'primeng/checkbox'; @@ -10,33 +10,42 @@ import { getSingleSelectableFieldOptions } from '../../utils/functions.util'; selector: 'dot-edit-content-checkbox-field', standalone: true, imports: [CheckboxModule, ReactiveFormsModule, FormsModule], - templateUrl: './dot-edit-content-checkbox-field.component.html', - styleUrls: ['./dot-edit-content-checkbox-field.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) } - ] + ], + template: ` + @for (option of $options(); track $index) { + + } + `, + styleUrls: ['./dot-edit-content-checkbox-field.component.scss'] }) -export class DotEditContentCheckboxFieldComponent implements OnInit { - @Input() field!: DotCMSContentTypeField; +export class DotEditContentCheckboxFieldComponent { private readonly controlContainer = inject(ControlContainer); - options = []; - ngOnInit() { - this.options = getSingleSelectableFieldOptions( - this.field.values || '', - this.field.dataType - ); - } + $field = input.required({ alias: 'field' }); + $options = computed(() => { + const field = this.$field(); + + return getSingleSelectableFieldOptions(field.values || '', field.dataType); + }); /** * Returns the form control for the select field. * @returns {AbstractControl} The form control for the select field. */ get formControl() { - return this.controlContainer.control.get(this.field.variable) as FormControl; + const field = this.$field(); + + return this.controlContainer.control.get(field.variable) as FormControl; } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.css b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.css deleted file mode 100644 index 54a763ee8b6d..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.css +++ /dev/null @@ -1,95 +0,0 @@ -.p-treeselect { - display: inline-flex; - cursor: pointer; - position: relative; - user-select: none; -} - -.p-treeselect-trigger { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.p-treeselect-label-container { - overflow: hidden; - flex: 1 1 auto; - cursor: pointer; -} - -.p-treeselect-label { - display: block; - white-space: nowrap; - cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; -} - -.p-treeselect-label-empty { - overflow: hidden; - visibility: hidden; -} - -.p-treeselect-token { - cursor: default; - display: inline-flex; - align-items: center; - flex: 0 0 auto; -} - -.p-treeselect-items-wrapper { - overflow: auto; -} - -.p-treeselect-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.p-treeselect-filter-container { - position: relative; - flex: 1 1 auto; -} - -.p-treeselect-filter-container .p-treeselect-filter-icon { - position: absolute; - top: 50%; - margin-top: -0.5rem; -} - -.p-treeselect-filter-container .p-inputtext { - width: 100%; -} - -.p-treeselect-close { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - overflow: hidden; - position: relative; - margin-left: auto; -} - -.p-treeselect-clear-icon { - position: absolute; - top: 50%; - margin-top: -0.5rem; -} - -.p-fluid .p-treeselect { - display: flex; -} - -.p-treeselect-clear-icon { - position: absolute; - top: 50%; - margin-top: -0.5rem; - cursor: pointer; -} - -.p-treeselect-clearable { - position: relative; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.ts deleted file mode 100644 index 20b79944c17c..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/componentes/treeselect.component.ts +++ /dev/null @@ -1,868 +0,0 @@ -/* eslint-disable no-fallthrough */ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @angular-eslint/no-output-on-prefix */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @angular-eslint/component-selector */ -/* eslint-disable @angular-eslint/component-class-suffix */ -/* eslint-disable @angular-eslint/no-host-metadata-property */ -import { AnimationEvent } from '@angular/animations'; -import { CommonModule } from '@angular/common'; -import { - AfterContentInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ContentChildren, - ElementRef, - EventEmitter, - forwardRef, - Input, - NgModule, - Output, - QueryList, - TemplateRef, - ViewChild, - ViewEncapsulation, - booleanAttribute, - OnInit -} from '@angular/core'; -import { NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { - OverlayOptions, - OverlayService, - PrimeNGConfig, - PrimeTemplate, - SharedModule, - TreeNode -} from 'primeng/api'; -import { DomHandler } from 'primeng/dom'; -import { ChevronDownIcon } from 'primeng/icons/chevrondown'; -import { SearchIcon } from 'primeng/icons/search'; -import { TimesIcon } from 'primeng/icons/times'; -import { Overlay, OverlayModule } from 'primeng/overlay'; -import { RippleModule } from 'primeng/ripple'; -import { ScrollerOptions } from 'primeng/scroller'; -import { Tree, TreeModule } from 'primeng/tree'; -import { ObjectUtils } from 'primeng/utils'; - -export const TREESELECT_VALUE_ACCESSOR = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TreeSelect), - multi: true -}; - -@Component({ - selector: 'p-treeSelect', - template: ` -
-
- -
-
-
- @if (valueTemplate) { - - } @else { - @if (display === 'comma') { - {{ label || 'empty' }} - } @else { - @for (node of value; track node) { -
- {{ node.label }} -
- } - @if (emptyValue) { - {{ placeholder || 'empty' }} - } - } - } -
- @if (checkValue() && !disabled && showClear) { - @if (!clearIconTemplate) { - - } - @if (clearIconTemplate) { - - - - } - } -
-
- @if (!triggerIconTemplate) { - - } - @if (triggerIconTemplate) { - - - - } -
- - -
- - @if (filter) { -
-
- - @if (!filterIconTemplate) { - - } - @if (filterIconTemplate) { - - - - } -
- -
- } -
- - @if (emptyTemplate) { - - - - } - @if (itemTogglerIconTemplate; as expanded) { - - - - } - @if (itemCheckboxIconTemplate) { - - - - } - @if (itemLoadingIconTemplate) { - - - - } - -
- -
-
-
-
- `, - styleUrls: ['./treeselect.component.css'], - host: { - class: 'p-element p-inputwrapper', - '[class.p-inputwrapper-filled]': '!emptyValue', - '[class.p-inputwrapper-focus]': 'focused || overlayVisible', - '[class.p-treeselect-clearable]': 'showClear && !disabled' - }, - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [TREESELECT_VALUE_ACCESSOR], - encapsulation: ViewEncapsulation.None -}) -export class TreeSelect implements AfterContentInit, OnInit { - @Input() type = 'button'; - - @Input() inputId: string; - - @Input() scrollHeight = '400px'; - - @Input() disabled: boolean; - - @Input() metaKeySelection = true; - - @Input() display = 'comma'; - - @Input() selectionMode = 'single'; - - @Input() tabindex: string; - - @Input() ariaLabelledBy: string; - - @Input() placeholder: string; - - @Input() panelClass: string; - - @Input() panelStyle: any; - - @Input() panelStyleClass: string; - - @Input() containerStyle: object; - - @Input() containerStyleClass: string; - - @Input() labelStyle: object; - - @Input() labelStyleClass: string; - - @Input() overlayOptions: OverlayOptions; - - @Input() emptyMessage = ''; - - @Input() appendTo: any; - - @Input() filter = false; - - @Input() filterBy = 'label'; - - @Input() filterMode = 'lenient'; - - @Input() filterPlaceholder: string; - - @Input() filterLocale: string; - - @Input() filterInputAutoFocus = true; - - @Input() propagateSelectionDown = true; - - @Input() propagateSelectionUp = true; - - @Input() showClear = false; - - @Input() resetFilterOnHide = true; - - /** - * Whether the data should be loaded on demand during scroll. - * @group Props - */ - @Input() virtualScroll: boolean | undefined; - /** - * Height of an item in the list for VirtualScrolling. - * @group Props - */ - @Input() virtualScrollItemSize: number | undefined; - /** - * Whether to use the scroller feature. The properties of scroller component can be used like an object in it. - * @group Props - */ - @Input() virtualScrollOptions: ScrollerOptions | undefined; - /** - * Displays a loader to indicate data load is in progress. - * @group Props - */ - @Input({ transform: booleanAttribute }) loading: boolean | undefined; - - @Input() get options(): any[] { - return this._options; - } - set options(options) { - this._options = options; - this.updateTreeState(); - } - - @ContentChildren(PrimeTemplate) templates: QueryList; - - @ViewChild('container') containerEl: ElementRef; - - @ViewChild('focusInput') focusInput: ElementRef; - - @ViewChild('filter') filterViewChild: ElementRef; - - @ViewChild('tree') treeViewChild: Tree; - - @ViewChild('panel') panelEl: ElementRef; - - @ViewChild('overlay') overlayViewChild: Overlay; - - @Output() onNodeExpand: EventEmitter = new EventEmitter(); - - @Output() onNodeCollapse: EventEmitter = new EventEmitter(); - - @Output() onShow: EventEmitter = new EventEmitter(); - - @Output() onHide: EventEmitter = new EventEmitter(); - - @Output() onClear: EventEmitter = new EventEmitter(); - - @Output() onFilter: EventEmitter = new EventEmitter(); - - @Output() onNodeUnselect: EventEmitter = new EventEmitter(); - - @Output() onNodeSelect: EventEmitter = new EventEmitter(); - - /* @deprecated */ - _showTransitionOptions: string; - @Input() get showTransitionOptions(): string { - return this._showTransitionOptions; - } - set showTransitionOptions(val: string) { - this._showTransitionOptions = val; - console.warn( - 'The showTransitionOptions property is deprecated since v14.2.0, use overlayOptions property instead.' - ); - } - - /* @deprecated */ - _hideTransitionOptions: string; - @Input() get hideTransitionOptions(): string { - return this._hideTransitionOptions; - } - set hideTransitionOptions(val: string) { - this._hideTransitionOptions = val; - console.warn( - 'The hideTransitionOptions property is deprecated since v14.2.0, use overlayOptions property instead.' - ); - } - - public filteredNodes: TreeNode[]; - - filterValue: string = null; - - serializedValue: any[]; - - valueTemplate: TemplateRef; - - headerTemplate: TemplateRef; - - emptyTemplate: TemplateRef; - - footerTemplate: TemplateRef; - - clearIconTemplate: TemplateRef; - - triggerIconTemplate: TemplateRef; - - filterIconTemplate: TemplateRef; - - closeIconTemplate: TemplateRef; - - itemTogglerIconTemplate: TemplateRef; - - itemCheckboxIconTemplate: TemplateRef; - - itemLoadingIconTemplate: TemplateRef; - - focused: boolean; - - overlayVisible: boolean; - - selfChange: boolean; - - value; - - expandedNodes: any[] = []; - - _options: any[]; - - public templateMap: any; - - onModelChange: Function = () => {}; - - onModelTouched: Function = () => {}; - - constructor( - public config: PrimeNGConfig, - public cd: ChangeDetectorRef, - public el: ElementRef, - public overlayService: OverlayService - ) {} - - ngOnInit() { - this.updateTreeState(); - } - - ngAfterContentInit() { - if (this.templates.length) { - this.templateMap = {}; - } - - this.templates.forEach((item) => { - switch (item.getType()) { - case 'value': - this.valueTemplate = item.template; - break; - - case 'header': - this.headerTemplate = item.template; - break; - - case 'empty': - this.emptyTemplate = item.template; - break; - - case 'footer': - this.footerTemplate = item.template; - break; - - case 'clearicon': - this.clearIconTemplate = item.template; - break; - - case 'triggericon': - this.triggerIconTemplate = item.template; - break; - - case 'filtericon': - this.filterIconTemplate = item.template; - break; - - case 'closeicon': - this.closeIconTemplate = item.template; - break; - - case 'itemtogglericon': - this.itemTogglerIconTemplate = item.template; - break; - - case 'itemcheckboxicon': - this.itemCheckboxIconTemplate = item.template; - break; - - case 'itemloadingicon': - this.itemLoadingIconTemplate = item.template; - break; - - default: //TODO: @deprecated Used "value" template instead - if (item.name) this.templateMap[item.name] = item.template; - else this.valueTemplate = item.template; - break; - } - }); - } - - onOverlayAnimationStart(event: AnimationEvent) { - switch (event.toState) { - case 'visible': - if (this.filter) { - ObjectUtils.isNotEmpty(this.filterValue) && - this.treeViewChild?._filter(this.filterValue); - this.filterInputAutoFocus && this.filterViewChild.nativeElement.focus(); - } - - break; - } - } - - onSelectionChange(event) { - this.value = event; - this.onModelChange(this.value); - this.cd.markForCheck(); - } - - onClick(event) { - if (this.disabled) { - return; - } - - if ( - !this.overlayViewChild?.el?.nativeElement?.contains(event.target) && - !DomHandler.hasClass(event.target, 'p-treeselect-close') - ) { - if (this.overlayVisible) { - this.hide(); - } else { - this.show(); - } - - this.focusInput.nativeElement.focus(); - } - } - - onKeyDown(event) { - switch (event.which) { - //down - case 40: - if (!this.overlayVisible && event.altKey) { - this.show(); - event.preventDefault(); - } else if (this.overlayVisible && this.panelEl?.nativeElement) { - const focusableElements = DomHandler.getFocusableElements( - this.panelEl.nativeElement - ); - - if (focusableElements && focusableElements.length > 0) { - focusableElements[0].focus(); - } - - event.preventDefault(); - } - - break; - - //space - case 32: - if (!this.overlayVisible) { - this.show(); - event.preventDefault(); - } - - break; - - //enter and escape - case 13: - - case 27: - if (this.overlayVisible) { - this.hide(); - event.preventDefault(); - } - - break; - - //tab - case 9: - this.hide(); - break; - - default: - break; - } - } - - onFilterInput(event) { - this.filterValue = event.target.value; - this.treeViewChild?._filter(this.filterValue); - this.onFilter.emit({ - originalEvent: event, - filteredValue: this.treeViewChild?.filteredNodes - }); - } - - show() { - this.overlayVisible = true; - } - - hide(event?: any) { - this.overlayVisible = false; - this.resetFilter(); - - this.onHide.emit(event); - this.cd.markForCheck(); - } - - clear(event) { - this.value = null; - this.resetExpandedNodes(); - this.resetPartialSelected(); - this.onModelChange(this.value); - this.onClear.emit(); - - event.stopPropagation(); - } - - checkValue() { - return this.value !== null && ObjectUtils.isNotEmpty(this.value); - } - - resetFilter() { - if (this.filter && !this.resetFilterOnHide) { - this.filteredNodes = this.treeViewChild?.filteredNodes; - this.treeViewChild?.resetFilter(); - } else { - this.filterValue = null; - } - } - - updateTreeState() { - if (this.value) { - const selectedNodes = this.selectionMode === 'single' ? [this.value] : [...this.value]; - this.resetExpandedNodes(); - this.resetPartialSelected(); - if (selectedNodes && this.options) { - this.updateTreeBranchState(null, null, selectedNodes); - } - } - } - - updateTreeBranchState(node, path, selectedNodes) { - if (node) { - if (this.isSelected(node)) { - this.expandPath(path); - selectedNodes.splice(selectedNodes.indexOf(node), 1); - } - - if (selectedNodes.length > 0 && node.children) { - for (const childNode of node.children) { - this.updateTreeBranchState(childNode, [...path, node], selectedNodes); - } - } - } else { - for (const childNode of this.options) { - this.updateTreeBranchState(childNode, [], selectedNodes); - } - } - } - - expandPath(expandedNodes) { - for (const node of expandedNodes) { - node.expanded = true; - } - - this.expandedNodes = [...expandedNodes]; - } - - nodeExpand(event) { - this.onNodeExpand.emit(event); - this.expandedNodes.push(event.node); - } - - nodeCollapse(event) { - this.onNodeCollapse.emit(event); - this.expandedNodes.splice(this.expandedNodes.indexOf(event.node), 1); - } - - resetExpandedNodes() { - for (const node of this.expandedNodes) { - node.expanded = false; - } - - this.expandedNodes = []; - } - - resetPartialSelected(nodes = this.options): void { - if (!nodes) { - return; - } - - for (const node of nodes) { - node.partialSelected = false; - - if (node.children && node.children?.length > 0) { - this.resetPartialSelected(node.children); - } - } - } - - findSelectedNodes(node, keys, selectedNodes) { - if (node) { - if (this.isSelected(node)) { - selectedNodes.push(node); - delete keys[node.key]; - } - - if (Object.keys(keys).length && node.children) { - for (const childNode of node.children) { - this.findSelectedNodes(childNode, keys, selectedNodes); - } - } - } else { - for (const childNode of this.options) { - this.findSelectedNodes(childNode, keys, selectedNodes); - } - } - } - - isSelected(node: TreeNode) { - return this.findIndexInSelection(node) != -1; - } - - findIndexInSelection(node: TreeNode) { - let index = -1; - - if (this.value) { - if (this.selectionMode === 'single') { - const areNodesEqual = - (this.value.key && this.value.key === node.key) || this.value == node; - index = areNodesEqual ? 0 : -1; - } else { - for (let i = 0; i < this.value.length; i++) { - const selectedNode = this.value[i]; - const areNodesEqual = - (selectedNode.key && selectedNode.key === node.key) || selectedNode == node; - if (areNodesEqual) { - index = i; - break; - } - } - } - } - - return index; - } - - onSelect(node) { - this.onNodeSelect.emit(node); - - if (this.selectionMode === 'single') { - this.hide(); - } - } - - onUnselect(node) { - this.onNodeUnselect.emit(node); - } - - onFocus() { - this.focused = true; - } - - onBlur() { - this.focused = false; - } - - writeValue(value: any): void { - this.value = value; - this.updateTreeState(); - this.cd.markForCheck(); - } - - registerOnChange(fn: Function): void { - this.onModelChange = fn; - } - - registerOnTouched(fn: Function): void { - this.onModelTouched = fn; - } - - setDisabledState(val: boolean): void { - this.disabled = val; - this.cd.markForCheck(); - } - - containerClass() { - return { - 'p-treeselect p-component p-inputwrapper': true, - 'p-treeselect-chip': this.display === 'chip', - 'p-disabled': this.disabled, - 'p-focus': this.focused - }; - } - - labelClass() { - return { - 'p-treeselect-label': true, - 'p-placeholder': this.label === this.placeholder, - 'p-treeselect-label-empty': !this.placeholder && this.emptyValue - }; - } - - get emptyValue() { - return !this.value || Object.keys(this.value).length === 0; - } - - get emptyOptions() { - return !this.options || this.options.length === 0; - } - - get label() { - const value = this.value || []; - - return value.length - ? value.map((node) => node.label).join(', ') - : this.selectionMode === 'single' && this.value - ? value.label - : this.placeholder; - } -} - -@NgModule({ - imports: [ - CommonModule, - OverlayModule, - RippleModule, - SharedModule, - TreeModule, - SearchIcon, - TimesIcon, - ChevronDownIcon - ], - exports: [TreeSelect, OverlayModule, SharedModule, TreeModule], - declarations: [TreeSelect] -}) -export class TreeSelectModule {} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.html index d6d2360184a5..02e6245f9ad4 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.html @@ -4,7 +4,7 @@ [formControl]="pathControl" [filter]="true" [options]="store.tree()" - [attr.aria-labelledby]="'field-' + field.variable" + [attr.aria-labelledby]="'field-' + $field().variable" [virtualScroll]="true" [virtualScrollItemSize]="50" [scrollHeight]="'450px'" diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.spec.ts index 21e5bba27f31..00858e0dd86c 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.spec.ts @@ -56,13 +56,8 @@ describe('DotEditContentHostFolderFieldComponent', () => { }); beforeEach(() => { - spectator = createComponent({ - props: { - field: { - ...HOST_FOLDER_TEXT_MOCK - } - } - }); + spectator = createComponent(); + spectator.setInput('field', { ...HOST_FOLDER_TEXT_MOCK }); store = spectator.inject(HostFolderFiledStore, true) as unknown as TypeMock; component = spectator.component; component.formControl.setValue(null); @@ -83,7 +78,7 @@ describe('DotEditContentHostFolderFieldComponent', () => { const options = component.store.tree(); expect(options).toBe(TREE_SELECT_SITES_MOCK); - expect(component.treeSelect.options).toBe(TREE_SELECT_SITES_MOCK); + expect(component.$treeSelect().options).toBe(TREE_SELECT_SITES_MOCK); expect(spyloadSites).toHaveBeenCalled(); }); @@ -95,9 +90,9 @@ describe('DotEditContentHostFolderFieldComponent', () => { await spectator.fixture.whenStable(); - const treeSelectHeight = spectator.component.treeSelect.scrollHeight; + const treeSelectHeight = spectator.component.$treeSelect().scrollHeight; const treeVirtualScrollHeight = - spectator.component.treeSelect.virtualScrollOptions.style['height']; + spectator.component.$treeSelect().virtualScrollOptions.style['height']; expect(treeSelectHeight).toBe(treeVirtualScrollHeight); }); @@ -111,7 +106,7 @@ describe('DotEditContentHostFolderFieldComponent', () => { spectator.detectChanges(); expect(component.formControl.value).toBe('demo.dotcms.com'); expect(component.pathControl.value.key).toBe(nodeSelected.key); - expect(component.treeSelect.value.label).toBe(nodeSelected.label); + expect(component.$treeSelect().value.label).toBe(nodeSelected.label); }); }); @@ -125,7 +120,7 @@ describe('DotEditContentHostFolderFieldComponent', () => { expect(component.formControl.value).toBe('demo.dotcms.com/level1/'); expect(component.pathControl.value.key).toBe(nodeSelected.key); - expect(component.treeSelect.value.label).toBe(nodeSelected.label); + expect(component.$treeSelect().value.label).toBe(nodeSelected.label); }); }); @@ -139,7 +134,7 @@ describe('DotEditContentHostFolderFieldComponent', () => { expect(component.formControl.value).toBe('demo.dotcms.com/level1/child1/'); expect(component.pathControl.value.key).toBe(nodeSelected.key); - expect(component.treeSelect.value.label).toBe(nodeSelected.label); + expect(component.$treeSelect().value.label).toBe(nodeSelected.label); }); }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.ts index 61c05659c073..2ea0d1dcc3a9 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/dot-edit-content-host-folder-field.component.ts @@ -2,17 +2,18 @@ import { NgClass } from '@angular/common'; import { ChangeDetectionStrategy, Component, - Input, OnInit, - ViewChild, effect, - inject + inject, + input, + viewChild } from '@angular/core'; import { ControlContainer, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TreeSelect, TreeSelectModule } from 'primeng/treeselect'; + import { DotCMSContentTypeField } from '@dotcms/dotcms-models'; -import { TreeSelect, TreeSelectModule } from './componentes/treeselect.component'; import { HostFolderFiledStore } from './store/host-folder-field.store'; import { TruncatePathPipe } from '../../pipes/truncate-path.pipe'; @@ -28,7 +29,6 @@ import { TruncatePathPipe } from '../../pipes/truncate-path.pipe'; standalone: true, imports: [TreeSelectModule, ReactiveFormsModule, TruncatePathPipe, NgClass], templateUrl: './dot-edit-content-host-folder-field.component.html', - styleUrls: ['./dot-edit-content-host-folder-field.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { @@ -39,8 +39,9 @@ import { TruncatePathPipe } from '../../pipes/truncate-path.pipe'; providers: [HostFolderFiledStore] }) export class DotEditContentHostFolderFieldComponent implements OnInit { - @Input() field!: DotCMSContentTypeField; - @ViewChild(TreeSelect) treeSelect!: TreeSelect; + $field = input.required({ alias: 'field' }); + $treeSelect = viewChild(TreeSelect); + readonly #controlContainer = inject(ControlContainer); readonly store = inject(HostFolderFiledStore); @@ -49,9 +50,10 @@ export class DotEditContentHostFolderFieldComponent implements OnInit { constructor() { effect(() => { this.store.nodeExpaned(); - if (this.treeSelect.treeViewChild) { - this.treeSelect.treeViewChild.updateSerializedValue(); - this.treeSelect.cd.detectChanges(); + const treeSelect = this.$treeSelect(); + if (treeSelect.treeViewChild) { + treeSelect.treeViewChild.updateSerializedValue(); + treeSelect.cd.detectChanges(); } }); effect(() => { @@ -75,6 +77,8 @@ export class DotEditContentHostFolderFieldComponent implements OnInit { } get formControl(): FormControl { - return this.#controlContainer.control.get(this.field.variable) as FormControl; + const field = this.$field(); + + return this.#controlContainer.control.get(field.variable) as FormControl; } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.html deleted file mode 100644 index 713233f83387..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.spec.ts index de198d217836..93486c510ebd 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.spec.ts @@ -49,11 +49,18 @@ describe('DotEditContentMultiselectFieldComponent', () => { spectator = createComponent(); }); - it('should render a options selected if the form have value', () => { + it('should render the options selected if the form have value', () => { spectator.setInput('field', MULTI_SELECT_FIELD_MOCK); spectator.detectComponentChanges(); - expect(spectator.query(MultiSelect).valuesAsString).toEqual('one, two'); + spectator.query(MultiSelect).show(); + spectator.detectChanges(); + + const options = spectator.component.$options(); + + spectator.queryAll('.p-multiselect-item').forEach((item, index) => { + expect(item.textContent).toBe(options[index].label); + }); }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.ts index 5a66836a93f4..3402fa5af22e 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-multi-select-field/dot-edit-content-multi-select-field.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { MultiSelectModule } from 'primeng/multiselect'; @@ -10,24 +10,26 @@ import { getSingleSelectableFieldOptions } from '../../utils/functions.util'; selector: 'dot-edit-content-multi-select-field', standalone: true, imports: [MultiSelectModule, ReactiveFormsModule], - templateUrl: './dot-edit-content-multi-select-field.component.html', - styleUrls: ['./dot-edit-content-multi-select-field.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) } - ] + ], + template: ` + + ` }) -export class DotEditContentMultiSelectFieldComponent implements OnInit { - @Input() field!: DotCMSContentTypeField; - options = []; +export class DotEditContentMultiSelectFieldComponent { + $field = input.required({ alias: 'field' }); + $options = computed(() => { + const field = this.$field(); - ngOnInit() { - this.options = getSingleSelectableFieldOptions( - this.field.values || '', - this.field.dataType - ); - } + return getSingleSelectableFieldOptions(field.values || '', field.dataType); + }); } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.html deleted file mode 100644 index cd3ff565fbfe..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.spec.ts index 1d0e1f2f0fef..c7d6662a5fa7 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.spec.ts @@ -33,7 +33,9 @@ describe('DotEditContentSelectFieldComponent', () => { it('should set the first value to the control if no value or defaultValue', () => { spectator.setInput('field', SELECT_FIELD_TEXT_MOCK); spectator.component.formControl.setValue(null); - spectator.detectChanges(); + + spectator.component.ngOnInit(); + expect(spectator.component.formControl.value).toEqual('Test,1'); const spanElement = spectator.query('span.p-dropdown-label'); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.ts index e94b1b73dcbe..45f0c8448d62 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-select-field/dot-edit-content-select-field.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, computed, inject, input } from '@angular/core'; import { AbstractControl, ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { DropdownModule } from 'primeng/dropdown'; @@ -12,30 +12,37 @@ import { getSingleSelectableFieldOptions } from '../../utils/functions.util'; selector: 'dot-edit-content-select-field', standalone: true, imports: [DropdownModule, ReactiveFormsModule], - templateUrl: './dot-edit-content-select-field.component.html', - styleUrls: ['./dot-edit-content-select-field.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) } - ] + ], + template: ` + + ` }) export class DotEditContentSelectFieldComponent implements OnInit { - @Input() field!: DotCMSContentTypeField; + $field = input.required({ alias: 'field' }); private readonly controlContainer = inject(ControlContainer); - options = []; + $options = computed(() => { + const field = this.$field(); + + return getSingleSelectableFieldOptions(field?.values || '', field.dataType); + }); ngOnInit() { - this.options = getSingleSelectableFieldOptions( - this.field?.values || '', - this.field.dataType - ); + const options = this.$options(); - if (this.formControl.value === null) { - this.formControl.setValue(this.options[0]?.value); + if (this.formControl.value === null && options.length > 0) { + this.formControl.setValue(options[0]?.value); } } @@ -44,8 +51,10 @@ export class DotEditContentSelectFieldComponent implements OnInit { * @returns {AbstractControl} The form control for the select field. */ get formControl() { + const field = this.$field(); + return this.controlContainer.control.get( - this.field.variable + field.variable ) as AbstractControl; } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.html deleted file mode 100644 index 61377975f83e..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.scss deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.spec.ts index 9608308f7091..6c7683c2aa36 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.spec.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from '@jest/globals'; -import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; +import { Spectator, createComponentFactory } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; +import { DebugElement } from '@angular/core'; import { ControlContainer, FormGroupDirective } from '@angular/forms'; import { By } from '@angular/platform-browser'; @@ -14,7 +15,7 @@ import { createFormGroupDirectiveMock, TAG_FIELD_MOCK } from '../../utils/mocks' describe('DotEditContentTagFieldComponent', () => { let spectator: Spectator; - let autoCompleteElement: Element; + let autoCompleteElement: DebugElement; let autoCompleteComponent: AutoComplete; const createComponent = createComponentFactory({ @@ -33,27 +34,28 @@ describe('DotEditContentTagFieldComponent', () => { spectator = createComponent({ props: { field: TAG_FIELD_MOCK - } + } as unknown }); + spectator.detectChanges(); - autoCompleteElement = spectator.query(byTestId(TAG_FIELD_MOCK.variable)); + autoCompleteComponent = spectator.query(AutoComplete); - autoCompleteComponent = spectator.debugElement.query( + autoCompleteElement = spectator.debugElement.query( By.css(`[data-testId="${TAG_FIELD_MOCK.variable}"]`) - ).componentInstance; + ); }); test.each([ { variable: `tag-id-${TAG_FIELD_MOCK.variable}`, - attribute: 'id' + attribute: 'ng-reflect-id' }, { variable: TAG_FIELD_MOCK.variable, attribute: 'ng-reflect-name' } ])('should have the $variable as $attribute', ({ variable, attribute }) => { - expect(autoCompleteElement.getAttribute(attribute)).toBe(variable); + expect(autoCompleteElement.attributes[attribute]).toBe(variable); }); it('should has multiple as true', () => { @@ -68,19 +70,6 @@ describe('DotEditContentTagFieldComponent', () => { expect(autoCompleteComponent.showClear).toBe(true); }); - it('should trigger selectItem on enter pressed', () => { - const selectItemMock = jest.spyOn(autoCompleteComponent, 'selectItem'); - - spectator.triggerEventHandler('p-autocomplete', 'onKeyUp', { - key: 'Enter', - target: { - value: 'test' - } - }); - - expect(selectItemMock).toBeCalledWith('test'); - }); - it('should trigger getTags on search with 3 or more characters', () => { const getTagsMock = jest.spyOn(spectator.component, 'getTags'); const autocompleteArg = { @@ -88,7 +77,7 @@ describe('DotEditContentTagFieldComponent', () => { }; spectator.triggerEventHandler('p-autocomplete', 'completeMethod', autocompleteArg); expect(getTagsMock).toBeCalledWith(autocompleteArg); - expect(spectator.query(AutoComplete).suggestions).toBeDefined(); + expect(autoCompleteComponent.suggestions).toBeDefined(); }); it('should dont have suggestions if search ir less than 3 characters', () => { @@ -98,6 +87,6 @@ describe('DotEditContentTagFieldComponent', () => { }; spectator.triggerEventHandler('p-autocomplete', 'completeMethod', autocompleteArg); expect(getTagsMock).toBeCalledWith(autocompleteArg); - expect(spectator.query(AutoComplete).suggestions).toBeNull(); + expect(autoCompleteComponent.suggestions).toBeNull(); }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.ts index 96fdb319ae44..8df42388c5f0 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component.ts @@ -1,7 +1,5 @@ -import { Observable } from 'rxjs'; - import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input, signal } from '@angular/core'; import { AbstractControl, ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { AutoCompleteModule } from 'primeng/autocomplete'; @@ -15,24 +13,43 @@ import { DotEditContentService } from '../../services/dot-edit-content.service'; @Component({ selector: 'dot-edit-content-tag-field', standalone: true, - imports: [CommonModule, AutoCompleteModule, DotSelectItemDirective, ReactiveFormsModule], - templateUrl: './dot-edit-content-tag-field.component.html', - styleUrls: ['./dot-edit-content-tag-field.component.scss'], + imports: [CommonModule, AutoCompleteModule, ReactiveFormsModule, DotSelectItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) } - ] + ], + template: ` + + ` }) export class DotEditContentTagFieldComponent { - @Input() field: DotCMSContentTypeField; - + $field = input.required({ alias: 'field' }); + $options = signal(null); private readonly editContentService = inject(DotEditContentService); private readonly controlContainer = inject(ControlContainer); - options$!: Observable; + /** + * Returns the form control for the select field. + * @returns {AbstractControl} The form control for the select field. + */ + get formControl() { + const field = this.$field(); + + return this.controlContainer.control.get(field.variable) as AbstractControl; + } /** * Retrieves tags based on the provided query. @@ -43,14 +60,8 @@ export class DotEditContentTagFieldComponent { return; } - this.options$ = this.editContentService.getTags(query); - } - - /** - * Returns the form control for the select field. - * @returns {AbstractControl} The form control for the select field. - */ - get formControl() { - return this.controlContainer.control.get(this.field.variable) as AbstractControl; + this.editContentService.getTags(query).subscribe((tags) => { + this.$options.set(tags); + }); } } diff --git a/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts b/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts index 04c8932c57a2..b9eef1e20dc9 100644 --- a/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts +++ b/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts @@ -51,10 +51,10 @@ export class DotEditContentService { * @param name - The name of the tags to retrieve. * @returns An Observable that emits an array of tag labels. */ - getTags(name: string) { + getTags(name: string): Observable { const params = new HttpParams().set('name', name); - return this.#http.get('/api/v2/tags', { params }).pipe( + return this.#http.get('/api/v2/tags', { params }).pipe( pluck('entity'), map((res) => Object.values(res).map((obj) => obj.label)) ); diff --git a/core-web/libs/edit-content/src/test-setup.ts b/core-web/libs/edit-content/src/test-setup.ts index ef84ee02978e..06872ce2d93b 100644 --- a/core-web/libs/edit-content/src/test-setup.ts +++ b/core-web/libs/edit-content/src/test-setup.ts @@ -1,12 +1,11 @@ import 'jest-preset-angular/setup-jest'; -import { SplitButtonMockComponent, SplitButtonMockModule } from '@dotcms/utils-testing'; - -/* - * This is a workaround for the following PrimeNg issue: https://github.com/primefaces/primeng/issues/12945 - * They already fixed it, but it's not in the latest v15 LTS yet: https://github.com/primefaces/primeng/pull/13597 - */ -jest.mock('primeng/splitbutton', () => ({ - SplitButtonModule: SplitButtonMockModule, - SplitButton: SplitButtonMockComponent -})); +// Workaround for the following issue: +// https://github.com/jsdom/jsdom/issues/2177#issuecomment-1724971596 +const originalConsoleError = console.error; +const jsDomCssError = 'Error: Could not parse CSS stylesheet'; +console.error = (...params) => { + if (!params.find((p) => p.toString().includes(jsDomCssError))) { + originalConsoleError(...params); + } +}; diff --git a/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/dot-experiments-configuration.component.spec.ts b/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/dot-experiments-configuration.component.spec.ts index 00b89653029d..eb9981516db4 100644 --- a/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/dot-experiments-configuration.component.spec.ts +++ b/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/dot-experiments-configuration.component.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { byTestId, createComponentFactory, @@ -191,7 +192,7 @@ describe('DotExperimentsConfigurationComponent', () => { spectator.detectComponentChanges(); expect(spectator.query(Menu)).toExist(); - spectator.query(Menu).model[1].command(); + spectator.query(Menu).model[1].command({ originalEvent: createFakeEvent('click') }); spectator.query(ConfirmDialog).accept(); @@ -207,7 +208,7 @@ describe('DotExperimentsConfigurationComponent', () => { spectator.detectComponentChanges(); //Add to bundle - spectator.query(Menu).model[5].command(); + spectator.query(Menu).model[5].command({ originalEvent: createFakeEvent('click') }); spectator.detectComponentChanges(); @@ -233,7 +234,7 @@ describe('DotExperimentsConfigurationComponent', () => { spectator.detectComponentChanges(); expect(spectator.query(Menu)).toExist(); - spectator.query(Menu).model[3].command(); + spectator.query(Menu).model[3].command({ originalEvent: createFakeEvent('click') }); spectator.query(ConfirmDialog).accept(); diff --git a/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/store/dot-experiments-configuration-store.spec.ts b/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/store/dot-experiments-configuration-store.spec.ts index a132d57f3b8b..098af35597cd 100644 --- a/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/store/dot-experiments-configuration-store.spec.ts +++ b/core-web/libs/portlets/dot-experiments/portlet/src/lib/dot-experiments-configuration/store/dot-experiments-configuration-store.spec.ts @@ -1,3 +1,4 @@ +import { createFakeEvent } from '@ngneat/spectator'; import { createServiceFactory, mockProvider, @@ -255,18 +256,24 @@ describe('DotExperimentsConfigurationStore', () => { store.vm$.pipe(take(1)).subscribe(({ menuItems }) => { // Start Experiment - menuItems[MENU_ITEMS_START_INDEX].command(); + menuItems[MENU_ITEMS_START_INDEX].command({ + originalEvent: createFakeEvent('click') + }); expect(dotExperimentsService.start).toHaveBeenCalledWith(EXPERIMENT_MOCK.id); // Push Publish - menuItems[MENU_ITEMS_PUSH_PUBLISH_INDEX].command(); + menuItems[MENU_ITEMS_PUSH_PUBLISH_INDEX].command({ + originalEvent: createFakeEvent('click') + }); expect(dotPushPublishDialogService.open).toHaveBeenCalledWith({ assetIdentifier: EXPERIMENT_MOCK.id, title: 'Push Publish' }); // Add to Bundle - menuItems[MENU_ITEMS_ADD_T0_BUNDLE_INDEX].command(); + menuItems[MENU_ITEMS_ADD_T0_BUNDLE_INDEX].command({ + originalEvent: createFakeEvent('click') + }); expect(store.showAddToBundle).toHaveBeenCalledWith(EXPERIMENT_MOCK.id); // test the ones with confirm dialog in the DotExperimentsConfigurationComponent. diff --git a/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/store/dot-locales-list.store.spec.ts b/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/store/dot-locales-list.store.spec.ts index 239e0cc3a61f..67a56ebd16c3 100644 --- a/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/store/dot-locales-list.store.spec.ts +++ b/core-web/libs/portlets/dot-locales/portlet/src/lib/dot-locales-list/store/dot-locales-list.store.spec.ts @@ -107,7 +107,6 @@ describe('DotLocalesListStore', () => { spectator.service.vm$.subscribe((viewModel) => { const pushPublishMenuItem = viewModel.locales[0].actions[1].menuItem; - pushPublishMenuItem.command(); expect(dotPushPublishDialogService.open).toHaveBeenCalledWith({ diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.spec.ts index 2e8f3472b782..012b29a9dc96 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.spec.ts @@ -15,6 +15,12 @@ import { PALETTE_PAGINATOR_ITEMS_PER_PAGE } from '../../store/edit-ema-palette.store'; +const CONTENTLETS_PROP_MOCK = { + items: CONTENTLETS_MOCK, + totalRecords: 10, + itemsPerPage: PALETTE_PAGINATOR_ITEMS_PER_PAGE +}; + describe('EditEmaPaletteContentletsComponent', () => { let spectator: Spectator; @@ -36,9 +42,7 @@ describe('EditEmaPaletteContentletsComponent', () => { spectator = createComponent({ props: { contentlets: { - items: CONTENTLETS_MOCK, - totalRecords: 10, - itemsPerPage: PALETTE_PAGINATOR_ITEMS_PER_PAGE + ...CONTENTLETS_PROP_MOCK }, control: new FormControl(''), paletteStatus: EditEmaPaletteStoreStatus.LOADED @@ -54,12 +58,18 @@ describe('EditEmaPaletteContentletsComponent', () => { }); it('should emit paginate event with filter on onPaginate', () => { - const spy = jest.spyOn(spectator.component.paginate, 'emit'); - spectator.triggerEventHandler(Paginator, 'onPageChange', { - page: 1, - contentVarName: 'sample' + const spyEmit = jest.spyOn(spectator.component.paginate, 'emit'); + + const mock_filter = { + query: 'test', + contentTypeVarName: 'test' + }; + spectator.setInput('contentlets', { + ...CONTENTLETS_PROP_MOCK, + filter: { ...mock_filter } }); - expect(spy).toHaveBeenCalledWith({ page: 1, contentVarName: 'sample' }); + spectator.triggerEventHandler(Paginator, 'onPageChange', { page: 1 }); + expect(spyEmit).toHaveBeenCalledWith({ page: 1, ...mock_filter }); }); it('should render contentlet list with data-item attribute', () => { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.ts index d88b69c4d228..418894b0309f 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/components/edit-ema-palette-contentlets/edit-ema-palette-contentlets.component.ts @@ -15,7 +15,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; import { InputTextModule } from 'primeng/inputtext'; -import { PaginatorModule } from 'primeng/paginator'; +import { PaginatorModule, PaginatorState } from 'primeng/paginator'; import { debounceTime, takeUntil } from 'rxjs/operators'; @@ -69,7 +69,7 @@ export class EditEmaPaletteContentletsComponent implements OnInit, OnDestroy { * @param {{ query: string; contentTypeVarName: string }} filter * @memberof EditEmaPaletteContentletsComponent */ - onPaginate(event, filter: { query: string; contentTypeVarName: string }) { + onPaginate(event: PaginatorState, filter: { query: string; contentTypeVarName: string }) { this.paginate.emit({ ...event, ...filter }); } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html index af8bcb118f42..dfdabec7b995 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html @@ -1,5 +1,4 @@ - @if ($editorProps().seoResults && ogTagsResults$) { ({ - SplitButtonModule: SplitButtonMockModule, - SplitButton: SplitButtonMockComponent -})); +// Workaround for the following issue: +// https://github.com/jsdom/jsdom/issues/2177#issuecomment-1724971596 +const originalConsoleError = console.error; +const jsDomCssError = 'Error: Could not parse CSS stylesheet'; +console.error = (...params) => { + if (!params.find((p) => p.toString().includes(jsDomCssError))) { + originalConsoleError(...params); + } +}; diff --git a/core-web/libs/portlets/edit-ema/ui/src/lib/dot-content-compare/components/dot-content-compare-table/dot-content-compare-table.component.spec.ts b/core-web/libs/portlets/edit-ema/ui/src/lib/dot-content-compare/components/dot-content-compare-table/dot-content-compare-table.component.spec.ts index 1dad59c7fde9..5fdcb9e8e245 100644 --- a/core-web/libs/portlets/edit-ema/ui/src/lib/dot-content-compare/components/dot-content-compare-table/dot-content-compare-table.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/ui/src/lib/dot-content-compare/components/dot-content-compare-table/dot-content-compare-table.component.spec.ts @@ -1,11 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { createFakeEvent } from '@ngneat/spectator'; import { of } from 'rxjs'; import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ButtonModule } from 'primeng/button'; import { Dropdown, DropdownModule } from 'primeng/dropdown'; @@ -15,7 +17,7 @@ import { TableModule } from 'primeng/table'; import { DotMessageService, DotFormatDateService } from '@dotcms/data-access'; import { DotcmsConfigService, LoginService } from '@dotcms/dotcms-js'; import { DotDiffPipe, DotMessagePipe, DotRelativeDatePipe } from '@dotcms/ui'; -import { MockDotMessageService } from '@dotcms/utils-testing'; +import { MockDotMessageService, mockMatchMedia } from '@dotcms/utils-testing'; import { DotContentCompareTableComponent } from './dot-content-compare-table.component'; @@ -296,7 +298,8 @@ describe('DotContentCompareTableComponent', () => { DotMessagePipe, FormsModule, DotRelativeDatePipe, - ButtonModule + ButtonModule, + BrowserAnimationsModule ], providers: [ { provide: DotMessageService, useValue: messageServiceMock }, @@ -324,6 +327,7 @@ describe('DotContentCompareTableComponent', () => { de = hostFixture.debugElement; hostComponent.data = dotContentCompareTableDataMock; hostFixture.detectChanges(); + mockMatchMedia(); }); describe('header', () => { @@ -347,11 +351,19 @@ describe('DotContentCompareTableComponent', () => { { label: 'Plain', value: false } ]); }); - it('should show versions selectButton with transformed label', () => { - const dropdown = de.query(By.css('[data-testId="versions-dropdown"]')).nativeElement; - expect(dropdown.innerHTML.replace(/^\s+|\s+$/gm, '')).toContain( - `${dotContentCompareTableDataMock.versions[0].modDate} by ${dotContentCompareTableDataMock.versions[0].modUserName}` - ); + it('should show versions selectButton with transformed label', async () => { + const dropdown = de.query(By.css('p-dropdown')); + + dropdown.componentInstance.show(); + hostFixture.detectChanges(); + + const versions = hostComponent.data.versions; + + dropdown.queryAll(By.css('.p-dropdown-item')).forEach((item, index) => { + const textContent = item.nativeElement.textContent; + const text = `${versions[index].modDate} by ${versions[index].modUserName}`; + expect(textContent).toContain(text); + }); }); }); @@ -469,7 +481,7 @@ describe('DotContentCompareTableComponent', () => { it('should emit changeVersion', () => { jest.spyOn(hostComponent.changeVersion, 'emit'); const dropdown: Dropdown = de.query(By.css('p-dropdown')).componentInstance; - dropdown.onChange.emit({ value: 'test' }); + dropdown.onChange.emit({ value: 'test', originalEvent: createFakeEvent('click') }); expect(hostComponent.changeVersion.emit).toHaveBeenCalledWith('test'); }); diff --git a/core-web/libs/portlets/edit-ema/ui/src/lib/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts b/core-web/libs/portlets/edit-ema/ui/src/lib/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts index ce60fee8e2f9..8fcd8c8c11f8 100644 --- a/core-web/libs/portlets/edit-ema/ui/src/lib/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/ui/src/lib/dot-results-seo-tool/dot-results-seo-tool.component.spec.ts @@ -1,4 +1,4 @@ -import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator'; +import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; import { diff --git a/core-web/libs/portlets/edit-ema/ui/src/lib/dot-select-seo-tool/dot-select-seo-tool.component.spec.ts b/core-web/libs/portlets/edit-ema/ui/src/lib/dot-select-seo-tool/dot-select-seo-tool.component.spec.ts index 4ba8b89e2b60..72fb056c3e42 100644 --- a/core-web/libs/portlets/edit-ema/ui/src/lib/dot-select-seo-tool/dot-select-seo-tool.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/ui/src/lib/dot-select-seo-tool/dot-select-seo-tool.component.spec.ts @@ -1,4 +1,4 @@ -import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator'; +import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; import { DotMessageService } from '@dotcms/data-access'; import { SEO_MEDIA_TYPES } from '@dotcms/dotcms-models'; diff --git a/core-web/libs/portlets/edit-ema/ui/src/test-setup.ts b/core-web/libs/portlets/edit-ema/ui/src/test-setup.ts index a904102a06ba..06872ce2d93b 100644 --- a/core-web/libs/portlets/edit-ema/ui/src/test-setup.ts +++ b/core-web/libs/portlets/edit-ema/ui/src/test-setup.ts @@ -1,18 +1,11 @@ -globalThis.ngJest = { - testEnvironmentOptions: { - errorOnUnknownElements: true, - errorOnUnknownProperties: true - } -}; import 'jest-preset-angular/setup-jest'; -import { SplitButtonMockComponent, SplitButtonMockModule } from '@dotcms/utils-testing'; - -/* - * This is a workaround for the following PrimeNg issue: https://github.com/primefaces/primeng/issues/12945 - * They already fixed it, but it's not in the latest v15 LTS yet: https://github.com/primefaces/primeng/pull/13597 - */ -jest.mock('primeng/splitbutton', () => ({ - SplitButtonModule: SplitButtonMockModule, - SplitButton: SplitButtonMockComponent -})); +// Workaround for the following issue: +// https://github.com/jsdom/jsdom/issues/2177#issuecomment-1724971596 +const originalConsoleError = console.error; +const jsDomCssError = 'Error: Could not parse CSS stylesheet'; +console.error = (...params) => { + if (!params.find((p) => p.toString().includes(jsDomCssError))) { + originalConsoleError(...params); + } +}; diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html index 0a4e281b6d65..105fa0abaef8 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html @@ -12,7 +12,7 @@ dotSelectItem data-testId="autocomplete" inputId="auto-complete-input" - appendTo="body"> + appendTo="body" />
  • diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts index d024ae5e1dbc..bcc4c69322d5 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts @@ -1,4 +1,5 @@ import { expect, it } from '@jest/globals'; +import { createFakeEvent } from '@ngneat/spectator'; import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; import { of, throwError } from 'rxjs'; @@ -12,7 +13,7 @@ import { DynamicDialogConfig, DynamicDialogRef, DynamicDialogModule } from 'prim import { DotMessageService } from '@dotcms/data-access'; import { DotMessagePipe } from '@dotcms/ui'; -import { MockDotMessageService } from '@dotcms/utils-testing'; +import { MockDotMessageService, mockMatchMedia } from '@dotcms/utils-testing'; import { AddStyleClassesDialogComponent } from './add-style-classes-dialog.component'; import { JsonClassesService } from './services/json-classes.service'; @@ -86,6 +87,7 @@ describe('AddStyleClassesDialogComponent', () => { service = spectator.inject(JsonClassesService); dialogRef = spectator.inject(DynamicDialogRef); autocomplete = spectator.query(AutoComplete); + mockMatchMedia(); }); it('should set attributes to autocomplete', () => { @@ -122,23 +124,14 @@ describe('AddStyleClassesDialogComponent', () => { it('should filter suggestions and pass to autocomplete on completeMethod', () => { spectator.detectChanges(); - spectator.triggerEventHandler(AutoComplete, 'completeMethod', { query: 'class1' }); + spectator.triggerEventHandler(AutoComplete, 'completeMethod', { + query: 'class1', + originalEvent: createFakeEvent('click') + }); expect(autocomplete.suggestions).toEqual(['class1']); }); - it('should add class on keyup.enter', () => { - const selectItemSpy = jest.spyOn(autocomplete, 'selectItem'); - spectator.detectChanges(); - - const input = document.createElement('input'); - input.value = 'class1'; - - spectator.triggerEventHandler(AutoComplete, 'onKeyUp', { key: 'Enter', target: input }); - - expect(selectItemSpy).toBeCalledWith('class1'); - }); - it('should save selected classes and close the dialog', () => { spectator.component.selectedClasses = ['class1']; spectator.component.save(); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts index 644fc614e06c..7e2f7eab8cfa 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts @@ -11,7 +11,7 @@ import { ButtonModule } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { DotMessageService } from '@dotcms/data-access'; -import { DotMessagePipe, DotSelectItemDirective } from '@dotcms/ui'; +import { DotMessagePipe } from '@dotcms/ui'; import { AddStyleClassesDialogComponent } from './add-style-classes-dialog.component'; import { JsonClassesService } from './services/json-classes.service'; @@ -35,8 +35,7 @@ const meta: Meta = { NgIf, AsyncPipe, HttpClientModule, - NoopAnimationsModule, - DotSelectItemDirective + NoopAnimationsModule ], providers: [ { diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts index 48328c58cdcb..e41e7bbd53eb 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts @@ -10,22 +10,14 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { catchError, map, shareReplay, tap } from 'rxjs/operators'; -import { DotMessagePipe, DotSelectItemDirective } from '@dotcms/ui'; +import { DotMessagePipe } from '@dotcms/ui'; import { JsonClassesService } from './services/json-classes.service'; @Component({ selector: 'dotcms-add-style-classes-dialog', standalone: true, - imports: [ - AutoCompleteModule, - FormsModule, - ButtonModule, - DotMessagePipe, - NgIf, - AsyncPipe, - DotSelectItemDirective - ], + imports: [AutoCompleteModule, FormsModule, ButtonModule, DotMessagePipe, NgIf, AsyncPipe], templateUrl: './add-style-classes-dialog.component.html', styleUrls: ['./add-style-classes-dialog.component.scss'], providers: [JsonClassesService], diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.html b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.html index 22fac5714955..5a5e1f7cbcca 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.html +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.html @@ -33,7 +33,6 @@ [filter]="true" [placeholder]="dropdownLabel" [formControl]="formControl" - [autoDisplayFirst]="false" scrollHeight="18.75rem" dotContainerOptions data-testId="btn-plus" diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.html b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.html index 6e83b358b98e..e7853bbc3771 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.html +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.html @@ -32,34 +32,38 @@ [value]="themes" #dataView layout="grid"> - -
    - - - - theme image -
    {{ theme.name }}
    + +
    + @for (theme of themes; track $index) { +
    + + + + theme image +
    {{ theme.name }}
    +
    + }
    diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.spec.ts index 8073a8d853f7..c5dd2c558a47 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-theme-selector/template-builder-theme-selector.component.spec.ts @@ -100,7 +100,12 @@ describe('TemplateBuilderThemeSelectorComponent', () => { spectator.component.ngOnInit(); spectator.detectChanges(); - spectator.component.dataView.onLazyLoad.emit({ first: 0 }); + spectator.component.dataView.onLazyLoad.emit({ + first: 0, + rows: 0, + sortField: '', + sortOrder: 0 + }); expect(paginatorService.getWithOffset).toHaveBeenCalledWith(0); }); diff --git a/core-web/libs/template-builder/src/test-setup.ts b/core-web/libs/template-builder/src/test-setup.ts index ef84ee02978e..06872ce2d93b 100644 --- a/core-web/libs/template-builder/src/test-setup.ts +++ b/core-web/libs/template-builder/src/test-setup.ts @@ -1,12 +1,11 @@ import 'jest-preset-angular/setup-jest'; -import { SplitButtonMockComponent, SplitButtonMockModule } from '@dotcms/utils-testing'; - -/* - * This is a workaround for the following PrimeNg issue: https://github.com/primefaces/primeng/issues/12945 - * They already fixed it, but it's not in the latest v15 LTS yet: https://github.com/primefaces/primeng/pull/13597 - */ -jest.mock('primeng/splitbutton', () => ({ - SplitButtonModule: SplitButtonMockModule, - SplitButton: SplitButtonMockComponent -})); +// Workaround for the following issue: +// https://github.com/jsdom/jsdom/issues/2177#issuecomment-1724971596 +const originalConsoleError = console.error; +const jsDomCssError = 'Error: Could not parse CSS stylesheet'; +console.error = (...params) => { + if (!params.find((p) => p.toString().includes(jsDomCssError))) { + originalConsoleError(...params); + } +}; diff --git a/core-web/libs/ui/src/lib/components/dot-action-menu-button/dot-action-menu-button.component.ts b/core-web/libs/ui/src/lib/components/dot-action-menu-button/dot-action-menu-button.component.ts index 5f7002b99054..de904ead891c 100644 --- a/core-web/libs/ui/src/lib/components/dot-action-menu-button/dot-action-menu-button.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-action-menu-button/dot-action-menu-button.component.ts @@ -1,10 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; -import { MenuItem } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { TooltipModule } from 'primeng/tooltip'; -import { DotActionMenuItem } from '@dotcms/dotcms-models'; +import { CustomMenuItem, DotActionMenuItem } from '@dotcms/dotcms-models'; import { DotMenuComponent } from '../dot-menu/dot-menu.component'; @@ -22,7 +21,7 @@ import { DotMenuComponent } from '../dot-menu/dot-menu.component'; standalone: true }) export class DotActionMenuButtonComponent implements OnInit { - filteredActions: MenuItem[] = []; + filteredActions: CustomMenuItem[] = []; @Input() item: Record; @@ -38,10 +37,9 @@ export class DotActionMenuButtonComponent implements OnInit { .map((action: DotActionMenuItem) => { return { ...action.menuItem, - command: ($event) => { + command: ($event: MouseEvent) => { action.menuItem.command(this.item); - $event = $event.originalEvent || $event; $event.stopPropagation(); } }; diff --git a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.html b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.html index 47cd903af970..7bae7ce6fe0b 100644 --- a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.html +++ b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.html @@ -1,16 +1,17 @@ -@for (action of groupedActions(); track $index; let first = $first) { +@for (action of groupedActions(); track $index) { @if (action.length > 1) { } @else { } } @empty { diff --git a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts index 1bdb1611e57e..728fb8c12d61 100644 --- a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts @@ -202,7 +202,7 @@ describe('DotWorkflowActionsComponent', () => { it('should have default size', () => { const { button, splitButton } = getComponents(spectator); expect(button.styleClass.trim()).toBe(''); - expect(splitButton.styleClass.trim()).toBe('p-button-outlined'); + expect(splitButton.styleClass.trim()).toBe(''); }); it('should set style class p-button-sm', () => { @@ -211,7 +211,7 @@ describe('DotWorkflowActionsComponent', () => { const { button, splitButton } = getComponents(spectator); - expect(splitButton.styleClass.trim()).toBe('p-button-sm p-button-outlined'); + expect(splitButton.styleClass.trim()).toBe('p-button-sm'); expect(button.styleClass.trim()).toBe('p-button-sm'); }); @@ -222,7 +222,7 @@ describe('DotWorkflowActionsComponent', () => { const { button, splitButton } = getComponents(spectator); expect(button.styleClass.trim()).toBe('p-button-lg'); - expect(splitButton.styleClass.trim()).toBe('p-button-lg p-button-outlined'); + expect(splitButton.styleClass.trim()).toBe('p-button-lg'); }); }); }); diff --git a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.ts b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.ts index 4ec3a6c44be8..c65b85ca9a0b 100644 --- a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.ts @@ -13,7 +13,7 @@ import { MenuItem } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { SplitButtonModule } from 'primeng/splitbutton'; -import { DotCMSActionSubtype, DotCMSWorkflowAction } from '@dotcms/dotcms-models'; +import { CustomMenuItem, DotCMSActionSubtype, DotCMSWorkflowAction } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '../../dot-message/dot-message.pipe'; @@ -87,7 +87,7 @@ export class DotWorkflowActionsComponent implements OnChanges { * @return {*} {MenuItem[][]} * @memberof DotWorkflowActionsComponent */ - private formatActions(actions: DotCMSWorkflowAction[] = []): MenuItem[][] { + private formatActions(actions: DotCMSWorkflowAction[] = []): CustomMenuItem[][] { const formatedActions = actions?.reduce((acc, action) => { if (action?.metadata?.subtype !== DotCMSActionSubtype.SEPARATOR) { acc.push({ diff --git a/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.spec.ts b/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.spec.ts index 1aa0232269fc..d4657c426cf9 100644 --- a/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.spec.ts +++ b/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.spec.ts @@ -1,7 +1,5 @@ import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator'; -import { By } from '@angular/platform-browser'; - import { AutoComplete, AutoCompleteModule } from 'primeng/autocomplete'; import { DotSelectItemDirective } from './dot-select-item.directive'; @@ -10,19 +8,19 @@ describe('DotSelectItemDirective', () => { let spectator: SpectatorDirective; let autoComplete: AutoComplete; let onKeyUpMock: jasmine.Spy; - let selectItem: jasmine.Spy; + let onOptionSelect: jasmine.Spy; const createDirective = createDirectiveFactory({ directive: DotSelectItemDirective, - template: ``, + template: ``, imports: [AutoCompleteModule] }); beforeEach(() => { spectator = createDirective(); - autoComplete = spectator.debugElement.query(By.css('p-autoComplete')).componentInstance; + autoComplete = spectator.query(AutoComplete); onKeyUpMock = spyOn(spectator.directive, 'onKeyUp').and.callThrough(); - selectItem = spyOn(autoComplete, 'selectItem'); + onOptionSelect = spyOn(autoComplete, 'onOptionSelect').and.callThrough(); }); it('should call onKeyUp from the directive', () => { @@ -44,7 +42,7 @@ describe('DotSelectItemDirective', () => { spectator.triggerEventHandler('p-autoComplete[dotSelectItem]', 'onKeyUp', event); - expect(selectItem).toHaveBeenCalledOnceWith(event.target.value); + expect(onOptionSelect).toHaveBeenCalledOnceWith(event, event.target.value); }); it('should not call autoComplete selectItem when key is not "Enter"', () => { @@ -55,6 +53,6 @@ describe('DotSelectItemDirective', () => { spectator.triggerEventHandler('p-autoComplete[dotSelectItem]', 'onKeyUp', event); - expect(selectItem).not.toHaveBeenCalled(); + expect(onOptionSelect).not.toHaveBeenCalled(); }); }); diff --git a/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.ts b/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.ts index 5619360212fb..15d7a830851e 100644 --- a/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.ts +++ b/core-web/libs/ui/src/lib/directives/dot-select-item/dot-select-item.directive.ts @@ -17,11 +17,12 @@ export class DotSelectItemDirective { */ @HostListener('onKeyUp', ['$event']) onKeyUp(event: KeyboardEvent) { - const target: HTMLInputElement = event.target as unknown as HTMLInputElement; - - if (event.key === 'Enter' && !!target.value) { - // TODO: find a way to get the selected item from the autocomplete selectItem method was removed since v.16 - this.autoComplete.selectItem(target.value); + const value = (event.target as HTMLInputElement).value; + if (event.key === 'Enter' && value) { + this.autoComplete.suggestions.push(value); + const options = this.autoComplete.visibleOptions(); + const size = options.length; + this.autoComplete.onOptionSelect(event, options[size - 1]); } } } diff --git a/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.spec.ts b/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.spec.ts index 4f78b46d3ca1..1085e78c1ca6 100644 --- a/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.spec.ts +++ b/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.spec.ts @@ -1,9 +1,9 @@ import { createHostFactory, SpectatorHost } from '@ngneat/spectator'; -import { By } from '@angular/platform-browser'; +import { Component } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DropdownModule } from 'primeng/dropdown'; +import { Dropdown, DropdownModule } from 'primeng/dropdown'; import { DotContainersService, DotMessageService } from '@dotcms/data-access'; import { @@ -14,7 +14,16 @@ import { } from '@dotcms/utils-testing'; import { DotContainerOptionsDirective } from './dot-container-options.directive'; -import { MockContainersDropdownComponent } from './mock-containers-dropdown.component'; + +@Component({ + selector: `dot-containers-dropdown-mock`, + imports: [DropdownModule, DotContainerOptionsDirective], + template: ` + + `, + standalone: true +}) +class MockContainersDropdownComponent {} const sortedContainersMock = containersMock .map((container) => ({ @@ -50,38 +59,33 @@ describe('ContainerOptionsDirective', () => { const createHost = createHostFactory({ component: MockContainersDropdownComponent, - imports: [BrowserAnimationsModule, DotContainerOptionsDirective, DropdownModule], + imports: [BrowserAnimationsModule], providers: [ - { - provide: DotContainersService, - useValue: new DotContainersServiceMock() - }, { provide: DotMessageService, useValue: new MockDotMessageService({}) + }, + { + provide: DotContainersService, + useValue: new DotContainersServiceMock() } ] }); beforeEach(() => { - spectator = createHost(``); + spectator = createHost(``); spectator.detectChanges(); mockMatchMedia(); }); - it('should set the group property of the dropdown to true', () => { - // get the dropdown component - const dropdown = spectator.debugElement.query(By.css('p-dropdown')); - // Get the dropdown component instance - const dropdownInstance = dropdown.componentInstance; - - expect(dropdownInstance.group).toBeTruthy(); + it('should set the group property of the dropdown to true', async () => { + await spectator.fixture.whenStable(); + const dropdown = spectator.query(Dropdown); + expect(dropdown.group).toBeTruthy(); }); it('should group containers by host', () => { - const dropdown = spectator.debugElement.query(By.css('p-dropdown')); - const dropdownInstance = dropdown.componentInstance; - - expect(dropdownInstance.options).toEqual(getGroupByHostContainersMock()); + const dropdown = spectator.query(Dropdown); + expect(dropdown.options).toEqual(getGroupByHostContainersMock()); }); }); diff --git a/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.stories.ts b/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.stories.ts index 648724a3d498..ff6c9019b338 100644 --- a/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.stories.ts +++ b/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.stories.ts @@ -9,7 +9,7 @@ import { DotContainersServiceMock, MockDotMessageService } from '@dotcms/utils-t import { DotContainerOptionsDirective } from './dot-container-options.directive'; -const meta: Meta = { +const meta: Meta = { title: 'DotCMS/Container Options Directive', component: Dropdown, decorators: [ @@ -34,14 +34,13 @@ const meta: Meta = { parameters: { layout: 'centered' }, - render: () => { - return { - template: `` - }; - } + render: (args) => ({ + props: args, + template: `` + }) }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Base: Story = {}; diff --git a/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.ts b/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.ts index 1a6285195995..aff3e2a18d31 100644 --- a/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.ts +++ b/core-web/libs/ui/src/lib/dot-container-options/dot-container-options.directive.ts @@ -1,10 +1,11 @@ -import { Observable, of, Subject } from 'rxjs'; +import { Observable, of } from 'rxjs'; -import { ChangeDetectorRef, Directive, OnDestroy, OnInit, Optional, Self } from '@angular/core'; +import { ChangeDetectorRef, Directive, OnInit, Optional, Self } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Dropdown } from 'primeng/dropdown'; -import { catchError, debounceTime, map, switchMap, takeUntil } from 'rxjs/operators'; +import { catchError, debounceTime, map, switchMap } from 'rxjs/operators'; import { DotContainersService, DotMessageService } from '@dotcms/data-access'; import { @@ -26,11 +27,10 @@ const DEFAULT_VALUE_NAME_INDEX = 'value'; selector: 'p-dropdown[dotContainerOptions]', standalone: true }) -export class DotContainerOptionsDirective implements OnInit, OnDestroy { +export class DotContainerOptionsDirective implements OnInit { private readonly control: Dropdown; private readonly maxOptions = 10; private readonly loadErrorMessage: string; - private destroy$: Subject = new Subject(); constructor( @Optional() @Self() private readonly primeDropdown: Dropdown, @@ -49,6 +49,16 @@ export class DotContainerOptionsDirective implements OnInit, OnDestroy { this.control.optionValue = DEFAULT_VALUE_NAME_INDEX; this.control.optionDisabled = 'inactive'; this.control.filterBy = 'value.friendlyName,value.title'; + + this.control.onFilter + .pipe( + takeUntilDestroyed(), + debounceTime(500), + switchMap((event: { filter: string }) => { + return this.fetchContainerOptions(event.filter); + }) + ) + .subscribe((options) => this.setOptions(options)); } else { console.warn('ContainerOptionsDirective is for use with PrimeNg Dropdown'); } @@ -58,15 +68,6 @@ export class DotContainerOptionsDirective implements OnInit, OnDestroy { this.fetchContainerOptions().subscribe((options) => { this.control.options = this.control.options || options; // avoid overwriting if they were already set }); - this.control.onFilter - .pipe( - takeUntil(this.destroy$), - debounceTime(500), - switchMap((event: { filter: string }) => { - return this.fetchContainerOptions(event.filter); - }) - ) - .subscribe((options) => this.setOptions(options)); } private fetchContainerOptions( @@ -145,9 +146,4 @@ export class DotContainerOptionsDirective implements OnInit, OnDestroy { return acc; }, {}); } - - ngOnDestroy() { - this.destroy$.next(true); - this.destroy$.complete(); - } } diff --git a/core-web/libs/ui/src/lib/dot-container-options/mock-containers-dropdown.component.ts b/core-web/libs/ui/src/lib/dot-container-options/mock-containers-dropdown.component.ts deleted file mode 100644 index 31d7044b63cf..000000000000 --- a/core-web/libs/ui/src/lib/dot-container-options/mock-containers-dropdown.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'dot-containers-dropdown-mock', - template: ` - - ` -}) -export class MockContainersDropdownComponent {} diff --git a/core-web/libs/ui/src/lib/dot-site-selector/dot-site-selector.directive.spec.ts b/core-web/libs/ui/src/lib/dot-site-selector/dot-site-selector.directive.spec.ts index f08ae393d497..6d592c861947 100644 --- a/core-web/libs/ui/src/lib/dot-site-selector/dot-site-selector.directive.spec.ts +++ b/core-web/libs/ui/src/lib/dot-site-selector/dot-site-selector.directive.spec.ts @@ -1,11 +1,11 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator'; +import { createDirectiveFactory, createFakeEvent, SpectatorDirective } from '@ngneat/spectator'; import { of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { fakeAsync } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Dropdown, DropdownModule } from 'primeng/dropdown'; +import { Dropdown, DropdownFilterEvent, DropdownModule } from 'primeng/dropdown'; import { DotEventsService, DotSiteService } from '@dotcms/data-access'; import { CoreWebService, mockSites, SiteService } from '@dotcms/dotcms-js'; @@ -60,12 +60,14 @@ describe('DotSiteSelectorDirective', () => { }); it('should get sites list with filter', fakeAsync(() => { - const filter = 'demo'; - - dropdown.onFilter.emit({ filter }); + const event: DropdownFilterEvent = { + filter: 'demo', + originalEvent: createFakeEvent('click') + }; + dropdown.onFilter.emit(event); spectator.tick(500); - expect(getSitesSpy).toHaveBeenCalledWith(filter, 10); + expect(dotSiteService.getSites).toHaveBeenCalledWith(event.filter, 10); })); }); diff --git a/core-web/libs/utils-testing/src/lib/dot-rendered-page-state.mock.ts b/core-web/libs/utils-testing/src/lib/dot-rendered-page-state.mock.ts index 335db33f11fe..29cfd4ccffed 100644 --- a/core-web/libs/utils-testing/src/lib/dot-rendered-page-state.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-rendered-page-state.mock.ts @@ -1,10 +1,26 @@ import { DotPageRender, DotPageRenderState } from '@dotcms/dotcms-models'; import { mockDotRenderedPage } from './dot-page-render.mock'; +import { dotcmsContentletMock } from './dotcms-contentlet.mock'; import { mockUser } from './login-service.mock'; export const mockDotRenderedPageState = new DotPageRenderState( mockUser(), - new DotPageRender(mockDotRenderedPage()), - null + new DotPageRender(mockDotRenderedPage()) +); + +export const mockDotRenderedPageStateWithPersona = new DotPageRenderState( + mockUser(), + new DotPageRender({ + ...mockDotRenderedPage(), + viewAs: { + ...mockDotRenderedPage().viewAs, + persona: { + ...dotcmsContentletMock, + name: 'Super Persona', + keyTag: 'SuperPersona', + personalized: true + } + } + }) ); diff --git a/core-web/libs/utils-testing/src/lib/dot-workflows-actions.mock.ts b/core-web/libs/utils-testing/src/lib/dot-workflows-actions.mock.ts index 665393cc4ca1..4686cc99cae8 100644 --- a/core-web/libs/utils-testing/src/lib/dot-workflows-actions.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-workflows-actions.mock.ts @@ -12,7 +12,6 @@ export const mockWorkflowsActions: DotCMSWorkflowAction[] = [ nextStep: '43e16aac-5799-46d0-945c-83753af39426', nextStepCurrentStep: false, order: 0, - owner: null, roleHierarchyForAssign: true, schemeId: '85c1515c-c4f3-463c-bac2-860b8fcacc34', showOn: ['UNLOCKED', 'LOCKED'], @@ -43,7 +42,6 @@ export const mockWorkflowsActions: DotCMSWorkflowAction[] = [ nextStep: 'ee24a4cb-2d15-4c98-b1bd-6327126451f3', nextStepCurrentStep: false, order: 0, - owner: null, roleHierarchyForAssign: false, schemeId: 'd61a59e1-a49c-46f2-a929-db2b4bfa88b2', showOn: ['LOCKED'], @@ -60,7 +58,6 @@ export const mockWorkflowsActions: DotCMSWorkflowAction[] = [ nextStep: 'dc3c9cd0-8467-404b-bf95-cb7df3fbc293', nextStepCurrentStep: false, order: 0, - owner: null, roleHierarchyForAssign: false, schemeId: 'd61a59e1-a49c-46f2-a929-db2b4bfa88b2', showOn: ['LOCKED'], diff --git a/core-web/package.json b/core-web/package.json index e428d83ca991..a8cb54f8f502 100644 --- a/core-web/package.json +++ b/core-web/package.json @@ -112,9 +112,9 @@ "ng2-dragula": "5.0.1", "ngx-markdown": "17.1.1", "node-fetch": "^2.6.1", - "primeflex": "^3.3.0", - "primeicons": "^6.0.1", - "primeng": "15.4.13-lts", + "primeflex": "3.3.1", + "primeicons": "7.0.0", + "primeng": "17.18.9", "prosemirror-model": "^1.16.1", "prosemirror-view": "^1.23.12", "react": "^18.2.0", diff --git a/core-web/yarn.lock b/core-web/yarn.lock index 3a1fd349659f..216f98717367 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -18495,20 +18495,20 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -primeflex@^3.3.0: +primeflex@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/primeflex/-/primeflex-3.3.1.tgz#361dddf6eb5db50d733e4cddd4b6e376a3d7bd68" integrity sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ== -primeicons@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/primeicons/-/primeicons-6.0.1.tgz#431fa7c79825934eefd62087d8e1faa6a9e376ad" - integrity sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA== +primeicons@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/primeicons/-/primeicons-7.0.0.tgz#6b25c3fdcb29bb745a3035bdc1ed5902f4a419cf" + integrity sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw== -primeng@15.4.13-lts: - version "15.4.13-lts" - resolved "https://registry.yarnpkg.com/primeng/-/primeng-15.4.13-lts.tgz#a2480a83ea9796889c81ce8036fecb65ead1ab03" - integrity sha512-yybyQ4n7phd1Z6DrSWfhsUKAbgR4QIDynmnfnL3iJ9cnu8Rckgx2OITMGx+0e0G8iaO5AZnqyKiYKIcsoineCg== +primeng@17.18.9: + version "17.18.9" + resolved "https://registry.yarnpkg.com/primeng/-/primeng-17.18.9.tgz#dc3b65689548b53ce07ce7032a0aa3d1eb5dfbea" + integrity sha512-1FT0B8wtgvs/joduB1DDOLe2IsP1pegOiEfSPAHSbc6otgNx/6iLR0k2M/xr2c9Ur1aC7tAikkVfH3FGpWof3w== dependencies: tslib "^2.3.0"