Skip to content

Commit 55a518d

Browse files
Merge branch 'main' into 31679-fix-rules-dialog
2 parents fa120c1 + e1fa535 commit 55a518d

55 files changed

Lines changed: 3468 additions & 233 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core-web/apps/dotcms-ui/src/app/app.routes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,19 @@ const PORTLETS_ANGULAR: Route[] = [
157157
},
158158
{
159159
path: 'tags',
160+
canActivate: [MenuGuardService],
161+
canActivateChild: [MenuGuardService],
160162
data: { reuseRoute: false },
161163
loadChildren: () => import('@dotcms/portlets/dot-tags/portlet').then((m) => m.dotTagsRoutes)
162164
},
165+
{
166+
path: 'plugins',
167+
canActivate: [MenuGuardService],
168+
canActivateChild: [MenuGuardService],
169+
data: { reuseRoute: false },
170+
loadChildren: () =>
171+
import('@dotcms/portlets/dot-plugins/portlet').then((m) => m.dotPluginsRoutes)
172+
},
163173
{
164174
path: '',
165175
canActivate: [MenuGuardService],

core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-apps-configuration-detail/components/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.html

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,96 @@
1616
</ng-template>
1717

1818
@for (field of $formFields(); track field) {
19-
<div [attr.data-testid]="field.name" class="field">
20-
@switch (field.type) {
21-
@case ('BUTTON') {
22-
<ng-container *ngTemplateOutlet="labelField; context: { field: field }" />
23-
<div>
24-
<button
25-
(click)="onIntegrate(field.value)"
19+
@if (field.type === 'HEADING') {
20+
<div
21+
class="dot-apps-configuration-detail__section-header"
22+
[attr.data-testid]="field.name">
23+
<h3>{{ field.label }}</h3>
24+
</div>
25+
} @else if (field.type === 'INFO') {
26+
<div
27+
class="dot-apps-configuration-detail__info-box"
28+
[attr.data-testid]="field.name">
29+
<markdown>{{ field.hint }}</markdown>
30+
</div>
31+
} @else {
32+
<div [attr.data-testid]="field.name" class="field">
33+
@switch (field.type) {
34+
@case ('BUTTON') {
35+
<ng-container
36+
*ngTemplateOutlet="labelField; context: { field: field }" />
37+
<div>
38+
<button
39+
(click)="onIntegrate(field.value)"
40+
[id]="field.name"
41+
[label]="field.label"
42+
[disabled]="!$appConfigured()"
43+
pButton
44+
type="button"></button>
45+
<ng-container
46+
*ngTemplateOutlet="warningIcon; context: { field: field }" />
47+
</div>
48+
<span class="form__group-hint">
49+
<markdown>{{ field.hint }}</markdown>
50+
</span>
51+
}
52+
@case ('STRING') {
53+
<ng-container
54+
*ngTemplateOutlet="labelField; context: { field: field }" />
55+
<ng-container
56+
*ngTemplateOutlet="warningIcon; context: { field: field }" />
57+
<textarea
58+
(click)="field.hidden ? $event.target.select() : null"
2659
[id]="field.name"
27-
[label]="field.label"
28-
[disabled]="!$appConfigured()"
29-
pButton
30-
type="button"></button>
60+
[formControlName]="field.name"
61+
#inputTextarea
62+
pTextarea
63+
[autoResize]="true"></textarea>
64+
<span class="p-field-hint">
65+
<markdown>{{ field.hint }}</markdown>
66+
</span>
67+
}
68+
@case ('GENERATED_STRING') {
69+
<ng-container
70+
*ngTemplateOutlet="labelField; context: { field: field }" />
71+
<dot-apps-configuration-detail-generated-string-field
72+
data-testid="generated-string-field"
73+
[formControlName]="field.name"
74+
[field]="field" />
75+
}
76+
@case ('BOOL') {
77+
<div class="flex items-center gap-2">
78+
<p-checkbox
79+
[class.required]="field.required"
80+
[inputId]="field.name"
81+
[formControlName]="field.name"
82+
[value]="field.value"
83+
[binary]="true" />
84+
<label [for]="field.name">{{ field.label }}</label>
85+
</div>
3186
<ng-container
3287
*ngTemplateOutlet="warningIcon; context: { field: field }" />
33-
</div>
34-
<span class="form__group-hint">
35-
<markdown>{{ field.hint }}</markdown>
36-
</span>
37-
}
38-
@case ('STRING') {
39-
<ng-container *ngTemplateOutlet="labelField; context: { field: field }" />
40-
<ng-container *ngTemplateOutlet="warningIcon; context: { field: field }" />
41-
<textarea
42-
(click)="field.hidden ? $event.target.select() : null"
43-
[id]="field.name"
44-
[formControlName]="field.name"
45-
#inputTextarea
46-
pTextarea
47-
[autoResize]="true"></textarea>
48-
<span class="p-field-hint">
49-
<markdown>{{ field.hint }}</markdown>
50-
</span>
51-
}
52-
@case ('GENERATED_STRING') {
53-
<ng-container *ngTemplateOutlet="labelField; context: { field: field }" />
54-
<dot-apps-configuration-detail-generated-string-field
55-
data-testid="generated-string-field"
56-
[formControlName]="field.name"
57-
[field]="field" />
58-
}
59-
@case ('BOOL') {
60-
<div class="flex items-center">
61-
<p-checkbox
62-
[class.required]="field.required"
63-
[inputId]="field.name"
88+
<span class="p-field-hint">
89+
<markdown>{{ field.hint }}</markdown>
90+
</span>
91+
}
92+
@case ('SELECT') {
93+
<ng-container
94+
*ngTemplateOutlet="labelField; context: { field: field }" />
95+
<ng-container
96+
*ngTemplateOutlet="warningIcon; context: { field: field }" />
97+
<p-select
98+
[id]="field.name"
6499
[formControlName]="field.name"
65-
[value]="field.value"
66-
[binary]="true" />
67-
<label [for]="field.name">{{ field.label }}</label>
68-
</div>
69-
<ng-container *ngTemplateOutlet="warningIcon; context: { field: field }" />
70-
<span class="p-field-hint">
71-
<markdown>{{ field.hint }}</markdown>
72-
</span>
73-
}
74-
@case ('SELECT') {
75-
<ng-container *ngTemplateOutlet="labelField; context: { field: field }" />
76-
<ng-container *ngTemplateOutlet="warningIcon; context: { field: field }" />
77-
<p-select
78-
[id]="field.name"
79-
[formControlName]="field.name"
80-
[class.required]="field.required"
81-
[options]="field.options" />
82-
<span class="p-field-hint">
83-
<markdown>{{ field.hint }}</markdown>
84-
</span>
100+
[class.required]="field.required"
101+
[options]="field.options" />
102+
<span class="p-field-hint">
103+
<markdown>{{ field.hint }}</markdown>
104+
</span>
105+
}
85106
}
86-
}
87-
</div>
107+
</div>
108+
}
88109
}
89110
</form>
90111
}

core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-apps-configuration-detail/components/dot-apps-configuration-detail-form/dot-apps-configuration-detail-form.component.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,34 @@ import { DotAppsConfigurationDetailFormComponent } from './dot-apps-configuratio
1818

1919
import { DotAppsConfigurationDetailGeneratedStringFieldComponent } from '../dot-apps-configuration-detail-generated-string-field/dot-apps-configuration-detail-generated-string-field.component';
2020

21+
const headingSecret = {
22+
dynamic: false,
23+
name: 'sectionHeader',
24+
hidden: false,
25+
hint: '',
26+
label: 'Server-Rendered Pages Configuration',
27+
required: false,
28+
type: 'HEADING',
29+
value: '',
30+
hasEnvVar: false,
31+
envShow: false,
32+
hasEnvVarValue: false
33+
};
34+
35+
const infoSecret = {
36+
dynamic: false,
37+
name: 'infoBox',
38+
hidden: false,
39+
hint: 'These settings apply ONLY to pages rendered by dotCMS servers.',
40+
label: '',
41+
required: false,
42+
type: 'INFO',
43+
value: '',
44+
hasEnvVar: false,
45+
envShow: false,
46+
hasEnvVarValue: false
47+
};
48+
2149
const secrets = [
2250
{
2351
dynamic: false,
@@ -315,6 +343,42 @@ describe('DotAppsConfigurationDetailFormComponent', () => {
315343
expect(spyValidOutput).toHaveBeenCalledTimes(3);
316344
});
317345

346+
it('should render HEADING field as section header with label text', () => {
347+
const spectatorWithHeading = createComponent({
348+
props: { formFields: [headingSecret, ...secrets] } as unknown
349+
});
350+
spectatorWithHeading.detectChanges();
351+
352+
const header = spectatorWithHeading.query('[data-testid="sectionHeader"]');
353+
expect(header).toBeTruthy();
354+
expect(header.classList).toContain('dot-apps-configuration-detail__section-header');
355+
expect(header.querySelector('h3').textContent.trim()).toBe(headingSecret.label);
356+
});
357+
358+
it('should render INFO field as info box with hint text', () => {
359+
const spectatorWithInfo = createComponent({
360+
props: { formFields: [infoSecret, ...secrets] } as unknown
361+
});
362+
spectatorWithInfo.detectChanges();
363+
364+
const infoBox = spectatorWithInfo.query('[data-testid="infoBox"]');
365+
expect(infoBox).toBeTruthy();
366+
expect(infoBox.classList).toContain('dot-apps-configuration-detail__info-box');
367+
expect(infoBox.querySelector('markdown')).toBeTruthy();
368+
});
369+
370+
it('should not add HEADING or INFO fields to the form group', () => {
371+
const spectatorWithExtra = createComponent({
372+
props: {
373+
formFields: [headingSecret, infoSecret, ...secrets]
374+
} as unknown
375+
});
376+
spectatorWithExtra.detectChanges();
377+
378+
expect(spectatorWithExtra.component.myFormGroup.contains('sectionHeader')).toBe(false);
379+
expect(spectatorWithExtra.component.myFormGroup.contains('infoBox')).toBe(false);
380+
});
381+
318382
it('should emit form state disabled when required field empty', () => {
319383
const spyValidOutput = jest.spyOn(spectator.component.valid, 'emit');
320384

core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ import {
3131
DotWorkflowActionsFireService,
3232
PaginatorService
3333
} from '@dotcms/data-access';
34-
import { SiteService } from '@dotcms/dotcms-js';
34+
import { DotcmsEventsService, SiteService } from '@dotcms/dotcms-js';
3535
import { DotSystemConfig } from '@dotcms/dotcms-models';
3636
import { DotFormDialogComponent, DotMessagePipe, DotApiLinkComponent } from '@dotcms/ui';
3737
import {
3838
DotCurrentUserServiceMock,
39+
DotcmsEventsServiceMock,
3940
MockDotMessageService,
4041
MockDotRouterService,
4142
mockDotThemes,
@@ -299,6 +300,10 @@ describe('DotTemplateCreateEditComponent', () => {
299300
get: jest.fn().mockReturnValue(of(mockDotThemes[1]))
300301
}
301302
},
303+
{
304+
provide: DotcmsEventsService,
305+
useValue: new DotcmsEventsServiceMock()
306+
},
302307
{ provide: DotSystemConfigService, useClass: MockDotSystemConfigService },
303308
{ provide: DotRouterService, useClass: MockDotRouterService }
304309
]

core-web/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-edit-contentlet/dot-edit-contentlet.component.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { of as observableOf } from 'rxjs';
22

3+
import { HttpTestingController } from '@angular/common/http/testing';
34
import { DebugElement } from '@angular/core';
45
import { ComponentFixture, waitForAsync } from '@angular/core/testing';
56
import { By } from '@angular/platform-browser';
@@ -27,6 +28,7 @@ describe('DotEditContentletComponent', () => {
2728
let dotEditContentletWrapper: DebugElement;
2829
let dotEditContentletWrapperComponent: DotContentletWrapperComponent;
2930
let dotContentletEditorService: DotContentletEditorService;
31+
let httpMock: HttpTestingController;
3032

3133
beforeEach(waitForAsync(() => {
3234
DOTTestBed.configureTestingModule({
@@ -71,6 +73,7 @@ describe('DotEditContentletComponent', () => {
7173
de = fixture.debugElement;
7274
component = de.componentInstance;
7375
dotContentletEditorService = de.injector.get(DotContentletEditorService);
76+
httpMock = de.injector.get(HttpTestingController);
7477

7578
jest.spyOn(component.shutdown, 'emit');
7679

@@ -80,6 +83,10 @@ describe('DotEditContentletComponent', () => {
8083
dotEditContentletWrapperComponent = dotEditContentletWrapper.componentInstance;
8184
});
8285

86+
afterEach(() => {
87+
httpMock.match(() => true).forEach((req) => req.flush({}));
88+
});
89+
8390
describe('default', () => {
8491
it('should have dot-contentlet-wrapper', () => {
8592
expect(dotEditContentletWrapper).toBeTruthy();

core-web/apps/dotcms-ui/src/app/view/components/login/dot-login-component/dot-login.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ <h3 class="text-2xl font-bold" data-testId="header">
7171
formControlName="rememberMe"
7272
data-testId="rememberMe"
7373
[binary]="true" />
74-
<label [for]="'rememberMe'">
74+
<label [for]="'rememberMe'" class="whitespace-nowrap">
7575
{{ loginInfo.i18nMessagesMap['remember-me'] }}
7676
</label>
7777
</div>

core-web/libs/data-access/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,5 @@ export * from './lib/dot-page-contenttype/dot-page-contenttype.service';
7272
export * from './lib/dot-favorite-contenttype/dot-favorite-contenttype.service';
7373
export * from './lib/dot-content-drive/dot-content-drive.service';
7474
export * from './lib/dot-usage/dot-usage.service';
75+
export * from './lib/dot-osgi/bundle-map.model';
76+
export * from './lib/dot-osgi/dot-osgi.service';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* OSGi bundle representation matching backend BundleMap.
3+
* @see dotCMS OSGIResource ResponseEntityBundleListView
4+
*/
5+
6+
/**
7+
* Sentinel value accepted by the backend's extra-packages PUT endpoint to reset
8+
* the exported-packages list to its installation defaults.
9+
* @see OSGIResource#updateExtraPackages
10+
*/
11+
export const OSGI_EXTRA_PACKAGES_RESET = 'RESET';
12+
13+
/** OSGi bundle states (org.osgi.framework.Bundle constants). */
14+
export const BUNDLE_STATE = {
15+
UNINSTALLED: 1,
16+
INSTALLED: 2,
17+
RESOLVED: 4,
18+
STARTING: 8,
19+
STOPPING: 16,
20+
ACTIVE: 32
21+
} as const;
22+
23+
/** Union of valid OSGi bundle state integers (values of {@link BUNDLE_STATE}). */
24+
export type OsgiBundleState = (typeof BUNDLE_STATE)[keyof typeof BUNDLE_STATE];
25+
26+
export interface BundleMap {
27+
bundleId: number;
28+
symbolicName: string;
29+
location: string;
30+
jarFile: string;
31+
state: OsgiBundleState;
32+
version: string;
33+
separator: string;
34+
isSystem: boolean;
35+
javaVersion?: string;
36+
javaClassVersion?: number;
37+
isMultiRelease?: boolean;
38+
isBuiltWithMaven?: boolean;
39+
usesDotcmsApis?: boolean;
40+
dotcmsCoreDependencyVersion?: string;
41+
}
42+
43+
/** Unified row model for the plugins table (installed bundles + undeployed jars). */
44+
export interface PluginRow {
45+
jarFile: string;
46+
symbolicName: string;
47+
state: OsgiBundleState | 'undeployed';
48+
bundleId?: number;
49+
version?: string;
50+
}

0 commit comments

Comments
 (0)