From 3ae5654cc2afb0d26da706e1ae9b66e585e56faf Mon Sep 17 00:00:00 2001 From: Kevin Davila <144152756+KevinDavilaDotCMS@users.noreply.github.com> Date: Fri, 31 May 2024 16:28:14 -0500 Subject: [PATCH] chore(sdk): Create dotCMS Angular library #28498 (#28661) ### Proposed Changes * Moved all SDK logic to standalone lib in NX workspace * This PR introduces a new method to create a links from the example apps to sdk libs, using compilerOptions on tsconfig file. To test it, build the angular sdk lib and run the angular example in core/core-web: `npx nx run sdk-angular:build` then `ng serve` in examples/angular This way can be used in other sdk/examples apps --------- Co-authored-by: KevinDavilaDotCMS --- core-web/libs/sdk/angular/.eslintrc.json | 25 + core-web/libs/sdk/angular/README.md | 134 +++ core-web/libs/sdk/angular/jest.config.ts | 22 + core-web/libs/sdk/angular/ng-package.json | 7 + core-web/libs/sdk/angular/package.json | 31 + core-web/libs/sdk/angular/project.json | 33 + core-web/libs/sdk/angular/src/index.ts | 3 + .../no-component/no-component.component.css | 3 + .../no-component.component.spec.ts | 24 + .../no-component/no-component.component.ts | 19 + .../lib/layout/column/column.component.css | 99 ++ .../layout/column/column.component.spec.ts | 33 + .../src/lib/layout/column/column.component.ts | 30 + .../layout/container/container.component.css | 9 + .../layout/container/container.component.html | 17 + .../container/container.component.spec.ts | 145 +++ .../layout/container/container.component.ts | 88 ++ .../contentlet/contentlet.component.spec.ts | 22 + .../layout/contentlet/contentlet.component.ts | 34 + .../dotcms-layout/dotcms-layout.component.css | 3 + .../dotcms-layout.component.spec.ts | 87 ++ .../dotcms-layout/dotcms-layout.component.ts | 72 ++ .../src/lib/layout/row/row.component.css | 6 + .../src/lib/layout/row/row.component.spec.ts | 28 + .../src/lib/layout/row/row.component.ts | 18 + .../angular/src}/lib/models/dotcms.model.ts | 7 +- .../libs/sdk/angular/src/lib/models/index.ts | 15 + .../dotcms-context/page-context.service.ts | 33 + .../dotcms-context/page-context.spec.ts | 50 + .../libs/sdk/angular/src/lib/utils/index.ts | 70 ++ .../angular/src/lib/utils/testing.utils.ts | 981 ++++++++++++++++++ core-web/libs/sdk/angular/src/test-setup.ts | 8 + core-web/libs/sdk/angular/tsconfig.json | 29 + core-web/libs/sdk/angular/tsconfig.lib.json | 12 + .../libs/sdk/angular/tsconfig.lib.prod.json | 9 + core-web/libs/sdk/angular/tsconfig.spec.json | 11 + core-web/nx.json | 5 + core-web/tsconfig.base.json | 1 + core-web/yarn.lock | 2 +- examples/angular/package-lock.json | 24 +- examples/angular/package.json | 19 +- examples/angular/src/app/app.component.ts | 8 +- examples/angular/src/app/app.config.ts | 7 +- examples/angular/src/app/app.routes.ts | 4 +- .../src/app/client-token/dotcms-client.ts | 20 + .../no-component/no-component.component.css | 3 - .../no-component/no-component.component.ts | 21 - .../src/app/lib/dotcms-client-token.ts | 17 - .../lib/layout/column/column.component.css | 99 -- .../layout/column/column.component.spec.ts | 22 - .../app/lib/layout/column/column.component.ts | 30 - .../layout/container/container.component.css | 0 .../layout/container/container.component.html | 41 - .../container/container.component.spec.ts | 22 - .../layout/container/container.component.ts | 77 -- .../dotcms-layout/dotcms-layout.component.css | 3 - .../dotcms-layout.component.spec.ts | 22 - .../dotcms-layout/dotcms-layout.component.ts | 62 -- .../src/app/lib/layout/row/row.component.css | 6 - .../app/lib/layout/row/row.component.spec.ts | 22 - .../src/app/lib/layout/row/row.component.ts | 18 - examples/angular/src/app/lib/models/index.ts | 5 - .../lib/resolver/dotcms-page.resolver.spec.ts | 10 - .../app/lib/resolver/dotcms-page.resolver.ts | 53 - .../dotcms-context/page-context.service.ts | 51 - .../dotcms-context/page-context.spec.ts | 16 - examples/angular/src/app/lib/utils/index.ts | 66 -- .../navigation/navigation.component.ts | 4 +- .../activity/activity.component.ts | 2 +- .../content-types/banner/banner.component.ts | 3 +- .../content-types/image/image.component.ts | 2 +- .../product/product.component.ts | 3 +- .../web-page-content.component.ts | 2 +- .../src/app/pages/pages.component.html | 2 +- .../angular/src/app/pages/pages.component.ts | 22 +- .../src/app/resolver/dotcms-page.resolver.ts | 55 + examples/angular/src/app/utils/index.ts | 10 +- .../environments/environment.development.ts | 2 +- examples/angular/tsconfig.app.json | 2 +- examples/angular/tsconfig.json | 10 +- 80 files changed, 2358 insertions(+), 734 deletions(-) create mode 100644 core-web/libs/sdk/angular/.eslintrc.json create mode 100644 core-web/libs/sdk/angular/README.md create mode 100644 core-web/libs/sdk/angular/jest.config.ts create mode 100644 core-web/libs/sdk/angular/ng-package.json create mode 100644 core-web/libs/sdk/angular/package.json create mode 100644 core-web/libs/sdk/angular/project.json create mode 100644 core-web/libs/sdk/angular/src/index.ts create mode 100644 core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.css create mode 100644 core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/column/column.component.css create mode 100644 core-web/libs/sdk/angular/src/lib/layout/column/column.component.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/column/column.component.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/container/container.component.css create mode 100644 core-web/libs/sdk/angular/src/lib/layout/container/container.component.html create mode 100644 core-web/libs/sdk/angular/src/lib/layout/container/container.component.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/container/container.component.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.css create mode 100644 core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/row/row.component.css create mode 100644 core-web/libs/sdk/angular/src/lib/layout/row/row.component.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/layout/row/row.component.ts rename {examples/angular/src/app => core-web/libs/sdk/angular/src}/lib/models/dotcms.model.ts (98%) create mode 100644 core-web/libs/sdk/angular/src/lib/models/index.ts create mode 100644 core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.service.ts create mode 100644 core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.spec.ts create mode 100644 core-web/libs/sdk/angular/src/lib/utils/index.ts create mode 100644 core-web/libs/sdk/angular/src/lib/utils/testing.utils.ts create mode 100644 core-web/libs/sdk/angular/src/test-setup.ts create mode 100644 core-web/libs/sdk/angular/tsconfig.json create mode 100644 core-web/libs/sdk/angular/tsconfig.lib.json create mode 100644 core-web/libs/sdk/angular/tsconfig.lib.prod.json create mode 100644 core-web/libs/sdk/angular/tsconfig.spec.json create mode 100644 examples/angular/src/app/client-token/dotcms-client.ts delete mode 100644 examples/angular/src/app/lib/components/no-component/no-component.component.css delete mode 100644 examples/angular/src/app/lib/components/no-component/no-component.component.ts delete mode 100644 examples/angular/src/app/lib/dotcms-client-token.ts delete mode 100644 examples/angular/src/app/lib/layout/column/column.component.css delete mode 100644 examples/angular/src/app/lib/layout/column/column.component.spec.ts delete mode 100644 examples/angular/src/app/lib/layout/column/column.component.ts delete mode 100644 examples/angular/src/app/lib/layout/container/container.component.css delete mode 100644 examples/angular/src/app/lib/layout/container/container.component.html delete mode 100644 examples/angular/src/app/lib/layout/container/container.component.spec.ts delete mode 100644 examples/angular/src/app/lib/layout/container/container.component.ts delete mode 100644 examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.css delete mode 100644 examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts delete mode 100644 examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.ts delete mode 100644 examples/angular/src/app/lib/layout/row/row.component.css delete mode 100644 examples/angular/src/app/lib/layout/row/row.component.spec.ts delete mode 100644 examples/angular/src/app/lib/layout/row/row.component.ts delete mode 100644 examples/angular/src/app/lib/models/index.ts delete mode 100644 examples/angular/src/app/lib/resolver/dotcms-page.resolver.spec.ts delete mode 100644 examples/angular/src/app/lib/resolver/dotcms-page.resolver.ts delete mode 100644 examples/angular/src/app/lib/services/dotcms-context/page-context.service.ts delete mode 100644 examples/angular/src/app/lib/services/dotcms-context/page-context.spec.ts delete mode 100644 examples/angular/src/app/lib/utils/index.ts create mode 100644 examples/angular/src/app/resolver/dotcms-page.resolver.ts diff --git a/core-web/libs/sdk/angular/.eslintrc.json b/core-web/libs/sdk/angular/.eslintrc.json new file mode 100644 index 000000000000..4cfe38bd218c --- /dev/null +++ b/core-web/libs/sdk/angular/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/core-web/libs/sdk/angular/README.md b/core-web/libs/sdk/angular/README.md new file mode 100644 index 000000000000..e926598bc8f0 --- /dev/null +++ b/core-web/libs/sdk/angular/README.md @@ -0,0 +1,134 @@ +# @dotcms/angular + +`@dotcms/angular` is the official set of Angular components, services and resolver designed to work seamlessly with dotCMS, making it easy to render dotCMS pages an use the page builder + +## Features + +- A collection of Angular components, services and resolver tailored to render + dotCMS pages. +- Streamlined integration with dotCMS page editor. +- Improved development experience with comprehensive TypeScript typings. + +## Installation + +Install the package via npm: + +```bash +npm install @dotcms/angular +``` + +Or using Yarn: + +```bash +yarn add @dotcms/angular +``` + +## Provider +``` +const DOTCMS_CLIENT_CONFIG: ClientConfig = { + dotcmsUrl: environment.dotcmsUrl, + authToken: environment.authToken, + siteId: environment.siteId +}; +``` +Add the dotcms config in the Angular app ApplicationConfig +``` +export const appConfig: ApplicationConfig = { + providers: [ + provideDotcmsClient(DOTCMS_CLIENT_CONFIG), + provideRouter(routes), + ], +}; +``` +## Resolver +```javascript +export const routes: Routes = [ + { + path: '**', + resolve: { + // This should be called `context`. + context: DotCMSPageResolver, + }, + component: DotCMSPagesComponent, + runGuardsAndResolvers: 'always' // Run the resolver on every navigation. Even if the URL hasn't changed. + }, +]; +``` + +Then, in your component, you can read the data using + +```javascript +protected readonly context = signal(null); + +ngOnInit() { + // Get the context data from the route + this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => { + this.context.set(data['context']); + }); +} +``` +## Components + +### `DotcmsLayoutComponent` + +A component that renders a layout for a dotCMS page. + +#### Inputs + +- **entity**: The context for a dotCMS page. +- **components**: An object with the relation of contentlets and the component to render each. + + +#### Usage + +```javascript + + + DYNAMIC_COMPONENTS: { [key: string]: DynamicComponentEntity } = { + Activity: import('../pages/content-types/activity/activity.component').then( + (c) => c.ActivityComponent, + ), + Banner: import('../pages/content-types/banner/banner.component').then( + (c) => c.BannerComponent, + ), + Image: import('../pages/content-types/image/image.component').then( + (c) => c.ImageComponent, + ), + webPageContent: import( + '../pages/content-types/web-page-content/web-page-content.component' + ).then((c) => c.WebPageContentComponent), + Product: import('../pages/content-types/product/product.component').then( + (c) => c.ProductComponent, + ), + }; + +components = signal(DYNAMIC_COMPONENTS); +``` + +## Contributing + +GitHub pull requests are the preferred method to contribute code to dotCMS. Before any pull requests can be accepted, an automated tool will ask you to agree to the [dotCMS Contributor's Agreement](https://gist.github.com/wezell/85ef45298c48494b90d92755b583acb3). + +## Licensing + +dotCMS comes in multiple editions and as such is dual licensed. The dotCMS Community Edition is licensed under the GPL 3.0 and is freely available for download, customization and deployment for use within organizations of all stripes. dotCMS Enterprise Editions (EE) adds a number of enterprise features and is available via a supported, indemnified commercial license from dotCMS. For the differences between the editions, see [the feature page](http://dotcms.com/cms-platform/features). + +## Support + +If you need help or have any questions, please [open an issue](https://github.com/dotCMS/core/issues/new/choose) in the GitHub repository. + +## Documentation + +Always refer to the official [DotCMS documentation](https://www.dotcms.com/docs/latest/) for comprehensive guides and API references. + +## Getting Help + +| Source | Location | +| --------------- | ------------------------------------------------------------------- | +| Installation | [Installation](https://dotcms.com/docs/latest/installation) | +| Documentation | [Documentation](https://dotcms.com/docs/latest/table-of-contents) | +| Videos | [Helpful Videos](http://dotcms.com/videos/) | +| Code Examples | [Codeshare](https://dotcms.com/codeshare/) | +| Forums/Listserv | [via Google Groups](https://groups.google.com/forum/#!forum/dotCMS) | +| Twitter | @dotCMS | +| Main Site | [dotCMS.com](https://dotcms.com/) | diff --git a/core-web/libs/sdk/angular/jest.config.ts b/core-web/libs/sdk/angular/jest.config.ts new file mode 100644 index 000000000000..207e6600a44f --- /dev/null +++ b/core-web/libs/sdk/angular/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'sdk-angular', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/sdk/angular', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$' + } + ] + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment' + ] +}; diff --git a/core-web/libs/sdk/angular/ng-package.json b/core-web/libs/sdk/angular/ng-package.json new file mode 100644 index 000000000000..d0b7aadd818f --- /dev/null +++ b/core-web/libs/sdk/angular/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/sdk/angular", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/core-web/libs/sdk/angular/package.json b/core-web/libs/sdk/angular/package.json new file mode 100644 index 000000000000..13a16f9cb9e8 --- /dev/null +++ b/core-web/libs/sdk/angular/package.json @@ -0,0 +1,31 @@ +{ + "name": "@dotcms/angular", + "version": "0.0.1-alpha.17", + "peerDependencies": { + "@angular/common": "^17.1.0", + "@angular/core": "^17.1.0", + "@angular/router": "^17.1.0", + "@dotcms/client": "0.0.1-alpha.17", + "rxjs": "~6.6.3" + }, + "description": "Official Angular Components library to render a dotCMS page.", + "repository": { + "type": "git", + "url": "git+https://github.com/dotCMS/core.git#master" + }, + "keywords": [ + "dotCMS", + "CMS", + "Content Management", + "API Client", + "REST API", + "Angular", + "Components" + ], + "author": "dotcms ", + "license": "MIT", + "bugs": { + "url": "https://github.com/dotCMS/core/issues" + }, + "homepage": "https://github.com/dotCMS/core/tree/master/core-web/libs/sdk/angular/README.md" +} diff --git a/core-web/libs/sdk/angular/project.json b/core-web/libs/sdk/angular/project.json new file mode 100644 index 000000000000..146f47920419 --- /dev/null +++ b/core-web/libs/sdk/angular/project.json @@ -0,0 +1,33 @@ +{ + "name": "sdk-angular", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/sdk/angular/src", + "prefix": "lib", + "tags": [], + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/angular:package", + "outputs": ["{workspaceRoot}/dist/{projectRoot}"], + "options": { + "project": "libs/sdk/angular/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/sdk/angular/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/sdk/angular/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/sdk/angular/jest.config.ts" + } + } + } +} diff --git a/core-web/libs/sdk/angular/src/index.ts b/core-web/libs/sdk/angular/src/index.ts new file mode 100644 index 000000000000..b0cb80689062 --- /dev/null +++ b/core-web/libs/sdk/angular/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/layout/dotcms-layout/dotcms-layout.component'; +export * from './lib/services/dotcms-context/page-context.service'; +export * from './lib/models'; diff --git a/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.css b/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.css new file mode 100644 index 000000000000..7f26ddcb5503 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.spec.ts b/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.spec.ts new file mode 100644 index 000000000000..40aebf30d05a --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.spec.ts @@ -0,0 +1,24 @@ +import { Spectator, createComponentFactory } from '@ngneat/spectator'; + +import { NoComponent } from './no-component.component'; + +import { DotCMSContentlet } from '../../models'; + +describe('NoComponentComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory(NoComponent); + + beforeEach(() => { + spectator = createComponent({ + props: { + contentlet: { contentType: 'exampleContentType' } as DotCMSContentlet + } + }); + }); + + it('should display the content type', () => { + const noComponent = spectator.debugElement.nativeElement; + expect(noComponent?.innerHTML).toBe('No Component for exampleContentType'); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.ts b/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.ts new file mode 100644 index 000000000000..ca273aacc436 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/components/no-component/no-component.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +import { DotCMSContentlet } from '../../models'; + +/** + * This is part of the Angular SDK. + * This is a component for the `NoComponentComponent` component. + */ +@Component({ + selector: 'dotcms-no-component', + standalone: true, + template: `No Component for {{ contentlet.contentType }}`, + styleUrl: './no-component.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NoComponent { + @Input() contentlet!: DotCMSContentlet; + @HostBinding('attr.data-testid') testId = 'no-component'; +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/column/column.component.css b/core-web/libs/sdk/angular/src/lib/layout/column/column.component.css new file mode 100644 index 000000000000..a975d093ddb0 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/column/column.component.css @@ -0,0 +1,99 @@ +:host.col-start-1 { + grid-column-start: 1; +} + +:host.col-start-2 { + grid-column-start: 2; +} + +:host.col-start-3 { + grid-column-start: 3; +} + +:host.col-start-4 { + grid-column-start: 4; +} + +:host.col-start-5 { + grid-column-start: 5; +} + +:host.col-start-6 { + grid-column-start: 6; +} + +:host.col-start-7 { + grid-column-start: 7; +} + +:host.col-start-8 { + grid-column-start: 8; +} + +:host.col-start-9 { + grid-column-start: 9; +} + +:host.col-start-10 { + grid-column-start: 10; +} + +:host.col-start-11 { + grid-column-start: 11; +} + +:host.col-start-12 { + grid-column-start: 12; +} + +:host.col-end-1 { + grid-column-end: 1; +} + +:host.col-end-2 { + grid-column-end: 2; +} + +:host.col-end-3 { + grid-column-end: 3; +} + +:host.col-end-4 { + grid-column-end: 4; +} + +:host.col-end-5 { + grid-column-end: 5; +} + +:host.col-end-6 { + grid-column-end: 6; +} + +:host.col-end-7 { + grid-column-end: 7; +} + +:host.col-end-8 { + grid-column-end: 8; +} + +:host.col-end-9 { + grid-column-end: 9; +} + +:host.col-end-10 { + grid-column-end: 10; +} + +:host.col-end-11 { + grid-column-end: 11; +} + +:host.col-end-12 { + grid-column-end: 12; +} + +:host.col-end-13 { + grid-column-end: 13; +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/column/column.component.spec.ts b/core-web/libs/sdk/angular/src/lib/layout/column/column.component.spec.ts new file mode 100644 index 000000000000..71dd2f4c298f --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/column/column.component.spec.ts @@ -0,0 +1,33 @@ +import { Spectator, createComponentFactory } from '@ngneat/spectator'; +import { MockComponent } from 'ng-mocks'; + +import { ColumnComponent } from './column.component'; + +import { DotPageAssetLayoutColumn } from '../../models'; +import { PageResponseMock } from '../../utils/testing.utils'; +import { ContainerComponent } from '../container/container.component'; + +describe('ColumnComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: ColumnComponent, + imports: [MockComponent(ContainerComponent)] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + column: PageResponseMock.layout.body.rows[0].columns[0] as DotPageAssetLayoutColumn + } + }); + }); + + it('should render one container', () => { + expect(spectator.queryAll(ContainerComponent)?.length).toBe(1); + }); + + it('should set correct containerClasses', () => { + expect(spectator.component.containerClasses).toBe('col-start-1 col-end-13'); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/layout/column/column.component.ts b/core-web/libs/sdk/angular/src/lib/layout/column/column.component.ts new file mode 100644 index 000000000000..226fd9e26783 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/column/column.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input, OnInit } from '@angular/core'; + +import { DotPageAssetLayoutColumn } from '../../models'; +import { getPositionStyleClasses } from '../../utils'; +import { ContainerComponent } from '../container/container.component'; + +@Component({ + selector: 'dotcms-column', + standalone: true, + imports: [ContainerComponent], + template: ` + @for(container of column.containers; track $index) { + + } + `, + styleUrl: './column.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ColumnComponent implements OnInit { + @Input() column!: DotPageAssetLayoutColumn; + @HostBinding('class') containerClasses = ''; + + ngOnInit() { + const { startClass, endClass } = getPositionStyleClasses( + this.column.leftOffset, + this.column.width + this.column.leftOffset + ); + this.containerClasses = `${startClass} ${endClass}`; + } +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/container/container.component.css b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.css new file mode 100644 index 000000000000..1fd8ac7e4ca1 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.css @@ -0,0 +1,9 @@ +:host.empty-container { + width: 100%; + background-color: #ecf0fd; + display: flex; + justify-content: center; + align-items: center; + color: #030e32; + height: 10rem; +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/container/container.component.html b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.html new file mode 100644 index 000000000000..cbb02b0be8a3 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.html @@ -0,0 +1,17 @@ +@if ($isInsideEditor()) { @if($contentlets().length){ @for (contentlet of $contentlets(); track +$index) { + + + + +} } @else { This container is empty. } } @else { @for (contentlet of $contentlets(); track $index) { + +} } diff --git a/core-web/libs/sdk/angular/src/lib/layout/container/container.component.spec.ts b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.spec.ts new file mode 100644 index 000000000000..ae9214708c1b --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.spec.ts @@ -0,0 +1,145 @@ +import { Spectator, byTestId, byText, createComponentFactory } from '@ngneat/spectator'; +import { of } from 'rxjs'; + +import { Component, Input } from '@angular/core'; + +import { ContainerComponent } from './container.component'; + +import { NoComponent } from '../../components/no-component/no-component.component'; +import { DotCMSContainer, DotCMSContentlet } from '../../models'; +import { PageContextService } from '../../services/dotcms-context/page-context.service'; +import { PageResponseMock } from '../../utils/testing.utils'; + +@Component({ + selector: 'dotcms-mock-component', + standalone: true, + template: 'Hello world' +}) +class DotcmsSDKMockComponent { + @Input() contentlet!: DotCMSContentlet; +} + +describe('ContainerComponent', () => { + let spectator: Spectator; + + describe('inside editor', () => { + const createComponent = createComponentFactory({ + component: ContainerComponent, + detectChanges: false, + providers: [ + { + provide: PageContextService, + useValue: { + pageContextValue: { + pageAsset: { + containers: PageResponseMock.containers + }, + components: { + Banner: of(DotcmsSDKMockComponent) + }, + isInsideEditor: true + } + } + } + ] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + container: PageResponseMock.layout.body.rows[0].columns[0] + .containers[0] as DotCMSContainer + } + }); + }); + + it('should render MockContainerComponent', () => { + spectator.detectChanges(); + expect(spectator.query(DotcmsSDKMockComponent)).toBeTruthy(); + }); + + it('should container have data attributes', () => { + spectator.detectChanges(); + const container = spectator.debugElement.nativeElement; + expect(container?.getAttribute('data-dot-accept-types')).toBeDefined(); + expect(container?.getAttribute('data-dot-identifier')).toBeDefined(); + expect(container?.getAttribute('data-max-contentlets')).toBeDefined(); + expect(container?.getAttribute('ata-dot-uuid')).toBeDefined(); + }); + + it('should contentlets have data attributes', () => { + spectator.detectChanges(); + const contentlets = spectator.queryAll(byTestId('dot-contentlet')); + contentlets.forEach((contentlet) => { + expect(contentlet.getAttribute('data-dot-identifier')).toBeDefined(); + expect(contentlet.getAttribute('data-dot-basetype')).toBeDefined(); + expect(contentlet.getAttribute('data-dot-title')).toBeDefined(); + expect(contentlet.getAttribute('data-dot-inode')).toBeDefined(); + expect(contentlet.getAttribute('data-dot-type')).toBeDefined(); + expect(contentlet.getAttribute('data-dot-container')).toBeDefined(); + expect(contentlet.getAttribute('data-dot-on-number-of-pages')).toBeDefined(); + }); + }); + + it('should render NoComponentComponent when no component is found', () => { + spectator.setInput( + 'container', + PageResponseMock.layout.body.rows[1].columns[0].containers[0] as DotCMSContainer + ); + spectator.detectChanges(); + expect(spectator.query(NoComponent)).toBeTruthy(); + }); + + it('should render message when container is empty', () => { + spectator.setInput( + 'container', + PageResponseMock.layout.body.rows[2].columns[0].containers[0] as DotCMSContainer + ); + spectator.detectChanges(); + expect(spectator.query(byText('This container is empty.'))).toBeTruthy(); + }); + }); + + describe('outside editor', () => { + const createComponent = createComponentFactory({ + component: ContainerComponent, + detectChanges: false, + providers: [ + { + provide: PageContextService, + useValue: { + pageContextValue: { + pageAsset: { + containers: PageResponseMock.containers + }, + components: { + Banner: of(DotcmsSDKMockComponent) + }, + isInsideEditor: false + } + } + } + ] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + container: PageResponseMock.layout.body.rows[0].columns[0] + .containers[0] as DotCMSContainer + } + }); + }); + + it('should not have data attributes', () => { + spectator.detectChanges(); + const container = spectator.query(byTestId('dot-container')); + expect(container?.getAttribute('data-dot-accept-types')).toBeUndefined(); + + const contentlets = spectator.queryAll(byTestId('dot-contentlet')); + contentlets.forEach((contentlet) => { + expect(contentlet.getAttribute('data-dot-identifier')).toBeUndefined(); + }); + }); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/layout/container/container.component.ts b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.ts new file mode 100644 index 000000000000..c7b733897498 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/container/container.component.ts @@ -0,0 +1,88 @@ +import { AsyncPipe, NgComponentOutlet } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + HostBinding, + Input, + OnChanges, + computed, + inject, + signal +} from '@angular/core'; + +import { NoComponent } from '../../components/no-component/no-component.component'; +import { DotCMSContainer, DotCMSContentlet, DynamicComponentEntity } from '../../models'; +import { PageContextService } from '../../services/dotcms-context/page-context.service'; +import { getContainersData } from '../../utils'; +import { ContentletComponent } from '../contentlet/contentlet.component'; + +interface DotContainer { + acceptTypes: string; + identifier: string; + maxContentlets: number; + uuid: string; + variantId?: string; +} + +@Component({ + selector: 'dotcms-container', + standalone: true, + imports: [AsyncPipe, NgComponentOutlet, NoComponent, ContentletComponent], + templateUrl: './container.component.html', + styleUrl: './container.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ContainerComponent implements OnChanges { + @Input({ required: true }) container!: DotCMSContainer; + + private readonly pageContextService: PageContextService = inject(PageContextService); + protected readonly NoComponent = NoComponent; + protected readonly $isInsideEditor = signal(false); + + protected componentsMap!: Record; + protected $contentlets = signal([]); + protected $dotContainer = signal(null); + protected $dotContainerAsString = computed(() => JSON.stringify(this.$dotContainer())); + + @HostBinding('attr.data-dot-accept-types') acceptTypes: string | null = null; + @HostBinding('attr.data-dot-identifier') identifier: string | null = null; + @HostBinding('attr.data-max-contentlets') maxContentlets: number | null = null; + @HostBinding('attr.data-dot-uuid') uuid: string | null = null; + @HostBinding('class') class: string | null = null; + @HostBinding('attr.data-dot-object') dotObject: string | null = null; + @HostBinding('attr.data-testid') testId = 'dot-container'; + + ngOnChanges() { + const { + pageAsset: { containers }, + components, + isInsideEditor + } = this.pageContextService.pageContextValue; + const { acceptTypes, maxContentlets, variantId, path, contentlets } = getContainersData( + containers, + this.container + ); + const { identifier, uuid } = this.container; + + this.componentsMap = components; + + this.$isInsideEditor.set(isInsideEditor); + this.$contentlets.set(contentlets); + this.$dotContainer.set({ + identifier: path ?? identifier, + acceptTypes, + maxContentlets, + variantId, + uuid + }); + + if (this.$isInsideEditor()) { + this.acceptTypes = acceptTypes; + this.identifier = identifier; + this.maxContentlets = maxContentlets; + this.uuid = uuid; + this.class = this.$contentlets().length ? null : 'empty-container'; + this.dotObject = 'container'; + } + } +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.spec.ts b/core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.spec.ts new file mode 100644 index 000000000000..79bd326682db --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContentletComponent } from './contentlet.component'; + +describe('ContentletComponent', () => { + let component: ContentletComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ContentletComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(ContentletComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.ts b/core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.ts new file mode 100644 index 000000000000..f462f23ea9da --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/contentlet/contentlet.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input, OnChanges } from '@angular/core'; + +import { DotCMSContentlet } from '../../models'; + +@Component({ + selector: 'dotcms-contentlet-wrapper', + standalone: true, + template: '', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ContentletComponent implements OnChanges { + @Input({ required: true }) contentlet!: DotCMSContentlet; + @Input() container!: string; + + @HostBinding('attr.data-dot-identifier') identifier: string | null = null; + @HostBinding('attr.data-dot-basetype') baseType: string | null = null; + @HostBinding('attr.data-dot-title') title: string | null = null; + @HostBinding('attr.data-dot-inode') inode: string | null = null; + @HostBinding('attr.data-dot-type') dotType: string | null = null; + @HostBinding('attr.data-dot-container') dotContainer: string | null = null; + @HostBinding('attr.data-dot-on-number-of-pages') numberOfPages: string | null = null; + @HostBinding('attr.data-dot-object') dotContent: string | null = null; + + ngOnChanges() { + this.identifier = this.contentlet.identifier; + this.baseType = this.contentlet.baseType; + this.title = this.contentlet.title; + this.inode = this.contentlet.inode; + this.dotType = this.contentlet.contentType; + this.dotContainer = this.container; + this.numberOfPages = this.contentlet['onNumberOfPages']; + this.dotContent = 'contentlet'; + } +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.css b/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.css new file mode 100644 index 000000000000..7f26ddcb5503 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts b/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts new file mode 100644 index 000000000000..fd6d020d4a9e --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts @@ -0,0 +1,87 @@ +import { Spectator } from '@ngneat/spectator'; +import { createRoutingFactory } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; + +import { Component, Input } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import * as dotcmsClient from '@dotcms/client'; + +import { PageResponseMock } from './../../utils/testing.utils'; +import { DotcmsLayoutComponent } from './dotcms-layout.component'; + +import { DotCMSContentlet, DotCMSPageAsset } from '../../models'; +import { PageContextService } from '../../services/dotcms-context/page-context.service'; +import { RowComponent } from '../row/row.component'; + +@Component({ + selector: 'dotcms-mock-component', + standalone: true, + template: 'Hello world' +}) +class DotcmsSDKMockComponent { + @Input() contentlet!: DotCMSContentlet; +} + +jest.mock('@dotcms/client', () => ({ + isInsideEditor: jest.fn().mockReturnValue(true), + initEditor: jest.fn(), + updateNavigation: jest.fn() +})); + +describe('DotcmsLayoutComponent', () => { + let spectator: Spectator; + + const createComponent = createRoutingFactory({ + component: DotcmsLayoutComponent, + imports: [MockComponent(RowComponent)], + providers: [ + { provide: ActivatedRoute, useValue: { url: of([]) } }, + { provide: Router, useValue: {} }, + { + provide: PageContextService, + useValue: { + setContext: jest.fn() + } + } + ] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + pageAsset: PageResponseMock as unknown as DotCMSPageAsset, + components: { + Banner: Promise.resolve(DotcmsSDKMockComponent) + } + } + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render rows', () => { + spectator.detectChanges(); + expect(spectator.queryAll(RowComponent).length).toBe(3); + }); + + it('should save pageContext', () => { + spectator.detectChanges(); + jest.spyOn(spectator.inject(PageContextService), 'setContext'); + expect(spectator.inject(PageContextService).setContext).toHaveBeenCalled(); + }); + + describe('inside editor', () => { + it('should call initEditor and updateNavigation from @dotcms/client', () => { + const initEditorSpy = jest.spyOn(dotcmsClient, 'initEditor'); + const updateNavigationSpy = jest.spyOn(dotcmsClient, 'updateNavigation'); + + spectator.detectChanges(); + expect(initEditorSpy).toHaveBeenCalled(); + expect(updateNavigationSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.ts b/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.ts new file mode 100644 index 000000000000..85f80e0f60e2 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/dotcms-layout/dotcms-layout.component.ts @@ -0,0 +1,72 @@ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + Input, + OnInit, + inject +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { filter } from 'rxjs/operators'; + +import { initEditor, isInsideEditor, updateNavigation } from '@dotcms/client'; + +import { DotCMSPageAsset, DynamicComponentEntity } from '../../models'; +import { PageContextService } from '../../services/dotcms-context/page-context.service'; +import { RowComponent } from '../row/row.component'; + +/** + * `DotcmsLayoutComponent` is a class that represents the layout for a DotCMS page. + * It includes a `pageAsset` property that represents the DotCMS page asset and a `components` property that represents the dynamic components for the page. + * + * @export + * @class DotcmsLayoutComponent + */ +@Component({ + selector: 'dotcms-layout', + standalone: true, + imports: [RowComponent], + template: `@for(row of pageAsset.layout.body.rows; track $index) { + + }`, + styleUrl: './dotcms-layout.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotcmsLayoutComponent implements OnInit { + @Input({ required: true }) pageAsset!: DotCMSPageAsset; + @Input({ required: true }) components!: Record; + + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly pageContextService = inject(PageContextService); + private readonly destroyRef$ = inject(DestroyRef); + + ngOnInit() { + this.route.url + .pipe( + takeUntilDestroyed(this.destroyRef$), + filter(() => isInsideEditor()) + ) + .subscribe((urlSegments) => { + const pathname = '/' + urlSegments.join('/'); + const config = { + pathname, + onReload: () => { + // Reload the page when the user edit the page + this.router.navigate([pathname], { + onSameUrlNavigation: 'reload' // Force Angular to reload the page + }); + } + }; + initEditor(config); + updateNavigation(pathname || '/'); + }); + } + + ngOnChanges() { + //Each time the layout changes, we need to update the context + this.pageContextService.setContext(this.pageAsset, this.components); + } +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/row/row.component.css b/core-web/libs/sdk/angular/src/lib/layout/row/row.component.css new file mode 100644 index 000000000000..d1ba185aadce --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/row/row.component.css @@ -0,0 +1,6 @@ +:host { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 1rem; + row-gap: 1rem; +} diff --git a/core-web/libs/sdk/angular/src/lib/layout/row/row.component.spec.ts b/core-web/libs/sdk/angular/src/lib/layout/row/row.component.spec.ts new file mode 100644 index 000000000000..3c6441552de8 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/row/row.component.spec.ts @@ -0,0 +1,28 @@ +import { Spectator, createComponentFactory } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; + +import { RowComponent } from './row.component'; + +import { DotPageAssetLayoutRow } from '../../models'; +import { PageResponseMock } from '../../utils/testing.utils'; +import { ColumnComponent } from '../column/column.component'; + +describe('RowComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: RowComponent, + imports: [MockComponent(ColumnComponent)] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + row: PageResponseMock.layout.body.rows[1] as DotPageAssetLayoutRow + } + }); + }); + + it('should render two columns', () => { + expect(spectator.queryAll(ColumnComponent)?.length).toBe(4); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/layout/row/row.component.ts b/core-web/libs/sdk/angular/src/lib/layout/row/row.component.ts new file mode 100644 index 000000000000..dcbcfd51c078 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/layout/row/row.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { DotPageAssetLayoutRow } from '../../models'; +import { ColumnComponent } from '../column/column.component'; + +@Component({ + selector: 'dotcms-row', + standalone: true, + imports: [ColumnComponent], + template: `@for(column of row.columns; track $index) { + + }`, + styleUrl: './row.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RowComponent { + @Input({ required: true }) row!: DotPageAssetLayoutRow; +} diff --git a/examples/angular/src/app/lib/models/dotcms.model.ts b/core-web/libs/sdk/angular/src/lib/models/dotcms.model.ts similarity index 98% rename from examples/angular/src/app/lib/models/dotcms.model.ts rename to core-web/libs/sdk/angular/src/lib/models/dotcms.model.ts index f961d16357fd..c19cd96fbbe7 100644 --- a/examples/angular/src/app/lib/models/dotcms.model.ts +++ b/core-web/libs/sdk/angular/src/lib/models/dotcms.model.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export interface DotCMSContainer { identifier: string; uuid: string; @@ -301,7 +302,7 @@ interface DotCMSSiteContentType { permissionType: string; } -interface DotCMSSiteParentPermissionable { +export interface DotCMSSiteParentPermissionable { Inode: string; Identifier: string; permissionByIdentifier: boolean; @@ -404,10 +405,10 @@ interface DotCMSSiteField { versionType: string; } -export interface DotCMSNavigationItem { +export interface DotcmsNavigationItem { code?: any; folder: string; - children?: DotCMSNavigationItem[]; + children?: DotcmsNavigationItem[]; host: string; languageId: number; href: string; diff --git a/core-web/libs/sdk/angular/src/lib/models/index.ts b/core-web/libs/sdk/angular/src/lib/models/index.ts new file mode 100644 index 000000000000..dc94812bc02f --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/models/index.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export * from './dotcms.model'; + +import { Type } from '@angular/core'; + +import { DotCMSPageAsset } from './dotcms.model'; +export type DynamicComponentEntity = Promise>; + +export interface DotCMSPageContext { + pageAsset: DotCMSPageAsset; + components: DotCMSPageComponent; + isInsideEditor: boolean; +} + +export type DotCMSPageComponent = Record; diff --git a/core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.service.ts b/core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.service.ts new file mode 100644 index 000000000000..6cb209d9b843 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.service.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Injectable } from '@angular/core'; + +import { isInsideEditor } from '@dotcms/client'; + +import { DotCMSPageAsset, DotCMSPageComponent, DotCMSPageContext } from '../../models'; + +@Injectable({ + providedIn: 'root' +}) +export class PageContextService { + private pageContext: DotCMSPageContext | null = null; + + get pageContextValue(): DotCMSPageContext { + return this.pageContext as DotCMSPageContext; + } + + /** + * Set the context + * + * @protected + * @param {DotCMSPageAsset} value + * @memberof DotcmsContextService + */ + setContext(pageAsset: DotCMSPageAsset, components: DotCMSPageComponent) { + this.pageContext = { + components, + pageAsset, + isInsideEditor: isInsideEditor() + }; + } +} diff --git a/core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.spec.ts b/core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.spec.ts new file mode 100644 index 000000000000..e3f40e410670 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/services/dotcms-context/page-context.spec.ts @@ -0,0 +1,50 @@ +import { SpectatorService, createServiceFactory } from '@ngneat/spectator'; + +import { TestBed } from '@angular/core/testing'; + +import { PageContextService } from './page-context.service'; + +import { DotCMSPageAsset, DotCMSPageComponent } from '../../models'; + +describe('PageContextService', () => { + let spectator: SpectatorService; + let service: PageContextService; + + const createService = createServiceFactory(PageContextService); + + beforeEach(() => { + TestBed.configureTestingModule({}); + spectator = createService(); + service = spectator.service; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should set the context', () => { + const pageAssetMock = {} as DotCMSPageAsset; + const componentsMock = {} as DotCMSPageComponent; + + service.setContext(pageAssetMock, componentsMock); + + expect(service.pageContextValue).toEqual({ + components: componentsMock, + pageAsset: pageAssetMock, + isInsideEditor: false + }); + }); + + it('should return the context', () => { + const pageAssetMock = {} as DotCMSPageAsset; + const componentsMock = {} as DotCMSPageComponent; + + service.setContext(pageAssetMock, componentsMock); + + expect(service.pageContextValue).toEqual({ + components: componentsMock, + pageAsset: pageAssetMock, + isInsideEditor: false + }); + }); +}); diff --git a/core-web/libs/sdk/angular/src/lib/utils/index.ts b/core-web/libs/sdk/angular/src/lib/utils/index.ts new file mode 100644 index 000000000000..bea51abf40dd --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/utils/index.ts @@ -0,0 +1,70 @@ +import { DotCMSContainer, DotCMSPageAssetContainer } from '../models'; + +//Changed the type, to avoid SQ issue. +//This should be put inside a lib +const endClassMap: Record = { + 1: 'col-end-1', + 2: 'col-end-2', + 3: 'col-end-3', + 4: 'col-end-4', + 5: 'col-end-5', + 6: 'col-end-6', + 7: 'col-end-7', + 8: 'col-end-8', + 9: 'col-end-9', + 10: 'col-end-10', + 11: 'col-end-11', + 12: 'col-end-12', + 13: 'col-end-13' +}; + +//Changed the type, to avoid SQ issue. +//This should be put inside a lib +const startClassMap: Record = { + 1: 'col-start-1', + 2: 'col-start-2', + 3: 'col-start-3', + 4: 'col-start-4', + 5: 'col-start-5', + 6: 'col-start-6', + 7: 'col-start-7', + 8: 'col-start-8', + 9: 'col-start-9', + 10: 'col-start-10', + 11: 'col-start-11', + 12: 'col-start-12' +}; + +export const getContainersData = ( + containers: DotCMSPageAssetContainer, + containerRef: DotCMSContainer +) => { + const { identifier, uuid } = containerRef; + + const { containerStructures, container } = containers[identifier]; + + const { variantId } = container?.parentPermissionable || {}; + + const acceptTypes: string = containerStructures + .map((structure) => structure.contentTypeVar) + .join(','); + + const contentlets = containers[identifier].contentlets[`uuid-${uuid}`]; + + return { + ...containers[identifier].container, + acceptTypes, + contentlets, + variantId + }; +}; + +export const getPositionStyleClasses = (start: number, end: number) => { + const startClass = startClassMap[start]; + const endClass = endClassMap[end]; + + return { + startClass, + endClass + }; +}; diff --git a/core-web/libs/sdk/angular/src/lib/utils/testing.utils.ts b/core-web/libs/sdk/angular/src/lib/utils/testing.utils.ts new file mode 100644 index 000000000000..8369e63580f6 --- /dev/null +++ b/core-web/libs/sdk/angular/src/lib/utils/testing.utils.ts @@ -0,0 +1,981 @@ +export const PageResponseMock = { + canCreateTemplate: true, + containers: { + '//demo.dotcms.com/application/containers/default/': { + containerStructures: [ + { + id: '77ec7720-bad8-431c-a0a3-6443fe87af73', + structureId: 'ce7295c8-df36-46c0-9c98-2fb764e9ec1c', + containerInode: 'f6a7e729-56f5-4fc0-a045-9711960b7427', + containerId: '69b3d24d-7e80-4be6-b04a-d352d16493ee', + code: '#dotParse("//demo.dotcms.com/application/containers/default/calltoaction.vtl")', + contentTypeVar: 'CallToAction' + } + ], + contentlets: { + 'uuid-1': [ + { + hostName: 'demo.dotcms.com', + modDate: 1599064929560, + imageMetaData: { + modDate: 1716268464265, + sha256: '041e8f2a721bf85fc833db50666a892ca9c5fcf816091cb6b6123a08ca701085', + length: 33386, + title: 'diving.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 33386, + name: 'diving.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064929608, + description: + 'World-class diving is at your doorstep. From shore or by boat, you have exclusive access to miles of pristine reefs, where diverse and dramatic undersea landscapes harbor the highest level of marine biodiversity on the planet.', + title: 'Diving', + body: '

Our dive destinations where created to deliver the ultimate dive experience and was established following an extensive search to identify the perfect location for a dive resort in terms of geography, climate, oceanic topography and marine biodiversity. Having identified the premier location, we developed resorts that, despite its remote position, offers a plenitude of facilities and comforts to make a dive trip, and all that surrounds it, an experience that you will cherish.

\n

Scuba
Unlimited diving on pristine, protected coral reefs for all divers be they open-circuit, technical or rebreather.

\n

Snorkeling
Ideal place for snorkelers. The house reef and many dive sites have beautiful shallows on the coral tops.

\n

Private Dive/Snorkel Guides
Let us fulfill your innermost wishes and tailor an exclusive, personalised diving experience to fit your individual needs and desires.

\n

Underwater photography
The photo opportunities here are stupendous and we offer support and equipment of a level that satisfies the many pros who visit us.

', + baseType: 'CONTENT', + inode: 'b77151f2-7206-4b03-862d-68217432d54d', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + owner: 'dotcms.org.1', + imageVersion: '/dA/b77151f2-7206-4b03-862d-68217432d54d/image/diving.jpg', + identifier: '4694d40b-d9be-4e09-b031-64ee3e7c9642', + image: '/dA/4694d40b-d9be-4e09-b031-64ee3e7c9642/image/diving.jpg', + imageContentAsset: '4694d40b-d9be-4e09-b031-64ee3e7c9642/image', + urlTitle: 'diving', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/diving', + creationDate: 1599064929560, + url: '/content.e9332c43-dfc2-4aa1-b0d6-60ca3df7cb76', + tags: 'snorkeling,waterenthusiast,diving,scuba', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/diving', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 3 + }, + { + hostName: 'demo.dotcms.com', + modDate: 1599064928581, + imageMetaData: { + modDate: 1716268464311, + sha256: '4cfeea66800ea90921d311c342ce98223786fa5eb31dc04871bcfe8dc4515800', + length: 21426, + title: 'box-info-3-270x270.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 21426, + name: 'box-info-3-270x270.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064928624, + description: + 'All winter locations have a variety of snowmobiles available for rent to all our registered guests.', + title: 'Snowmobiling', + body: '

Experience the rush of snowmobiling through spectacular terrain. With thousands of miles of groomed and ungroomed trails to choose from, you’ll never run out of new places to ride. When you’ve had your fill of thrills in the unmatched scenery, head back to town and warm up with hot cocoa—or a local brew—by a crackling fire.

\n

Discover the scenic backwoods with our experienced guides! Use one of our machines or bring you own!

A full service vacation that includes all meals, lodging, amenities, and guide service to 1500+ miles of groomed, secondary, and mountain snowmobile trails from your cabin door. There is also ample opportunity for you to enjoy “off trail ” play areas. Guests enjoy our hideaway location - with lots of places to snowmobile, awesome scenery, abundant wildlife, and no crowds. From our ranch you can access over 600 miles of groomed trails - with another1000 miles of secondary trails - and lots of off-trail play areas.

You could easily spend a week here and ride different country every day. Annual snowfall ranges from 140” at the ranch and up to 500” in the surrounding mountains!

', + baseType: 'CONTENT', + inode: 'bec05965-653e-4148-a59c-ac9682d91e54', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'Snowmobiling', + owner: 'dotcms.org.1', + imageVersion: + '/dA/bec05965-653e-4148-a59c-ac9682d91e54/image/box-info-3-270x270.jpg', + identifier: '6ac5921e-e062-49a6-9808-f41aff9343c5', + image: '/dA/6ac5921e-e062-49a6-9808-f41aff9343c5/image/box-info-3-270x270.jpg', + imageContentAsset: '6ac5921e-e062-49a6-9808-f41aff9343c5/image', + urlTitle: 'snowmobiling', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/snowmobiling', + creationDate: 1599064928581, + url: '/content.3614c9b2-f329-4d94-9c8e-143d5b254870', + tags: 'snowmobile,winterenthusiast', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/snowmobiling', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 2 + } + ], + 'uuid-2': [ + { + hostName: 'demo.dotcms.com', + modDate: 1617890012618, + imageMetaData: { + modDate: 1716268463813, + sha256: '01bed04a0807b45245d38188da3bece44e42fcdd0cf8e8bfe0585e8bd7a61913', + length: 15613, + title: 'box-info-2-270x270.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 15613, + name: 'box-info-2-270x270.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1617890012681, + description: + "Snowboarding, once a prime route for teen rebellion, today is definitely mainstream. Those teens — both guys and Shred Bettys, who took up snowboarding in the late '80s and '90s now are riding with their kids.", + title: 'Snowboarding', + body: "

As with skiing, there are different styles of riding. Free-riding is all-mountain snowboarding on the slopes, in the trees, down the steeps and through the moguls. Freestyle is snowboarding in a pipe or park filled with rails, fun boxes and other features.

Snowboarding parks are designed for specific skill levels, from beginner parks with tiny rails hugging the ground to terrain parks with roller-coaster rails, fun boxes and tabletops for more experienced snowboarders.

Whether you're a first-timer or already comfortable going lip-to-lip in a pipe, there are classes and special clinics for you at our ski and snowboard resorts. Our resorts offer multiday clinics, so if you're headed to ski this winter, consider wrapping your vacation dates around a snowboarding clinic.

", + baseType: 'CONTENT', + inode: 'd77576ce-6e3a-4cf3-b412-8e5209f56cae', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'Snowboarding', + owner: 'dotcms.org.1', + imageVersion: + '/dA/d77576ce-6e3a-4cf3-b412-8e5209f56cae/image/box-info-2-270x270.jpg', + identifier: '574f0aec-185a-4160-9c17-6d037b298318', + image: '/dA/574f0aec-185a-4160-9c17-6d037b298318/image/box-info-2-270x270.jpg', + imageContentAsset: '574f0aec-185a-4160-9c17-6d037b298318/image', + urlTitle: 'snowboarding', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/snowboarding', + creationDate: 1599064927605, + url: '/content.2f6fe5b8-a2cc-4ecb-a868-db632d695fca', + tags: 'snowboarding,winterenthusiast', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/snowboarding', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 2 + }, + { + hostName: 'demo.dotcms.com', + modDate: 1599064928089, + imageMetaData: { + modDate: 1716268463631, + sha256: '07b3d78da174c14744c865ac82e34cdada87f846f8e48cf55f9cc6731da0dc1b', + length: 18643, + title: 'surfing.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 18643, + name: 'surfing.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064928140, + description: + 'The search for the perfect wave is never-ending, but the journey itself is incredibly fulfilling. Visit one of our surf resorts and get one step closer to finding nirvana.', + title: 'Surfing', + body: '

Is your surfboard always leaning against the wall next to the door, waiting to be grabbed on the run? Do you organize your whole week around those hours spent outdoors doing what you love most? Are you happy when there’s an offshore wind and do you feel irritated when wind conditions are not quite right? Well, my friend, I’m afraid that you may already be addicted to surfing.

Regardless of your level, whether you’re into big wave surfing or still a kook, there’s no denying that surfing gives you quite the adrenaline rush. But it’s not necessarily the danger that gets you hooked, there are a bunch of other factors that contribute to the natural high: the setting, the anticipation, the rewards, all result in a surge of feel-good chemicals – endorphins, dopamine, and serotonin.

', + baseType: 'CONTENT', + inode: '3bf914e1-1f42-4ee1-8264-9e7b807af712', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'Surfing Vactions', + owner: 'dotcms.org.1', + imageVersion: '/dA/3bf914e1-1f42-4ee1-8264-9e7b807af712/image/surfing.jpg', + identifier: '8ccfa397-4369-44bb-b450-33151387eb02', + image: '/dA/8ccfa397-4369-44bb-b450-33151387eb02/image/surfing.jpg', + imageContentAsset: '8ccfa397-4369-44bb-b450-33151387eb02/image', + urlTitle: 'surfing', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/surfing', + creationDate: 1599064928089, + url: '/content.6def68a6-67f4-4f56-b8fc-118b3daab841', + tags: 'surfing,waterenthusiast', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/surfing', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 2 + } + ], + 'uuid-3': [ + { + hostName: 'demo.dotcms.com', + modDate: 1599064929068, + imageMetaData: { + modDate: 1716268464105, + sha256: '028e5fa94f67eecdac5616925246dfd713c8ddbe710fc1a6349512f1c59f378d', + length: 14358, + title: 'box-info-1-270x270.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 14358, + name: 'box-info-1-270x270.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064929116, + description: + "Skiing is the best family sport because it's so much more than a sport: it's a medium of shared experiences in a living, breathing culture of connection.", + title: 'Skiing', + body: "

Admiring the dramatic mountain landscape from atop the ski lift, feeling the cold air nipping at your cheeks, experiencing the rush of adrenaline as you carve the snow – there's nothing quite like a day on the slopes. And while the mountain conditions are top priority, our ski vacation spots also boast charming towns and lively après-ski scenes to explore. Factors like resort variety, scenic beauty, annual snowfall and overall atmosphere, we have the world's best places to ski. Wondering where to go? Take a peek at the destinations, and don't forget to vote for your favorites.

", + baseType: 'CONTENT', + inode: '278aa6dc-57cd-4cf6-ac18-df48c2430cb1', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'Skiing', + owner: 'dotcms.org.1', + imageVersion: + '/dA/278aa6dc-57cd-4cf6-ac18-df48c2430cb1/image/box-info-1-270x270.jpg', + identifier: '6a8102b5-fdb0-4ad5-9a5d-e982bcdb54c8', + image: '/dA/6a8102b5-fdb0-4ad5-9a5d-e982bcdb54c8/image/box-info-1-270x270.jpg', + imageContentAsset: '6a8102b5-fdb0-4ad5-9a5d-e982bcdb54c8/image', + urlTitle: 'skiing', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/skiing', + creationDate: 1599064929068, + url: '/content.97dded8a-f260-4730-aa99-56c9ea01c16d', + tags: 'skiing,winterenthusiast', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/skiing', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 2 + }, + { + hostName: 'demo.dotcms.com', + modDate: 1599064927114, + imageMetaData: { + modDate: 1716268463430, + sha256: 'd87e67d27c4f1b431d44ef5fdcadc2b1f816d55e60796bba0ae0dd9b90cfe02c', + length: 22168, + title: 'costa-rica-fishing.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 22168, + name: 'costa-rica-fishing.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064927161, + description: + 'Do you know exactly which fish you are trying to catch? If not, you can learn about all the different types of fish you can catch in Costa Rica. Plus we let you know where the best places to catch them.', + title: 'Sport Fishing', + body: '

Encompassing an area as vast as the European continent, French Polynesia is a playground for amateur sport fishermen. French Polynesian fishermen head out armed only with a fishing pole and a harness to the great blue where they confront species known for their fighting qualities: tuna, swordfish and of course the Marlin (Haura,in Tahitian). The largest of the catch can weigh over 500 kilos!

\n

The fact that Polynesian waters harbor so many of these giants, draws in many sport fishermen. At the end of March, the 11th Tahitian Billfish Tournament will bring together over 60 fishing boats and expects to attract many foreign crews. During the four-day tournament, the boats will search out these huge fish along the coasts of Tahiti for a truly adrenalin charged experience.

', + baseType: 'CONTENT', + inode: 'aeb77b14-02ab-42ec-aea6-499c8e0bcba1', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'Costa Rica boasts some of the best sportfishing', + owner: 'dotcms.org.1', + imageVersion: + '/dA/aeb77b14-02ab-42ec-aea6-499c8e0bcba1/image/costa-rica-fishing.jpg', + identifier: '50351143-3ba6-4c54-9e9b-0d8a90d2f9b0', + image: '/dA/50351143-3ba6-4c54-9e9b-0d8a90d2f9b0/image/costa-rica-fishing.jpg', + imageContentAsset: '50351143-3ba6-4c54-9e9b-0d8a90d2f9b0/image', + urlTitle: 'sport-fishing', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/sport-fishing', + creationDate: 1599064927114, + url: '/content.b1dcbe8a-d81f-405e-a176-4aa4a6a0b0a3', + tags: 'sport fishing,fishing', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/sport-fishing', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 2 + } + ], + 'uuid-4': [ + { + hostName: 'demo.dotcms.com', + modDate: 1599064926137, + imageMetaData: { + modDate: 1716268463519, + sha256: '729049464994cc3b93b27f9478e907107e7407a14544c619d6efa90dd19a16f3', + length: 24219, + title: 'white-water-raft-2.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 24219, + name: 'white-water-raft-2.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064926184, + description: + 'For those looking for a thrill, we offer some of the worlds bets white water rafting. Class II - IV rivers.', + title: 'White Water Rafting', + body: '

Between the year-round warm water and the natural beauty the country has to offer, a trip to Costa Rica presents the opportunity for world-class rafting in a tropical paradise. Costa Rica has 14 major river systems that begin in the volcanic mountain ranges and flow out towards the Caribbean Sea, the Pacific Ocean, the San Juan River, or Lake Nicaragua — with plenty of waterfalls along the way. These rivers produce world-famous white-water rapids, as well as beautiful scenery along the banks.

Choose Your White Water Rafting Adventure.
We have a wide range of options when it comes to rafting adventures, with both single day tours and multi-day white water excursions available, and river difficulties varying from the calm stroll of Class I rivers to the extreme thrill of Class V rivers. So, whether you’d like to take an easy float down the river to spot wildlife with the whole family, or you’re interested in challenging yourself on adrenaline-packed rapids with nicknames such as “La Boca del Diablo,” there’s a whitewater rafting trip for you.

', + baseType: 'CONTENT', + inode: '72736205-e217-4f69-a28c-4e3496512d73', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'White Water Rafting in Tropical Rain Forests', + owner: 'dotcms.org.1', + imageVersion: + '/dA/72736205-e217-4f69-a28c-4e3496512d73/image/white-water-raft-2.jpg', + identifier: '0e9340f8-08d2-46e3-ae25-be0137c575d0', + image: '/dA/0e9340f8-08d2-46e3-ae25-be0137c575d0/image/white-water-raft-2.jpg', + imageContentAsset: '0e9340f8-08d2-46e3-ae25-be0137c575d0/image', + urlTitle: 'white-water-rafting', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/white-water-rafting', + creationDate: 1599064926137, + url: '/content.a5f36908-8c57-46c0-9ee5-c37e065087d9', + tags: 'white water rafting,ecoenthusiast', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/white-water-rafting', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 3 + }, + { + hostName: 'demo.dotcms.com', + modDate: 1599064930049, + imageMetaData: { + modDate: 1716268463545, + sha256: '7e1cf9d3c8c144f592af72658456031c8283bfe4a5ecce3e188c71aa7b1e590e', + length: 37207, + title: 'zip-line.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 37207, + name: 'zip-line.jpg', + width: 270, + contentType: 'image/jpeg', + height: 270 + }, + publishDate: 1599064930101, + description: + 'Ever wondered what it is like to fly through the forest canopy? Zip-lining ais the best way to explore the forest canopy, where thick branches serve as platforms for the adventurous traveler, more than 100 feet above the forest floor.', + title: 'Zip-Lining', + body: '

Ever wondered what it is a monkey finds so fascinating about the forest canopy? Costa Rica is a pioneer in canopy exploration, where thick branches serve as platforms for the adventurous traveler, more than 100 feet above the jungle floor. If you’re wondering why you’d want to head to the top of a tree just to look around, remember that 90% of Costa Rica animals and 50% of plant species in rainforests live in the upper levels of the trees. When you explore that far off the ground, the view is something you’ll never forget! A Costa Rica zip line tour, hanging bridges hike, or aerial tram tour are all fantastic ways to take advantage of Costa Rica’s stunning forest canopy views.

\n

Almost anyone of any age and physical condition can enjoy a Costa Rica zip line adventure as it is not strenuous. Secured into a harness and attached to a sturdy cable, you will have the opportunity to fly through the rainforest canopy and experience a bird’s eye view of the lively forest below. A Costa Rica zip line is about a five hour adventure that operates rain or shine all year and is led by bilingual guides. For the non-adrenaline junkie, the aerial tram can take you through the rainforest in comfort and safety. Prefer to linger? Hanging bridges offer panoramic views for acres, and an experienced guide will be happy to point out a variety of birds and animals.

', + baseType: 'CONTENT', + inode: '8df9a375-0386-401c-b5d6-da21c1c5c301', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '778f3246-9b11-4a2a-a101-e7fdf111bdad', + contentType: 'Activity', + live: true, + altTag: 'Zip-line', + owner: 'dotcms.org.1', + imageVersion: '/dA/8df9a375-0386-401c-b5d6-da21c1c5c301/image/zip-line.jpg', + identifier: '50757fb4-75df-4e2c-8335-35d36bdb944b', + image: '/dA/50757fb4-75df-4e2c-8335-35d36bdb944b/image/zip-line.jpg', + imageContentAsset: '50757fb4-75df-4e2c-8335-35d36bdb944b/image', + urlTitle: 'zip-lining', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + URL_MAP_FOR_CONTENT: '/activities/zip-lining', + creationDate: 1599064930050, + url: '/content.c17f9c4c-ad14-4777-ae61-334ee6c9fcbf', + tags: 'ecoenthusiast,zip-lining', + titleImage: 'image', + modUserName: 'Admin User', + urlMap: '/activities/zip-lining', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 2 + } + ] + }, + container: { + archived: false, + categoryId: 'f6a7e729-56f5-4fc0-a045-9711960b7427', + deleted: false, + friendlyName: 'container', + hostId: '48190c8c-42c4-46af-8d1a-0cd5db894797', + hostName: 'demo.dotcms.com', + iDate: 1687784158222, + idate: 1687784158222, + identifier: '69b3d24d-7e80-4be6-b04a-d352d16493ee', + inode: 'f6a7e729-56f5-4fc0-a045-9711960b7427', + languageId: 1, + live: true, + locked: false, + maxContentlets: 25, + title: 'Default' + } + }, + '//demo.dotcms.com/application/containers/banner/': { + containerStructures: [ + { + id: 'f87da091-44eb-4662-8cd1-16d31afa14a8', + structureId: '4c441ada-944a-43af-a653-9bb4f3f0cb2b', + containerInode: '1d9165ad-f8ea-4006-8017-b103eb0d9f9b', + containerId: '5e8c9a71-8cae-4d96-a2f7-d25b9cf69a83', + code: '#dotParse("//demo.dotcms.com/application/containers/banner/banner.vtl")', + contentTypeVar: 'Banner' + } + ], + contentlets: { + 'uuid-1': [ + { + hostName: 'demo.dotcms.com', + modDate: 1716324004216, + imageMetaData: { + modDate: 1716268463471, + sha256: 'd9c3c87a691fe062ce370ddda2e9a71fa333739260711f93d5a82f56a08c1703', + length: 1158082, + title: 'adventure-boat-exotic-1371360.jpg', + editableAsText: false, + version: 20220201, + isImage: true, + fileSize: 1158082, + name: 'adventure-boat-exotic-1371360.jpg', + width: 1400, + contentType: 'image/jpeg', + height: 679 + }, + link: '/store/category/apparel/#products-title', + publishDate: 1716324004446, + caption: 'Look Great Doing it. Stock up on New Apparel!!', + title: 'Explore the World II', + baseType: 'CONTENT', + inode: '84534373-582a-4e62-8189-03abd0435778', + archived: false, + ownerName: 'Admin User', + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '4c441ada-944a-43af-a653-9bb4f3f0cb2b', + contentType: 'Banner', + live: true, + owner: 'dotcms.org.1', + imageVersion: + '/dA/84534373-582a-4e62-8189-03abd0435778/image/adventure-boat-exotic-1371360.jpg', + identifier: '2e5d54e6-7ea3-4d72-8577-b8731b206ca0', + buttonText: '20% Off - Shop Now', + image: '/dA/2e5d54e6-7ea3-4d72-8577-b8731b206ca0/image/adventure-boat-exotic-1371360.jpg', + imageContentAsset: '2e5d54e6-7ea3-4d72-8577-b8731b206ca0/image', + publishUserName: 'Admin User', + publishUser: 'dotcms.org.1', + languageId: 1, + creationDate: 1599247961410, + textColor: '#FFFFFF', + url: '/content.8b12d62d-5253-44b9-a2d0-8c4e5ec06e68', + tags: 'beach', + layout: '1', + titleImage: 'image', + modUserName: 'Admin User', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: true, + sortOrder: 0, + modUser: 'dotcms.org.1', + onNumberOfPages: 1 + } + ] + }, + container: { + archived: false, + categoryId: '1d9165ad-f8ea-4006-8017-b103eb0d9f9b', + deleted: false, + friendlyName: 'container', + hostId: '48190c8c-42c4-46af-8d1a-0cd5db894797', + hostName: 'demo.dotcms.com', + iDate: 1600437115745, + idate: 1600437115745, + identifier: '5e8c9a71-8cae-4d96-a2f7-d25b9cf69a83', + inode: '1d9165ad-f8ea-4006-8017-b103eb0d9f9b', + languageId: 1, + live: true, + locked: false, + maxContentlets: 25, + sortOrder: 0, + source: 'FILE', + title: 'Banner', + type: 'containers', + useDiv: false, + versionId: '5e8c9a71-8cae-4d96-a2f7-d25b9cf69a83', + versionType: 'containers', + working: true + } + }, + SYSTEM_CONTAINER: { + containerStructures: [], + contentlets: { + 'uuid-1': [] + }, + container: { + archived: false, + categoryId: 'SYSTEM_CONTAINER', + code: '\n\n\n#set($language = $languagewebapi.getLanguage("$!{dotContentMap.languageId}"))\n\n
\n \n
\n
\n
\n

$!{dotContentMap.title}

\n
\n $!{dotContentMap.contentType.name}\n
\n \n
\n
\n\n', + deleted: false, + friendlyName: 'System Container', + iDate: 1716356412597, + idate: 1716356412597, + identifier: 'SYSTEM_CONTAINER', + inode: 'SYSTEM_CONTAINER', + live: true, + locked: false, + useDiv: false, + versionId: 'SYSTEM_CONTAINER', + versionType: 'containers', + working: false + } + } + }, + layout: { + width: '', + title: 'anonymouslayout1600437132653', + header: true, + footer: true, + body: { + rows: [ + { + columns: [ + { + containers: [ + { + identifier: '//demo.dotcms.com/application/containers/banner/', + uuid: '1' + } + ], + widthPercent: 100, + leftOffset: 1, + styleClass: 'banner-tall', + preview: false, + width: 12, + left: 0 + } + ], + styleClass: 'p-0 banner-tall' + }, + { + columns: [ + { + containers: [ + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '1' + } + ], + widthPercent: 25, + leftOffset: 1, + styleClass: '', + preview: false, + width: 3, + left: 0 + }, + { + containers: [ + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '2' + } + ], + widthPercent: 25, + leftOffset: 4, + styleClass: '', + preview: false, + width: 3, + left: 3 + }, + { + containers: [ + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '3' + } + ], + widthPercent: 25, + leftOffset: 7, + styleClass: '', + preview: false, + width: 3, + left: 6 + }, + { + containers: [ + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '4' + } + ], + widthPercent: 25, + leftOffset: 10, + styleClass: '', + preview: false, + width: 3, + left: 9 + } + ], + styleClass: null + }, + { + columns: [ + { + containers: [ + { + identifier: 'SYSTEM_CONTAINER', + uuid: '1' + } + ], + widthPercent: 100, + leftOffset: 1, + styleClass: '', + preview: false, + width: 12, + left: 0 + } + ], + styleClass: null + } + ] + }, + sidebar: null + }, + numberContents: 9, + page: { + __icon__: 'pageIcon', + archived: false, + baseType: 'HTMLPAGE', + cachettl: '0', + canEdit: true, + canLock: true, + canRead: true, + canonicalUrl: '/', + contentType: 'htmlpageasset', + creationDate: 1599065449560, + deleted: false, + description: 'TravelLux - Your Destination for Exclusive Experiences', + extension: 'page', + folder: 'SYSTEM_FOLDER', + friendlyName: 'TravelLux - Your Destination for Exclusive Experiences', + hasLiveVersion: true, + hasTitleImage: true, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + hostName: 'demo.dotcms.com', + httpsRequired: false, + identifier: 'a9f30020-54ef-494e-92ed-645e757171c2', + inode: 'f9ce2b8e-eb06-4218-b036-9f266e2113b9', + isContentlet: true, + languageId: 1, + live: true, + liveInode: 'f9ce2b8e-eb06-4218-b036-9f266e2113b9', + locked: true, + lockedBy: 'dotcms.org.1', + lockedByName: 'Admin User', + lockedOn: 1690313868046, + metaDescription: + 'dotCMS provides a user-friendly interface for creating, editing, and publishing content. This starter site is designed to demonstrate some the basic features of dotCMS.', + mimeType: 'application/dotpage', + modDate: 1689887118429, + modUser: 'dotcms.org.1', + modUserName: 'Admin User', + name: 'index', + ogDescription: + 'dotCMS provides a user-friendly interface for creating, editing, and publishing content. This starter site is designed to demonstrate some the basic features of dotCMS.', + ogImage: 'eb372522595f11163666d29c786d5a9e', + ogTitle: 'dotCMS Hybrid Content Management System Demo Site', + ogType: 'website', + owner: 'dotcms.org.1', + ownerName: 'Admin User', + pageTitle: 'dotCMS Demo Site', + pageURI: '/index', + pageUrl: 'index', + path: '/index', + publishDate: 1689887118802, + publishUser: 'dotcms.org.1', + publishUserName: 'Admin User', + searchEngineIndex: 'index,follow,snippet', + shortyLive: 'f9ce2b8eeb', + shortyWorking: 'f9ce2b8eeb', + sitemapImportance: '1.0', + sortOrder: 0, + stInode: 'c541abb1-69b3-4bc5-8430-5e09e5239cc8', + statusIcons: + "", + template: 'fdc739f6-fe53-4271-9c8c-a3e05d12fcac', + title: 'Home', + titleImage: 'ogImage', + type: 'htmlpage', + url: '/index', + working: true, + workingInode: 'f9ce2b8e-eb06-4218-b036-9f266e2113b9' + }, + site: { + lowIndexPriority: false, + variantId: 'DEFAULT', + default: true, + aliases: 'localhost\n127.0.0.1', + inode: '59bb8831-6706-4589-9ca0-ff74016e02b2', + hostname: 'demo.dotcms.com', + tagStorage: 'SYSTEM_HOST', + hostThumbnail: null, + systemHost: false, + parent: true, + structureInode: '855a2d72-f2f3-4169-8b04-ac5157c4380c', + name: 'demo.dotcms.com', + owner: 'dotcms.org.1', + permissionType: 'com.dotmarketing.portlets.contentlet.model.Contentlet', + permissionId: '48190c8c-42c4-46af-8d1a-0cd5db894797', + identifier: '48190c8c-42c4-46af-8d1a-0cd5db894797', + modDate: 1634235141702, + type: 'contentlet', + live: true, + host: 'SYSTEM_HOST', + new: false, + folder: 'SYSTEM_FOLDER', + languageId: 1, + modUser: 'dotcms.org.1', + title: 'demo.dotcms.com', + archived: false, + sortOrder: 0, + fileAsset: false, + working: true, + indexPolicyDependencies: 'DEFER', + categoryId: '59bb8831-6706-4589-9ca0-ff74016e02b2', + versionId: '48190c8c-42c4-46af-8d1a-0cd5db894797', + contentTypeId: '855a2d72-f2f3-4169-8b04-ac5157c4380c', + titleImage: null, + htmlpage: false, + dotAsset: false, + persona: false, + form: false, + vanityUrl: false, + keyValue: false, + languageVariable: false, + locked: false + }, + template: { + iDate: 1562013807321, + type: 'template', + owner: '', + inode: '446da365-352f-4ac9-8d0d-4083e68a787e', + identifier: 'fdc739f6-fe53-4271-9c8c-a3e05d12fcac', + source: 'DB', + title: 'anonymous_layout_1600437132653', + friendlyName: '', + modDate: 1716492043079, + modUser: 'system', + sortOrder: 0, + showOnMenu: false, + body: 'null', + image: '', + drawed: true, + drawedBody: + '{"width":"","title":"anonymouslayout1600437132653","header":true,"footer":true,"body":{"rows":[{"columns":[{"containers":[{"identifier":"//demo.dotcms.com/application/containers/banner/","uuid":"1"}],"widthPercent":100,"leftOffset":1,"styleClass":"banner-tall","preview":false,"width":12,"left":0}],"styleClass":"p-0 banner-tall"},{"columns":[{"containers":[{"identifier":"//demo.dotcms.com/application/containers/default/","uuid":"1"}],"widthPercent":25,"leftOffset":1,"styleClass":"","preview":false,"width":3,"left":0},{"containers":[{"identifier":"//demo.dotcms.com/application/containers/default/","uuid":"2"}],"widthPercent":25,"leftOffset":4,"styleClass":"","preview":false,"width":3,"left":3},{"containers":[{"identifier":"//demo.dotcms.com/application/containers/default/","uuid":"3"}],"widthPercent":25,"leftOffset":7,"styleClass":"","preview":false,"width":3,"left":6},{"containers":[{"identifier":"//demo.dotcms.com/application/containers/default/","uuid":"4"}],"widthPercent":25,"leftOffset":10,"styleClass":"","preview":false,"width":3,"left":9}]},{"columns":[{"containers":[{"identifier":"SYSTEM_CONTAINER","uuid":"1"}],"widthPercent":100,"leftOffset":1,"styleClass":"","preview":false,"width":12,"left":0}]}]}}', + countAddContainer: 0, + countContainers: 0, + theme: 'd7b0ebc2-37ca-4a5a-b769-e8a3ff187661', + header: 'null', + footer: 'null', + anonymous: true, + template: false, + live: false, + archived: false, + working: true, + deleted: false, + versionType: 'template', + versionId: 'fdc739f6-fe53-4271-9c8c-a3e05d12fcac', + permissionId: 'fdc739f6-fe53-4271-9c8c-a3e05d12fcac', + name: 'anonymous_layout_1600437132653', + locked: false, + permissionType: 'com.dotmarketing.portlets.templates.model.Template', + new: false, + categoryId: '446da365-352f-4ac9-8d0d-4083e68a787e', + idate: 1562013807321, + canEdit: true + }, + viewAs: { + visitor: { + tags: [], + device: 'COMPUTER', + isNew: true, + userAgent: { + operatingSystem: 'MAC_OS_X', + browser: 'CHROME12', + id: 50990865, + browserVersion: { + version: '0.0.0.0', + majorVersion: '125', + minorVersion: '0' + } + }, + referer: 'http://localhost:3000/', + personas: {} + }, + language: { + id: 1, + languageCode: 'en', + countryCode: 'US', + language: 'English', + country: 'United States', + isoCode: 'en-us' + }, + mode: 'EDIT_MODE', + variantId: 'DEFAULT' + } +}; + +export const NavMock = { + children: [ + { + code: null, + folder: 'fa455fb5-b961-4d0c-9e63-e79a8ba8622a', + hash: 1851560015, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + href: '/blog', + languageId: 1, + order: 0, + target: '_self', + title: 'Travel Blog', + type: 'folder' + }, + { + code: null, + folder: '6c8a2ac4-36a7-4b01-b9c0-c2c1d91ddfdb', + hash: 888578018, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + href: '/destinations', + languageId: 1, + order: 1, + target: '_self', + title: 'Destinations', + type: 'folder' + }, + { + code: null, + folder: '8027c7b2-61cb-46f0-ac81-3771efdfbe4c', + hash: 1519987246, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + href: '/store', + languageId: 1, + order: 2, + target: '_self', + title: 'Store', + type: 'folder' + }, + { + code: null, + folder: 'd54e36f2-12da-4721-a7c5-45fd3fb652cd', + hash: 1642975162, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + href: '/members', + languageId: 1, + order: 3, + target: '_self', + title: 'Members Only', + type: 'folder' + }, + { + code: null, + folder: '9c5e78a9-62ce-48e0-99ce-6817b0f5c2f3', + hash: 1807344928, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + href: '/contact-us', + languageId: 1, + order: 4, + target: '_self', + title: 'Contact Us', + type: 'folder' + } + ], + code: null, + folder: 'SYSTEM_FOLDER', + hash: 994054630, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + href: '/System foldersystem folder', + languageId: 1, + order: 0, + target: '_self', + title: 'System folder', + type: 'folder' +}; diff --git a/core-web/libs/sdk/angular/src/test-setup.ts b/core-web/libs/sdk/angular/src/test-setup.ts new file mode 100644 index 000000000000..a0634c4b03fd --- /dev/null +++ b/core-web/libs/sdk/angular/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true + } +}; +import 'jest-preset-angular/setup-jest'; diff --git a/core-web/libs/sdk/angular/tsconfig.json b/core-web/libs/sdk/angular/tsconfig.json new file mode 100644 index 000000000000..2928604897e2 --- /dev/null +++ b/core-web/libs/sdk/angular/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/core-web/libs/sdk/angular/tsconfig.lib.json b/core-web/libs/sdk/angular/tsconfig.lib.json new file mode 100644 index 000000000000..d62a8082de0b --- /dev/null +++ b/core-web/libs/sdk/angular/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/core-web/libs/sdk/angular/tsconfig.lib.prod.json b/core-web/libs/sdk/angular/tsconfig.lib.prod.json new file mode 100644 index 000000000000..034aa3afea25 --- /dev/null +++ b/core-web/libs/sdk/angular/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": true + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/core-web/libs/sdk/angular/tsconfig.spec.json b/core-web/libs/sdk/angular/tsconfig.spec.json new file mode 100644 index 000000000000..cf500c23cef7 --- /dev/null +++ b/core-web/libs/sdk/angular/tsconfig.spec.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/core-web/nx.json b/core-web/nx.json index 9cb5369fe733..30d83278737c 100644 --- a/core-web/nx.json +++ b/core-web/nx.json @@ -121,6 +121,11 @@ "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] + }, + "@nx/angular:package": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } }, "namedInputs": { diff --git a/core-web/tsconfig.base.json b/core-web/tsconfig.base.json index 3682df4e420a..41932ad351f5 100644 --- a/core-web/tsconfig.base.json +++ b/core-web/tsconfig.base.json @@ -19,6 +19,7 @@ "paths": { "@components/*": ["apps/dotcms-ui/src/app/view/components/*"], "@directives/*": ["apps/dotcms-ui/src/app/view/directives/*"], + "@dotcms/angular": ["libs/sdk/angular/src/index.ts"], "@dotcms/app/*": ["apps/dotcms-ui/src/app/*"], "@dotcms/block-editor": ["libs/block-editor/src/public-api.ts"], "@dotcms/client": ["libs/sdk/client/src/index.ts"], diff --git a/core-web/yarn.lock b/core-web/yarn.lock index 6fb595fc88d1..5b505c279258 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -8517,7 +8517,7 @@ autoprefixer@10.4.18: picocolors "^1.0.0" postcss-value-parser "^4.2.0" -autoprefixer@^10.4.16, autoprefixer@^10.4.6, autoprefixer@^10.4.9: +autoprefixer@^10.4.0, autoprefixer@^10.4.16, autoprefixer@^10.4.6, autoprefixer@^10.4.9: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== diff --git a/examples/angular/package-lock.json b/examples/angular/package-lock.json index e398c124590a..163ce79466a8 100644 --- a/examples/angular/package-lock.json +++ b/examples/angular/package-lock.json @@ -8,15 +8,14 @@ "name": "angular", "version": "0.0.0", "dependencies": { - "@angular/animations": "^17.0.0", - "@angular/common": "^17.0.0", - "@angular/compiler": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/forms": "^17.0.0", - "@angular/platform-browser": "^17.0.0", - "@angular/platform-browser-dynamic": "^17.0.0", - "@angular/router": "^17.0.0", - "@dotcms/client": "^0.0.1-alpha.14", + "@angular/animations": "^17.1.0", + "@angular/common": "^17.1.0", + "@angular/compiler": "^17.1.0", + "@angular/core": "^17.1.0", + "@angular/forms": "^17.1.0", + "@angular/platform-browser": "^17.1.0", + "@angular/platform-browser-dynamic": "^17.1.0", + "@angular/router": "^17.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.2" @@ -24,7 +23,7 @@ "devDependencies": { "@angular-devkit/build-angular": "^17.0.7", "@angular/cli": "^17.0.7", - "@angular/compiler-cli": "^17.0.0", + "@angular/compiler-cli": "^17.1.0", "@types/jasmine": "~5.1.0", "autoprefixer": "^10.4.19", "jasmine-core": "~5.1.0", @@ -2393,11 +2392,6 @@ "node": ">=10.0.0" } }, - "node_modules/@dotcms/client": { - "version": "0.0.1-alpha.14", - "resolved": "https://registry.npmjs.org/@dotcms/client/-/client-0.0.1-alpha.14.tgz", - "integrity": "sha512-QrpwuSH/hJM8W0eJYedYg2UsPM03/lR35ReDHy91EjPuvXHNJQmiva6+oQNlkbBqdhySDBjl/wQo/e7ELE8QEw==" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", diff --git a/examples/angular/package.json b/examples/angular/package.json index bdaada73941b..ab453eef05be 100644 --- a/examples/angular/package.json +++ b/examples/angular/package.json @@ -10,15 +10,14 @@ }, "private": true, "dependencies": { - "@angular/animations": "^17.0.0", - "@angular/common": "^17.0.0", - "@angular/compiler": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/forms": "^17.0.0", - "@angular/platform-browser": "^17.0.0", - "@angular/platform-browser-dynamic": "^17.0.0", - "@angular/router": "^17.0.0", - "@dotcms/client": "^0.0.1-alpha.17", + "@angular/animations": "^17.1.0", + "@angular/common": "^17.1.0", + "@angular/compiler": "^17.1.0", + "@angular/core": "^17.1.0", + "@angular/forms": "^17.1.0", + "@angular/platform-browser": "^17.1.0", + "@angular/platform-browser-dynamic": "^17.1.0", + "@angular/router": "^17.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.2" @@ -26,7 +25,7 @@ "devDependencies": { "@angular-devkit/build-angular": "^17.0.7", "@angular/cli": "^17.0.7", - "@angular/compiler-cli": "^17.0.0", + "@angular/compiler-cli": "^17.1.0", "@types/jasmine": "~5.1.0", "autoprefixer": "^10.4.19", "jasmine-core": "~5.1.0", diff --git a/examples/angular/src/app/app.component.ts b/examples/angular/src/app/app.component.ts index 4b1d101c1911..68bc8174f385 100644 --- a/examples/angular/src/app/app.component.ts +++ b/examples/angular/src/app/app.component.ts @@ -1,14 +1,12 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; - -import { DotcmsLayoutComponent } from './lib/layout/dotcms-layout/dotcms-layout.component'; +import { DotcmsLayoutComponent } from '@dotcms/angular'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, DotcmsLayoutComponent], templateUrl: './app.component.html', - styleUrl: './app.component.css' + styleUrl: './app.component.css', }) -export class AppComponent { -} +export class AppComponent {} diff --git a/examples/angular/src/app/app.config.ts b/examples/angular/src/app/app.config.ts index 66c81e226858..b7d0b16a3cce 100644 --- a/examples/angular/src/app/app.config.ts +++ b/examples/angular/src/app/app.config.ts @@ -5,9 +5,7 @@ import { ClientConfig } from '@dotcms/client'; import { routes } from './app.routes'; import { environment } from '../environments/environment'; - -import { provideDotCMSClient } from './lib/dotcms-client-token'; -import { PageContextService } from './lib/services/dotcms-context/page-context.service'; +import { provideDotcmsClient } from './client-token/dotcms-client'; const DOTCMS_CLIENT_CONFIG: ClientConfig = { dotcmsUrl: environment.dotcmsUrl, @@ -17,8 +15,7 @@ const DOTCMS_CLIENT_CONFIG: ClientConfig = { export const appConfig: ApplicationConfig = { providers: [ + provideDotcmsClient(DOTCMS_CLIENT_CONFIG), provideRouter(routes), - provideDotCMSClient(DOTCMS_CLIENT_CONFIG), - PageContextService ], }; diff --git a/examples/angular/src/app/app.routes.ts b/examples/angular/src/app/app.routes.ts index 2310128e5aa8..c1964f9a726e 100644 --- a/examples/angular/src/app/app.routes.ts +++ b/examples/angular/src/app/app.routes.ts @@ -1,6 +1,6 @@ import { Routes } from '@angular/router'; import { DotCMSPagesComponent } from './pages/pages.component'; -import { DotCMSPageResolver } from './lib/resolver/dotcms-page.resolver'; +import { DotCMSPageResolver } from './resolver/dotcms-page.resolver'; export const routes: Routes = [ { @@ -12,4 +12,4 @@ export const routes: Routes = [ component: DotCMSPagesComponent, runGuardsAndResolvers: 'always' // Run the resolver on every navigation. Even if the URL hasn't changed. }, -]; +]; \ No newline at end of file diff --git a/examples/angular/src/app/client-token/dotcms-client.ts b/examples/angular/src/app/client-token/dotcms-client.ts new file mode 100644 index 000000000000..6bcd4402229b --- /dev/null +++ b/examples/angular/src/app/client-token/dotcms-client.ts @@ -0,0 +1,20 @@ +import { EnvironmentProviders, InjectionToken, makeEnvironmentProviders } from '@angular/core'; + +import { ClientConfig, DotCmsClient, dotcmsClient } from '@dotcms/client'; + +export const DOTCMS_CLIENT_TOKEN = new InjectionToken('DOTCMS_CLIENT'); + +/** + * This is a provider for the `DOTCMS_CLIENT_TOKEN` token. + * + * @param {*} config + * @return {*} + */ +export const provideDotcmsClient = (config: ClientConfig): EnvironmentProviders => { + return makeEnvironmentProviders([ + { + provide: DOTCMS_CLIENT_TOKEN, + useValue: dotcmsClient.init(config) + } + ]); +}; diff --git a/examples/angular/src/app/lib/components/no-component/no-component.component.css b/examples/angular/src/app/lib/components/no-component/no-component.component.css deleted file mode 100644 index 5d4e87f30f63..000000000000 --- a/examples/angular/src/app/lib/components/no-component/no-component.component.css +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/examples/angular/src/app/lib/components/no-component/no-component.component.ts b/examples/angular/src/app/lib/components/no-component/no-component.component.ts deleted file mode 100644 index 9731fa92d9e5..000000000000 --- a/examples/angular/src/app/lib/components/no-component/no-component.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { DotCMSContentlet } from '../../models'; - -/** - * This is part of the Angular SDK. - * This is a component for the `NoComponentComponent` component. - */ -@Component({ - selector: 'app-no-component', - standalone: true, - imports: [ - CommonModule, - ], - template: `
No Component for {{contentlet.contentType}}
`, - styleUrl: './no-component.component.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class NoComponentComponent { - @Input() contentlet!: DotCMSContentlet; -} diff --git a/examples/angular/src/app/lib/dotcms-client-token.ts b/examples/angular/src/app/lib/dotcms-client-token.ts deleted file mode 100644 index f217abc7c298..000000000000 --- a/examples/angular/src/app/lib/dotcms-client-token.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { InjectionToken, Provider } from '@angular/core'; -import { ClientConfig, DotCmsClient, dotcmsClient } from '@dotcms/client'; - -export const DOTCMS_CLIENT_TOKEN = new InjectionToken('DOTCMS_CLIENT'); - -/** - * This is a provider for the `DOTCMS_CLIENT_TOKEN` token. - * - * @param {*} config - * @return {*} - */ -export const provideDotCMSClient = (config: ClientConfig): Provider => { - return { - provide: DOTCMS_CLIENT_TOKEN, - useValue: dotcmsClient.init(config) - } -} diff --git a/examples/angular/src/app/lib/layout/column/column.component.css b/examples/angular/src/app/lib/layout/column/column.component.css deleted file mode 100644 index 771b6d3b84fe..000000000000 --- a/examples/angular/src/app/lib/layout/column/column.component.css +++ /dev/null @@ -1,99 +0,0 @@ -:host.col-start-1 { - grid-column-start: 1; -} - -:host.col-start-2 { - grid-column-start: 2; -} - -:host.col-start-3 { - grid-column-start: 3; -} - -:host.col-start-4 { - grid-column-start: 4; -} - -:host.col-start-5 { - grid-column-start: 5; -} - -:host.col-start-6 { - grid-column-start: 6; -} - -:host.col-start-7 { - grid-column-start: 7; -} - -:host.col-start-8 { - grid-column-start: 8; -} - -:host.col-start-9 { - grid-column-start: 9; -} - -:host.col-start-10 { - grid-column-start: 10; -} - -:host.col-start-11 { - grid-column-start: 11; -} - -:host.col-start-12 { - grid-column-start: 12; -} - -:host.col-end-1 { - grid-column-end: 1; -} - -:host.col-end-2 { - grid-column-end: 2; -} - -:host.col-end-3 { - grid-column-end: 3; -} - -:host.col-end-4 { - grid-column-end: 4; -} - -:host.col-end-5 { - grid-column-end: 5; -} - -:host.col-end-6 { - grid-column-end: 6; -} - -:host.col-end-7 { - grid-column-end: 7; -} - -:host.col-end-8 { - grid-column-end: 8; -} - -:host.col-end-9 { - grid-column-end: 9; -} - -:host.col-end-10 { - grid-column-end: 10; -} - -:host.col-end-11 { - grid-column-end: 11; -} - -:host.col-end-12 { - grid-column-end: 12; -} - -:host.col-end-13 { - grid-column-end: 13; -} diff --git a/examples/angular/src/app/lib/layout/column/column.component.spec.ts b/examples/angular/src/app/lib/layout/column/column.component.spec.ts deleted file mode 100644 index 9331ed4a3cda..000000000000 --- a/examples/angular/src/app/lib/layout/column/column.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ColumnComponent } from './column.component'; - -describe('ColumnComponent', () => { - let component: ColumnComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ColumnComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ColumnComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/examples/angular/src/app/lib/layout/column/column.component.ts b/examples/angular/src/app/lib/layout/column/column.component.ts deleted file mode 100644 index de4551c7d3b8..000000000000 --- a/examples/angular/src/app/lib/layout/column/column.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ChangeDetectionStrategy, Component, HostBinding, Input, OnInit } from '@angular/core'; - -import { ContainerComponent } from '../container/container.component'; -import { getPositionStyleClasses } from '../../utils'; -import { DotPageAssetLayoutColumn } from '../../models'; - -@Component({ - selector: 'dotcms-column', - standalone: true, - imports: [ContainerComponent], - template: ` - @for(container of column.containers; track $index) { - - } - `, - styleUrl: './column.component.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ColumnComponent implements OnInit { - @Input() column!: DotPageAssetLayoutColumn; - @HostBinding('class') containerClasses: string = ''; - - ngOnInit() { - const { startClass, endClass } = getPositionStyleClasses( - this.column.leftOffset, - this.column.width + this.column.leftOffset - ); - this.containerClasses = `${startClass} ${endClass}` - } -} diff --git a/examples/angular/src/app/lib/layout/container/container.component.css b/examples/angular/src/app/lib/layout/container/container.component.css deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/examples/angular/src/app/lib/layout/container/container.component.html b/examples/angular/src/app/lib/layout/container/container.component.html deleted file mode 100644 index 345cce70f96a..000000000000 --- a/examples/angular/src/app/lib/layout/container/container.component.html +++ /dev/null @@ -1,41 +0,0 @@ - -@if (isInsideEditor()) { -
- - @if(contentlets().length){ - @for (contentlet of contentlets(); track $index) { -
- -
- } - - } @else { - This container is empty. - } -
-} @else { - @for (contentlet of contentlets(); track $index) { - - } -} diff --git a/examples/angular/src/app/lib/layout/container/container.component.spec.ts b/examples/angular/src/app/lib/layout/container/container.component.spec.ts deleted file mode 100644 index d3876f4f0bda..000000000000 --- a/examples/angular/src/app/lib/layout/container/container.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ContainerComponent } from './container.component'; - -describe('ContainerComponent', () => { - let component: ContainerComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ContainerComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ContainerComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/examples/angular/src/app/lib/layout/container/container.component.ts b/examples/angular/src/app/lib/layout/container/container.component.ts deleted file mode 100644 index 32ac47287666..000000000000 --- a/examples/angular/src/app/lib/layout/container/container.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - Input, - OnChanges, - computed, - inject, - signal, -} from '@angular/core'; -import { AsyncPipe, NgComponentOutlet } from '@angular/common'; - -import { getContainersData } from '../../utils'; -import { - PageContextService, -} from '../../services/dotcms-context/page-context.service'; -import { NoComponentComponent } from '../../components/no-component/no-component.component'; -import { DotCMSContainer, DotCMSContentlet, DynamicComponentEntity } from '../../models'; - -interface DotContainer { - acceptTypes: string; - identifier: string; - maxContentlets: number; - uuid: string; - variantId?: string; -} - -const EMPTY_CONTAINER_EDIT_MODE_STYLES = { - width: '100%', - backgroundColor: '#ECF0FD', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - color: '#030E32', - height: '10rem', -}; - -@Component({ - selector: 'dotcms-container', - standalone: true, - imports: [AsyncPipe, NgComponentOutlet, NoComponentComponent], - templateUrl: './container.component.html', - styleUrl: './container.component.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ContainerComponent implements OnChanges { - @Input({ required: true }) container!: DotCMSContainer; - - private readonly pageContextService: PageContextService = inject(PageContextService); - protected readonly emptyContainerStyles: Record = EMPTY_CONTAINER_EDIT_MODE_STYLES; - protected readonly NoComponentComponent = NoComponentComponent; - protected readonly isInsideEditor = signal(false); - - protected componentsMap!: Record; - protected contentlets = signal([]); - protected dotContainer = signal(null); - protected dotContainerAsString = computed(() => - JSON.stringify(this.dotContainer()) - ); - - ngOnChanges() { - const { containers, isInsideEditor } = this.pageContextService.pageContextValue; - const { acceptTypes, maxContentlets, variantId, path, contentlets } = getContainersData(containers, this.container); - const { identifier, uuid } = this.container; - - this.componentsMap = this.pageContextService.getComponentMap(); - - this.isInsideEditor.set(isInsideEditor); - this.contentlets.set(contentlets); - this.dotContainer.set({ - identifier: path ?? identifier, - acceptTypes, - maxContentlets, - variantId, - uuid, - }); - } -} diff --git a/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.css b/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.css deleted file mode 100644 index 5d4e87f30f63..000000000000 --- a/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.css +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts b/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts deleted file mode 100644 index 21081f0c0754..000000000000 --- a/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DotcmsLayoutComponent } from './dotcms-layout.component'; - -describe('DotcmsLayoutComponent', () => { - let component: DotcmsLayoutComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DotcmsLayoutComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(DotcmsLayoutComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.ts b/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.ts deleted file mode 100644 index 1cc73a0c8b95..000000000000 --- a/examples/angular/src/app/lib/layout/dotcms-layout/dotcms-layout.component.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - DestroyRef, - Input, - OnInit, - inject, -} from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; - -import { initEditor, isInsideEditor, updateNavigation } from '@dotcms/client'; - -import { RowComponent } from '../row/row.component'; -import { PageContextService } from '../../services/dotcms-context/page-context.service'; -import { DotCMSPageAsset, DynamicComponentEntity } from '../../models'; - -@Component({ - selector: 'dotcms-layout', - standalone: true, - imports: [RowComponent], - template: `@for(row of entity.layout.body.rows; track $index) { - - }`, - styleUrl: './dotcms-layout.component.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DotcmsLayoutComponent implements OnInit { - @Input({ required: true }) entity!: DotCMSPageAsset; - @Input({ required: true }) components!: Record< - string, - DynamicComponentEntity - >; - - private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); - private readonly pageContextService = inject(PageContextService); - private readonly destroyRef$ = inject(DestroyRef); - - ngOnInit() { - this.pageContextService.setComponentMap(this.components); - - this.route.url - .pipe(takeUntilDestroyed(this.destroyRef$)) - .subscribe((urlSegments) => { - if (isInsideEditor()) { - const pathname = '/' + urlSegments.join('/'); - const config = { - pathname, - onReload: () => { - // Reload the page when the user edit the page - this.router.navigate([pathname], { - onSameUrlNavigation: 'reload', // Force Angular to reload the page - }); - }, - }; - initEditor(config); - updateNavigation(pathname || '/'); - } - }); - } -} diff --git a/examples/angular/src/app/lib/layout/row/row.component.css b/examples/angular/src/app/lib/layout/row/row.component.css deleted file mode 100644 index 6f38aedbde1b..000000000000 --- a/examples/angular/src/app/lib/layout/row/row.component.css +++ /dev/null @@ -1,6 +0,0 @@ -:host { - display: grid; - grid-template-columns: repeat(12, 1fr); - gap: 1rem; - row-gap: 1rem; -} diff --git a/examples/angular/src/app/lib/layout/row/row.component.spec.ts b/examples/angular/src/app/lib/layout/row/row.component.spec.ts deleted file mode 100644 index d2645e8ca3b0..000000000000 --- a/examples/angular/src/app/lib/layout/row/row.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { RowComponent } from './row.component'; - -describe('RowComponent', () => { - let component: RowComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [RowComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(RowComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/examples/angular/src/app/lib/layout/row/row.component.ts b/examples/angular/src/app/lib/layout/row/row.component.ts deleted file mode 100644 index 40cfdb2068e0..000000000000 --- a/examples/angular/src/app/lib/layout/row/row.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; - -import { ColumnComponent } from '../column/column.component'; -import { DotPageAssetLayoutRow } from '../../models'; - -@Component({ - selector: 'dotcms-row', - standalone: true, - imports: [ColumnComponent], - template: `@for(column of row.columns; track $index) { - - }`, - styleUrl: './row.component.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class RowComponent { - @Input() row!: DotPageAssetLayoutRow; -} diff --git a/examples/angular/src/app/lib/models/index.ts b/examples/angular/src/app/lib/models/index.ts deleted file mode 100644 index ce13b7a7181c..000000000000 --- a/examples/angular/src/app/lib/models/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./dotcms.model"; - -import { Type } from "@angular/core"; -export type DynamicComponentEntity = Promise>; - diff --git a/examples/angular/src/app/lib/resolver/dotcms-page.resolver.spec.ts b/examples/angular/src/app/lib/resolver/dotcms-page.resolver.spec.ts deleted file mode 100644 index 8635719d6ded..000000000000 --- a/examples/angular/src/app/lib/resolver/dotcms-page.resolver.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { DotCMSPageResolver } from './dotcms-page.resolver'; - -describe('DotCMSPageResolver', () => { - - beforeEach(() => { - TestBed.configureTestingModule({}); - }); -}); diff --git a/examples/angular/src/app/lib/resolver/dotcms-page.resolver.ts b/examples/angular/src/app/lib/resolver/dotcms-page.resolver.ts deleted file mode 100644 index 5815329ffe02..000000000000 --- a/examples/angular/src/app/lib/resolver/dotcms-page.resolver.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { inject } from '@angular/core'; -import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router'; - -import { DOTCMS_CLIENT_TOKEN } from '../dotcms-client-token'; -import { DotCMSNavigationItem, DotCMSPageAsset } from '../models'; -import { PageContextService } from '../services/dotcms-context/page-context.service'; - -/** - * This resolver is used to fetch the page and navigation data from dotCMS. - * - * @param {ActivatedRouteSnapshot} route - * @param {RouterStateSnapshot} _state - * @return {*} - */ -export const DotCMSPageResolver: ResolveFn< - Promise<{ - pageAsset: DotCMSPageAsset; - nav: DotCMSNavigationItem; - }> -> = async (route: ActivatedRouteSnapshot) => { - const client = inject(DOTCMS_CLIENT_TOKEN); - const pageContextService = inject(PageContextService); - - const url = route.url.map((segment) => segment.path).join('/'); - const queryParams = route.queryParams; - - const pageProps = { - path: url || 'index', - language_id: queryParams['language_id'], - mode: queryParams['mode'], - variantName: queryParams['variantName'], - 'com.dotmarketing.persona.id': - queryParams['com.dotmarketing.persona.id'] || '', - }; - - const navProps = { - path: '/', - depth: 2, - languageId: queryParams['language_id'], - }; - - const pageRequest = client.page.get(pageProps) as Promise<{ entity: DotCMSPageAsset }>; - const navRequest = client.nav.get(navProps) as Promise<{ entity: DotCMSNavigationItem }>; - - const [pageResponse, navResponse] = await Promise.all([pageRequest, navRequest]); - - const pageAsset = pageResponse.entity; - const nav = navResponse.entity; - - pageContextService.setContext(pageAsset); - - return { pageAsset, nav }; -}; diff --git a/examples/angular/src/app/lib/services/dotcms-context/page-context.service.ts b/examples/angular/src/app/lib/services/dotcms-context/page-context.service.ts deleted file mode 100644 index 61f3a03cf5c7..000000000000 --- a/examples/angular/src/app/lib/services/dotcms-context/page-context.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; - -import { isInsideEditor } from '@dotcms/client'; - -import { DotCMSPageAsset, DynamicComponentEntity } from '../../models'; - -export interface ComponentItem { - component: Promise; - module?: Promise; -} - -export interface DotCMSPageContext extends DotCMSPageAsset { - isInsideEditor: boolean; -} - -@Injectable({ - providedIn: 'root' -}) -export class PageContextService { - - private componentsMap!: Record ; - private pageContext = new BehaviorSubject(null); - readonly pageContext$ = this.pageContext.asObservable() as Observable; - - get pageContextValue(): DotCMSPageContext { - return this.pageContext.value as DotCMSPageContext; - } - - getComponentMap(): Record { - return this.componentsMap; - } - - setComponentMap(components: Record ) { - this.componentsMap = components; - } - - /** - * Set the context - * - * @protected - * @param {DotCMSPageAsset} value - * @memberof DotcmsContextService - */ - setContext(value: DotCMSPageAsset) { - this.pageContext.next({ - ...value, - isInsideEditor: isInsideEditor() - }); - } -} diff --git a/examples/angular/src/app/lib/services/dotcms-context/page-context.spec.ts b/examples/angular/src/app/lib/services/dotcms-context/page-context.spec.ts deleted file mode 100644 index 203400db3f2d..000000000000 --- a/examples/angular/src/app/lib/services/dotcms-context/page-context.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { PageContextService } from './page-context.service'; - -describe('DotcmsContextService', () => { - let service: PageContextService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(PageContextService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/examples/angular/src/app/lib/utils/index.ts b/examples/angular/src/app/lib/utils/index.ts deleted file mode 100644 index 0e2466cdbc65..000000000000 --- a/examples/angular/src/app/lib/utils/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { DotCMSContainer, DotCMSPageAssetContainer } from "../models"; - -const endClassMap: Record = { - 1: 'col-end-1', - 2: 'col-end-2', - 3: 'col-end-3', - 4: 'col-end-4', - 5: 'col-end-5', - 6: 'col-end-6', - 7: 'col-end-7', - 8: 'col-end-8', - 9: 'col-end-9', - 10: 'col-end-10', - 11: 'col-end-11', - 12: 'col-end-12', - 13: 'col-end-13', -}; - -const startClassMap: Record = { - 1: 'col-start-1', - 2: 'col-start-2', - 3: 'col-start-3', - 4: 'col-start-4', - 5: 'col-start-5', - 6: 'col-start-6', - 7: 'col-start-7', - 8: 'col-start-8', - 9: 'col-start-9', - 10: 'col-start-10', - 11: 'col-start-11', - 12: 'col-start-12', -}; - -export const getContainersData = (containers: DotCMSPageAssetContainer, containerRef: DotCMSContainer) => { - const { identifier, uuid } = containerRef; - - const { containerStructures, container } = containers[identifier]; - - // Get the variant id - const { variantId } = container?.parentPermissionable || {}; - - // Get accepts types of content types for this container - const acceptTypes: string = containerStructures - .map((structure) => structure.contentTypeVar) - .join(','); - - // Get the contentlets for "this" container - const contentlets = containers[identifier].contentlets[`uuid-${uuid}`]; - - return { - ...containers[identifier].container, - acceptTypes, - contentlets, - variantId, - }; -}; - -export const getPositionStyleClasses = (start: number, end: number) => { - const startClass = startClassMap[start]; - const endClass = endClassMap[end]; - - return { - startClass, - endClass, - }; -}; diff --git a/examples/angular/src/app/pages/components/navigation/navigation.component.ts b/examples/angular/src/app/pages/components/navigation/navigation.component.ts index 2456bf1fcfcc..36d06e69774f 100644 --- a/examples/angular/src/app/pages/components/navigation/navigation.component.ts +++ b/examples/angular/src/app/pages/components/navigation/navigation.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core'; -import { DotCMSNavigationItem } from '../../../lib/models'; import { Params, Router, RouterLink } from '@angular/router'; +import { DotcmsNavigationItem } from '@dotcms/angular'; @Component({ selector: 'app-navigation', @@ -27,7 +27,7 @@ import { Params, Router, RouterLink } from '@angular/router'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class NavigationComponent implements OnInit{ - @Input() items!: DotCMSNavigationItem[]; + @Input() items!: DotcmsNavigationItem[]; private readonly router = inject(Router) diff --git a/examples/angular/src/app/pages/content-types/activity/activity.component.ts b/examples/angular/src/app/pages/content-types/activity/activity.component.ts index fa6ef8f41309..0080a0471936 100644 --- a/examples/angular/src/app/pages/content-types/activity/activity.component.ts +++ b/examples/angular/src/app/pages/content-types/activity/activity.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { DotCMSContentlet } from '@dotcms/angular'; -import { DotCMSContentlet } from '../../../lib/models'; @Component({ selector: 'app-activity', diff --git a/examples/angular/src/app/pages/content-types/banner/banner.component.ts b/examples/angular/src/app/pages/content-types/banner/banner.component.ts index e41106bdeced..8a6120e22706 100644 --- a/examples/angular/src/app/pages/content-types/banner/banner.component.ts +++ b/examples/angular/src/app/pages/content-types/banner/banner.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { DotCMSContentlet } from '@dotcms/angular'; + -import { DotCMSContentlet } from '../../../lib/models'; @Component({ selector: 'app-banner', diff --git a/examples/angular/src/app/pages/content-types/image/image.component.ts b/examples/angular/src/app/pages/content-types/image/image.component.ts index 79a6a9738e0c..9a6ca4470f79 100644 --- a/examples/angular/src/app/pages/content-types/image/image.component.ts +++ b/examples/angular/src/app/pages/content-types/image/image.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { DotCMSContentlet } from '../../../lib/models'; +import { DotCMSContentlet } from '@dotcms/angular'; @Component({ selector: 'app-image', diff --git a/examples/angular/src/app/pages/content-types/product/product.component.ts b/examples/angular/src/app/pages/content-types/product/product.component.ts index 1033e6c87baf..c4f98c72e22a 100644 --- a/examples/angular/src/app/pages/content-types/product/product.component.ts +++ b/examples/angular/src/app/pages/content-types/product/product.component.ts @@ -5,8 +5,9 @@ import { Input, OnInit, } from '@angular/core'; -import { DotCMSContentlet } from '../../../lib/models'; + import { RouterLink } from '@angular/router'; +import { DotCMSContentlet } from '@dotcms/angular'; @Component({ selector: 'app-product', diff --git a/examples/angular/src/app/pages/content-types/web-page-content/web-page-content.component.ts b/examples/angular/src/app/pages/content-types/web-page-content/web-page-content.component.ts index 9325d59e6a88..5059faa101c2 100644 --- a/examples/angular/src/app/pages/content-types/web-page-content/web-page-content.component.ts +++ b/examples/angular/src/app/pages/content-types/web-page-content/web-page-content.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { DotCMSContentlet } from '../../../lib/models'; +import { DotCMSContentlet } from '@dotcms/angular'; @Component({ selector: 'app-web-page-content', diff --git a/examples/angular/src/app/pages/pages.component.html b/examples/angular/src/app/pages/pages.component.html index f1b97c667126..704632bc38d1 100644 --- a/examples/angular/src/app/pages/pages.component.html +++ b/examples/angular/src/app/pages/pages.component.html @@ -8,7 +8,7 @@ }
- +
@if(pageAsset.layout.footer) { diff --git a/examples/angular/src/app/pages/pages.component.ts b/examples/angular/src/app/pages/pages.component.ts index 273e12dc7898..aab52fcc2fa7 100644 --- a/examples/angular/src/app/pages/pages.component.ts +++ b/examples/angular/src/app/pages/pages.component.ts @@ -2,20 +2,25 @@ import { Component, DestroyRef, OnInit, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; -import { DotcmsLayoutComponent } from '../lib/layout/dotcms-layout/dotcms-layout.component'; - import { HeaderComponent } from './components/header/header.component'; import { FooterComponent } from './components/footer/footer.component'; import { NavigationComponent } from './components/navigation/navigation.component'; import { DYNAMIC_COMPONENTS } from '../utils'; +import { DotcmsLayoutComponent } from '@dotcms/angular'; + @Component({ selector: 'dotcms-pages', standalone: true, - imports: [DotcmsLayoutComponent, HeaderComponent, NavigationComponent, FooterComponent], + imports: [ + DotcmsLayoutComponent, + HeaderComponent, + NavigationComponent, + FooterComponent, + ], templateUrl: './pages.component.html', - styleUrl: './pages.component.css' + styleUrl: './pages.component.css', }) export class DotCMSPagesComponent implements OnInit { private readonly route = inject(ActivatedRoute); @@ -26,9 +31,10 @@ export class DotCMSPagesComponent implements OnInit { ngOnInit() { // Get the context data from the route - this.route.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => { - this.context.set(data['context']); - }); - + this.route.data + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((data) => { + this.context.set(data['context']); + }); } } diff --git a/examples/angular/src/app/resolver/dotcms-page.resolver.ts b/examples/angular/src/app/resolver/dotcms-page.resolver.ts new file mode 100644 index 000000000000..e755c1aa5b03 --- /dev/null +++ b/examples/angular/src/app/resolver/dotcms-page.resolver.ts @@ -0,0 +1,55 @@ +import { ActivatedRouteSnapshot } from '@angular/router'; +import { DotCMSPageAsset, DotcmsNavigationItem } from '@dotcms/angular'; +import { inject } from '@angular/core'; +import { DOTCMS_CLIENT_TOKEN } from '../client-token/dotcms-client'; + +/** + * This resolver is used to fetch the page and navigation data from dotCMS. + * + * @param {ActivatedRouteSnapshot} route + * @param {RouterStateSnapshot} _state + * @return {*} + */ +export const DotCMSPageResolver = async ( + route: ActivatedRouteSnapshot, +): Promise<{ + pageAsset: DotCMSPageAsset; + nav: DotcmsNavigationItem; +}> => { + const client = inject(DOTCMS_CLIENT_TOKEN); + + const url = route.url.map((segment) => segment.path).join('/'); + const queryParams = route.queryParams; + + const pageProps = { + path: url || 'index', + language_id: queryParams['language_id'], + mode: queryParams['mode'], + variantName: queryParams['variantName'], + 'com.dotmarketing.persona.id': + queryParams['com.dotmarketing.persona.id'] || '', + }; + + const navProps = { + path: '/', + depth: 2, + languageId: queryParams['language_id'], + }; + + const pageRequest = client.page.get(pageProps) as Promise<{ + entity: DotCMSPageAsset; + }>; + const navRequest = client.nav.get(navProps) as Promise<{ + entity: DotcmsNavigationItem; + }>; + + const [pageResponse, navResponse] = await Promise.all([ + pageRequest, + navRequest, + ]); + + const pageAsset = pageResponse.entity; + const nav = navResponse.entity; + + return { pageAsset, nav }; +}; diff --git a/examples/angular/src/app/utils/index.ts b/examples/angular/src/app/utils/index.ts index 74a0d8a32ee6..2fe8a4f084c3 100644 --- a/examples/angular/src/app/utils/index.ts +++ b/examples/angular/src/app/utils/index.ts @@ -1,19 +1,19 @@ -import { DynamicComponentEntity } from '../lib/models'; +import { DynamicComponentEntity } from '@dotcms/angular'; export const DYNAMIC_COMPONENTS: { [key: string]: DynamicComponentEntity } = { Activity: import('../pages/content-types/activity/activity.component').then( - (c) => c.ActivityComponent + (c) => c.ActivityComponent, ), Banner: import('../pages/content-types/banner/banner.component').then( - (c) => c.BannerComponent + (c) => c.BannerComponent, ), Image: import('../pages/content-types/image/image.component').then( - (c) => c.ImageComponent + (c) => c.ImageComponent, ), webPageContent: import( '../pages/content-types/web-page-content/web-page-content.component' ).then((c) => c.WebPageContentComponent), Product: import('../pages/content-types/product/product.component').then( - (c) => c.ProductComponent + (c) => c.ProductComponent, ), }; diff --git a/examples/angular/src/environments/environment.development.ts b/examples/angular/src/environments/environment.development.ts index 75824a488482..ebd38147d5f8 100644 --- a/examples/angular/src/environments/environment.development.ts +++ b/examples/angular/src/environments/environment.development.ts @@ -1,5 +1,5 @@ export const environment = { dotcmsUrl: "http://localhost:8080", - authToken: "YOUR_API_TOKEN", + authToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhcGllNTcyMjQ1NC0yMzg0LTRhZjQtYjFiNi00MjZmODZhNzQ0NGMiLCJ4bW9kIjoxNzE3MTIwMTA3MDAwLCJuYmYiOjE3MTcxMjAxMDcsImlzcyI6ImNkNTljOTljYzAiLCJsYWJlbCI6ImtkIiwiZXhwIjoxODExNzM5NjAwLCJpYXQiOjE3MTcxMjAxMDcsImp0aSI6ImYzZWZlY2E5LTZhNWEtNDEyYS04YjhmLWUwOTBjMmU1MTM3MiJ9.IrnvmsoUg0ldEgqWkquQey0i0nRnA9midTS2RUaAfto", siteId: "true", }; diff --git a/examples/angular/tsconfig.app.json b/examples/angular/tsconfig.app.json index 374cc9d294aa..dcd2a95f2bac 100644 --- a/examples/angular/tsconfig.app.json +++ b/examples/angular/tsconfig.app.json @@ -3,7 +3,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": [], }, "files": [ "src/main.ts" diff --git a/examples/angular/tsconfig.json b/examples/angular/tsconfig.json index f37b67ff0277..7129880a5921 100644 --- a/examples/angular/tsconfig.json +++ b/examples/angular/tsconfig.json @@ -19,10 +19,12 @@ "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, - "lib": [ - "ES2022", - "dom" - ] + "lib": ["ES2022", "dom"], + "paths": { + //This acts like npm link. + "@dotcms/angular": ["../../core-web/dist/libs/sdk/angular"], + "@dotcms/client": ["../../core-web/dist/libs/sdk/client"] + } }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false,