[ACS-10117] Deprecate ADF Storybook and custom Docker builds

This commit is contained in:
DominikIwanek
2025-09-18 13:15:14 +02:00
parent bd94d8abe8
commit e830d6209a
24 changed files with 1319 additions and 2 deletions
@@ -0,0 +1,91 @@
---
Title: Login Dialog component
Added: v2.6.0
Status: Active
Last reviewed: 2018-10-02
---
# [Login Dialog component](../../../lib/core/src/lib/login/components/login-dialog.component.ts "Defined in login-dialog.component.ts")
This component is deprecated and will removed because it's unused.
Allows a user to perform a login via a dialog.
## Details
The [Login Dialog component](login-dialog.component.md) allows you to perform a login via a dialog.
### Showing the dialog
Unlike most components, the [Login Dialog Component](login-dialog.component.md) is typically shown in a dialog box
rather than the main page and you are responsible for opening the dialog yourself. You can use the
[Angular Material Dialog](https://material.angular.io/components/dialog/overview) for this,
as shown in the usage example. ADF provides the [`LoginDialogComponentData`](../../../lib/core/src/lib/login/components/login-dialog-component-data.interface.ts) interface
to work with the Dialog's
[data option](https://material.angular.io/components/dialog/overview#sharing-data-with-the-dialog-component-):
```ts
export interface LoginDialogComponentData {
title: string;
actionName?: string;
logged: Subject<any>;
}
```
The properties are described in the table below:
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| title | `string` | "" | Dialog title |
| actionName | `string` | "" | Text to appear on the dialog's main action button ("Login", "Access", etc) |
| logged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | | Event emitted when the login succeeds. |
If you don't want to manage the dialog yourself then it is easier to use the
[Login Dialog Panel component](login-dialog-panel.component.md), or the
methods of the [Login Dialog service](../services/login-dialog.service.md), which create
the dialog for you.
### Usage example
```ts
import { MatDialog } from '@angular/material/dialog';
import { LoginDialogComponentData, LoginDialogComponent} from '@adf/core'
import { Subject } from 'rxjs/Subject';
...
constructor(dialog: MatDialog ... ) {}
openLoginDialog() {
data: LoginDialogComponentData = {
title: "Perform a Login",
actionName: "Access",
logged: new Subject<any>()
};
this.dialog.open(
LoginDialogComponent,
{
data, panelClass: 'adf-login-dialog',
width: '630px'
}
);
data.logged.subscribe(() => {
// Action after being logged in...
},
(error)=>{
//your error handling
},
()=>{
//action called when an action or cancel is clicked on the dialog
this.dialog.closeAll();
});
}
```
All the results will be streamed to the logged [subject](http://reactivex.io/rxjs/manual/overview.html#subject) present in the [`LoginDialogComponentData`](../../../lib/core/src/lib/login/components/login-dialog-component-data.interface.ts) object passed to the dialog.
When the dialog action is selected by clicking, the `data.logged` stream will be completed.
## See also
- [Login component](login.component.md)
@@ -0,0 +1,35 @@
---
Title: Form Definition Selector Cloud
Added: v3.3.0
Status: Active
---
# [Form Definition Selector Cloud](../../../lib/process-services-cloud/src/lib/form/components/form-definition-selector-cloud.component.ts "Defined in form-definition-selector-cloud.component.ts")
Allows one form to be selected from a dropdown list. For forms to be displayed in this component they will need to be compatible with standAlone tasks.
This component will be removed because it's unused.
## Basic Usage
```html
<adf-cloud-form-definition-selector
[appName]="'simple-app'"
(selectForm)="onFormSelect($event)">
</adf-cloud-form-definition-selector>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| appName | `string` | "" | Name of the application. If specified, this shows the users who have access to the app. |
### Events
| Name | Type | Description |
| ---- | ---- | ----------- |
| selectForm | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when a form is selected. |
@@ -0,0 +1,24 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Subject } from 'rxjs';
export interface LoginDialogComponentData {
title: string;
actionName?: string;
logged: Subject<any>;
}
@@ -0,0 +1,23 @@
<header mat-dialog-title
data-automation-id="login-dialog-title">
{{ data?.title }}
</header>
<mat-dialog-content class="adf-login-dialog-content">
<adf-login-dialog-panel #adfLoginPanel (success)="onLoginSuccess($event)" />
</mat-dialog-content>
<mat-dialog-actions class="adf-login-dialog-content-actions" align="end">
<button mat-button (click)="close()"
data-automation-id="login-dialog-actions-cancel">
{{ 'LOGIN.DIALOG.CANCEL' | translate }}
</button>
<button mat-button
class="choose-action"
data-automation-id="login-dialog-actions-perform"
[disabled]="!isFormValid()"
(click)="submitForm()">
{{ buttonActionName | translate }}
</button>
</mat-dialog-actions>
@@ -0,0 +1,4 @@
.adf-login-dialog-content adf-login .adf-login-content .adf-login-card-wide {
padding: 0;
box-shadow: none;
}
@@ -0,0 +1,59 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Inject, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { TranslatePipe } from '@ngx-translate/core';
import { LoginDialogPanelComponent } from '../login-dialog-panel/login-dialog-panel.component';
import { LoginDialogComponentData } from './login-dialog-component-data.interface';
/** @deprecated this component will be removed because it's unused */
@Component({
selector: 'adf-login-dialog',
templateUrl: './login-dialog.component.html',
styleUrls: ['./login-dialog.component.scss'],
imports: [MatDialogModule, LoginDialogPanelComponent, TranslatePipe, MatButtonModule],
encapsulation: ViewEncapsulation.None
})
export class LoginDialogComponent {
@ViewChild('adfLoginPanel', { static: true })
loginPanel: LoginDialogPanelComponent;
buttonActionName = '';
constructor(@Inject(MAT_DIALOG_DATA) public data: LoginDialogComponentData) {
this.buttonActionName = data.actionName ? `LOGIN.DIALOG.${data.actionName.toUpperCase()}` : 'LOGIN.DIALOG.CHOOSE';
}
close() {
this.data.logged.complete();
}
submitForm(): void {
this.loginPanel.submitForm();
}
onLoginSuccess(event: any) {
this.data.logged.next(event);
this.close();
}
isFormValid() {
return this.loginPanel ? this.loginPanel.isValid() : false;
}
}
+8 -1
View File
@@ -17,12 +17,19 @@
import { NgModule } from '@angular/core';
import { LoginDialogPanelComponent } from './components/login-dialog-panel/login-dialog-panel.component';
import { LoginDialogComponent } from './components/login-dialog/login-dialog.component';
import { LoginComponent } from './components/login/login.component';
import { LoginFooterDirective } from './directives/login-footer.directive';
import { LoginHeaderDirective } from './directives/login-header.directive';
export const LOGIN_DIRECTIVES = [LoginComponent, LoginFooterDirective, LoginHeaderDirective, LoginDialogPanelComponent] as const;
export const LOGIN_DIRECTIVES = [
LoginComponent,
LoginFooterDirective,
LoginHeaderDirective,
LoginDialogComponent,
LoginDialogPanelComponent
] as const;
/** @deprecated use `...LOGIN_DIRECTIVES` or import the standalone directives directly */
@NgModule({
@@ -0,0 +1,23 @@
<div class="adf-app-listgrid">
<div class="adf-app-listgrid-item">
<mat-card
appearance="outlined"
tabindex="0"
role="button"
class="adf-app-listgrid-item-card"
title="{{applicationInstance.name}}"
[ngClass]="getTheme()"
(click)="onSelectApp(applicationInstance)"
(keyup.enter)="onSelectApp(applicationInstance)">
<div class="adf-app-listgrid-item-card-logo">
<mat-icon class="adf-app-listgrid-item-card-logo-icon">{{ getIcon() }}</mat-icon>
</div>
<div mat-card-title class="adf-app-listgrid-item-card-title">
<h1 class="adf-app-listgrid-item-card-title-text">{{applicationInstance.name}}</h1>
</div>
<mat-card-subtitle class="adf-app-listgrid-item-card-subtitle">
<div class="adf-line-clamp">{{applicationInstance.description}}</div>
</mat-card-subtitle>
</mat-card>
</div>
</div>
@@ -0,0 +1,167 @@
/* stylelint-disable scss/no-global-function-names */
@mixin adf-line-clamp($line-height: 1.25, $lines: 3) {
position: relative;
line-height: $line-height;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
/* stylelint-disable */
@supports (-webkit-line-clamp: 1) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $lines;
height: calc(0.99em * #{$line-height} * #{$lines});
}
@supports not (-webkit-line-clamp: 1) {
white-space: nowrap;
width: 100%;
}
/* stylelint-enable */
}
$tile-themes: (
theme-1: (
bg: #269abc,
color: #168aac
),
theme-2: (
bg: #7da9b0,
color: #6d99a0
),
theme-3: (
bg: #7689ab,
color: #66799b
),
theme-4: (
bg: #c74e3e,
color: #b73e2e
),
theme-5: (
bg: #fab96c,
color: #eaa95c
),
theme-6: (
bg: #759d4c,
color: #658d3c
),
theme-7: (
bg: #b1b489,
color: #a1a479
),
theme-8: (
bg: #a17299,
color: #916289
),
theme-9: (
bg: #696c67,
color: #595c57
),
theme-10: (
bg: #cabb33,
color: #baab23
)
);
adf-cloud-app-details {
.adf-app-listgrid {
padding: 8px;
display: block;
.adf-app-listgrid-item {
outline: none;
padding: 8px;
box-sizing: border-box;
flex: unset;
max-width: unset;
&-card {
@for $i from 1 through 10 {
&.theme-#{$i} {
$tile-theme: map-get($tile-themes, theme-#{$i});
background-color: map-get($tile-theme, bg);
.adf-app-listgrid-item-card-logo-icon {
color: map-get($tile-theme, color);
}
}
}
outline: none;
transition:
transform 280ms cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
min-height: 200px;
flex-direction: column;
box-sizing: border-box;
display: flex;
padding: 0;
max-width: unset;
&:hover {
box-shadow:
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12),
0 5px 5px -3px rgba(0, 0, 0, 0.2);
cursor: pointer;
transform: scale(1.015);
}
&-logo {
position: absolute;
right: 20px;
top: 20px;
padding: 16px;
z-index: 9;
.adf-app-listgrid-item-card-logo-icon {
font-size: 70px;
width: 1em;
height: 1em;
}
}
&-title:has(.adf-app-listgrid-item-card-title-text) {
padding: 16px;
margin-bottom: 0;
z-index: 9999;
h1 {
color: white;
width: 80%;
font-size: var(--theme-headline-font-size);
margin: 0;
line-height: normal;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
&-subtitle:has(.adf-line-clamp) {
color: white;
z-index: 9999;
padding: 16px;
flex: 1 0 auto;
.adf-line-clamp {
@include adf-line-clamp(1.25, 3);
}
}
&-actions {
padding: 0 16px 16px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
min-height: 48px;
box-sizing: border-box;
&-icon {
color: #e9f1f3;
}
}
}
}
}
}
@@ -0,0 +1,72 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { fakeApplicationInstance } from '../../mock/app-model.mock';
import { AppDetailsCloudComponent } from './app-details-cloud.component';
import { DEFAULT_APP_INSTANCE_THEME } from '../../models/application-instance.model';
import { NoopTranslateModule } from '@alfresco/adf-core';
describe('AppDetailsCloudComponent', () => {
let component: AppDetailsCloudComponent;
let fixture: ComponentFixture<AppDetailsCloudComponent>;
let host: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopTranslateModule, AppDetailsCloudComponent]
});
fixture = TestBed.createComponent(AppDetailsCloudComponent);
component = fixture.componentInstance;
host = fixture.nativeElement as HTMLElement;
component.applicationInstance = fakeApplicationInstance[0];
});
const getAppCard = () => host.querySelector<HTMLElement>('.adf-app-listgrid-item-card');
it('should display application name', () => {
fixture.detectChanges();
const appName = host.querySelector<HTMLDivElement>('.adf-app-listgrid-item-card-title');
expect(appName.innerText.trim()).toEqual(fakeApplicationInstance[0].name);
});
it('should emit a click event when app selected', () => {
spyOn(component.selectedApp, 'emit');
fixture.detectChanges();
const app = getAppCard();
app.click();
expect(component.selectedApp.emit).toHaveBeenCalledWith(fakeApplicationInstance[0]);
});
it('should render card with default icon and theme when are not provided', () => {
component.applicationInstance = fakeApplicationInstance[2];
fixture.detectChanges();
const card = getAppCard();
expect(card.classList.contains(DEFAULT_APP_INSTANCE_THEME));
const icon = host.querySelector('.adf-app-listgrid-item-card-logo-icon');
expect(icon).toBeTruthy();
});
it('should render card with a non ApplicationInstanceModel input object', () => {
component.applicationInstance = { name: 'application-new-3', createdAt: '2018-09-21T12:31:39.000Z', status: 'Pending' };
fixture.detectChanges();
const app = getAppCard();
expect(app).toBeTruthy();
});
});
@@ -0,0 +1,66 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { ApplicationInstanceModel, DEFAULT_APP_INSTANCE_ICON, DEFAULT_APP_INSTANCE_THEME } from '../../models/application-instance.model';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
/** @deprecated this component will be removed because it's unused */
@Component({
selector: 'adf-cloud-app-details',
imports: [CommonModule, MatIconModule, MatCardModule],
templateUrl: './app-details-cloud.component.html',
styleUrls: ['./app-details-cloud.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AppDetailsCloudComponent {
@Input({ required: true })
applicationInstance: ApplicationInstanceModel;
@Output()
selectedApp = new EventEmitter<ApplicationInstanceModel>();
/**
* Pass the selected app as next
*
* @param app application model
*/
onSelectApp(app: ApplicationInstanceModel): void {
this.selectedApp.emit(app);
}
/**
* Get application instance theme
*
* @returns the name of the theme
*/
getTheme(): string {
return this.applicationInstance.theme || DEFAULT_APP_INSTANCE_THEME;
}
/**
* Get application instance icon
*
* @returns the name of the icon
*/
getIcon(): string {
return this.applicationInstance.icon || DEFAULT_APP_INSTANCE_ICON;
}
}
@@ -0,0 +1,46 @@
<div class="menu-container" *ngIf="apps$ | async as appsList; else loadingOrError">
<ng-container *ngIf="appsList.length > 0; else noApps">
<div
*ngIf="isGrid(); else appList"
class="adf-app-apps-grid">
<adf-cloud-app-details
*ngFor="let app of appsList"
[applicationInstance]="app"
(selectedApp)="onSelectApp($event)" />
</div>
<ng-template #appList>
<mat-list class="adf-app-list">
<mat-list-item class="adf-app-list-item" (click)="onSelectApp(app)" (keyup.enter)="onSelectApp(app)"
*ngFor="let app of appsList" tabindex="0" role="button" title="{{app.name}}">
<mat-icon matListItemIcon>touch_app</mat-icon>
<span class="adf-app-list-item-text" matLine>{{app.name}}</span>
</mat-list-item>
</mat-list>
</ng-template>
</ng-container>
</div>
<ng-template #noApps>
<div class="adf-app-list-empty">
<ng-content select="adf-custom-empty-content-template" *ngIf="hasEmptyCustomContentTemplate; else defaultEmptyTemplate"
class="adf-custom-empty-template" />
<ng-template #defaultEmptyTemplate>
<adf-empty-content icon="apps" [title]="'ADF_CLOUD_TASK_LIST.APPS.NO_APPS.TITLE' | translate"
[subtitle]="'ADF_CLOUD_TASK_LIST.APPS.NO_APPS.SUBTITLE' | translate" />
</ng-template>
</div>
</ng-template>
<ng-template #loadingOrError>
<div *ngIf="loadingError$ | async; else loading" class="adf-app-list-error">
<adf-empty-content icon="error_outline" [title]="'ADF_CLOUD_TASK_LIST.APPS.ERROR.TITLE' | translate"
[subtitle]="'ADF_CLOUD_TASK_LIST.APPS.ERROR.SUBTITLE' | translate" />
</div>
<ng-template #loading>
<ng-container>
<div class="adf-app-list-spinner">
<mat-spinner class="adf-app-list-cloud--spinner" />
</div>
</ng-container>
</ng-template>
</ng-template>
@@ -0,0 +1,41 @@
@use '../../../flex' as flex;
adf-cloud-app-list {
width: 100%;
.adf-app-list-item {
cursor: pointer;
}
.adf-app-list-spinner,
.adf-app-list-empty,
.adf-app-list-error {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 85vh;
.adf-app-list-cloud-spinner {
margin: 0 auto;
}
}
.adf-app-apps-grid {
flex-flow: row wrap;
display: flex;
adf-cloud-app-details {
flex: 1 1 100%;
max-width: 33.3333%;
@include flex.layout-bp(lt-md) {
max-width: 50%;
}
@include flex.layout-bp(lt-sm) {
max-width: 100%;
}
}
}
}
@@ -0,0 +1,220 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlfrescoApiService } from '@alfresco/adf-content-services';
import { of, throwError } from 'rxjs';
import { fakeApplicationInstance } from '../../mock/app-model.mock';
import { AppListCloudComponent, LAYOUT_GRID, LAYOUT_LIST } from './app-list-cloud.component';
import { AppsProcessCloudService } from '../../services/apps-process-cloud.service';
import { MatIconModule } from '@angular/material/icon';
import { CustomEmptyContentTemplateDirective, NoopTranslateModule } from '@alfresco/adf-core';
describe('AppListCloudComponent', () => {
let component: AppListCloudComponent;
let fixture: ComponentFixture<AppListCloudComponent>;
let appsProcessCloudService: AppsProcessCloudService;
let getAppsSpy: jasmine.Spy;
let alfrescoApiService: AlfrescoApiService;
const mock: any = {
oauth2Auth: {
callCustomApi: () => Promise.resolve(fakeApplicationInstance)
},
isLoggedIn: () => false,
reply: jasmine.createSpy('reply')
};
@Component({
imports: [MatIconModule, CustomEmptyContentTemplateDirective, AppListCloudComponent],
template: `
<adf-cloud-app-list>
<adf-custom-empty-content-template>
<mat-icon>apps</mat-icon>
<p id="custom-id">No Apps Found</p>
</adf-custom-empty-content-template>
</adf-cloud-app-list>
`
})
class CustomEmptyAppListCloudTemplateComponent {}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopTranslateModule, AppListCloudComponent, CustomEmptyAppListCloudTemplateComponent]
});
fixture = TestBed.createComponent(AppListCloudComponent);
component = fixture.componentInstance;
alfrescoApiService = TestBed.inject(AlfrescoApiService);
appsProcessCloudService = TestBed.inject(AppsProcessCloudService);
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock);
getAppsSpy = spyOn(appsProcessCloudService, 'getDeployedApplicationsByStatus').and.returnValue(of(fakeApplicationInstance));
});
it('should define layoutType with the default value', () => {
component.layoutType = '';
fixture.detectChanges();
expect(component.isGrid()).toBe(true);
});
it('Should fetch deployed apps', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
component.apps$.subscribe((response: any[]) => {
expect(response).toBeDefined();
expect(response.length).toEqual(3);
expect(response[0].name).toEqual('application-new-1');
expect(response[0].status).toEqual('Deployed');
expect(response[0].icon).toEqual('favorite_border');
expect(response[0].theme).toEqual('theme-2');
expect(response[1].name).toEqual('application-new-2');
expect(response[1].status).toEqual('Pending');
expect(response[1].icon).toEqual('favorite_border');
expect(response[1].theme).toEqual('theme-2');
done();
});
});
});
it('should display default adf-empty-content template when response empty', () => {
getAppsSpy.and.returnValue(of([]));
fixture.detectChanges();
const defaultEmptyTemplate = fixture.nativeElement.querySelector('.adf-app-list-empty');
const emptyContent = fixture.debugElement.nativeElement.querySelector('.adf-empty-content');
const emptyTitle = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__title');
const emptySubtitle = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__subtitle');
expect(defaultEmptyTemplate).toBeDefined();
expect(defaultEmptyTemplate).not.toBeNull();
expect(emptyContent).not.toBeNull();
expect(emptyTitle.innerText).toBe('ADF_CLOUD_TASK_LIST.APPS.NO_APPS.TITLE');
expect(emptySubtitle.innerText).toBe('ADF_CLOUD_TASK_LIST.APPS.NO_APPS.SUBTITLE');
expect(getAppsSpy).toHaveBeenCalled();
});
it('should display default no permissions template when response returns exception', (done) => {
getAppsSpy.and.returnValue(throwError({}));
fixture.detectChanges();
fixture.whenStable().then(() => {
component.loadingError$.next(true);
fixture.detectChanges();
const errorTemplate = fixture.nativeElement.querySelector('.adf-app-list-error');
const errorTitle = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__title');
const errorSubtitle = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__subtitle');
expect(errorTemplate).not.toBeNull();
expect(errorTitle.innerText).toBe('ADF_CLOUD_TASK_LIST.APPS.ERROR.TITLE');
expect(errorSubtitle.innerText).toBe('ADF_CLOUD_TASK_LIST.APPS.ERROR.SUBTITLE');
expect(getAppsSpy).toHaveBeenCalled();
done();
});
});
describe('Grid Layout ', () => {
it('should display a grid by default', () => {
fixture.detectChanges();
expect(component.isGrid()).toBe(true);
expect(component.isList()).toBe(false);
});
it('should defined adf-cloud-app-details when layout type is grid', () => {
fixture.detectChanges();
const adfCloudDetailsElement = fixture.nativeElement.querySelectorAll('adf-cloud-app-details');
const appName = fixture.nativeElement.querySelector('.adf-app-listgrid-item-card-title');
expect(adfCloudDetailsElement).toBeDefined();
expect(adfCloudDetailsElement).not.toBeNull();
expect(adfCloudDetailsElement.length).toEqual(3);
expect(component.isGrid()).toBe(true);
expect(component.isList()).toBe(false);
expect(appName.innerText.trim()).toEqual(fakeApplicationInstance[0].name);
});
it('should display a grid when configured to', () => {
component.layoutType = LAYOUT_GRID;
fixture.detectChanges();
expect(component.isGrid()).toBe(true);
expect(component.isList()).toBe(false);
});
it('should throw an exception on init if unknown type configured', () => {
component.layoutType = 'unknown';
expect(component.ngOnInit).toThrowError();
});
});
describe('List Layout ', () => {
beforeEach(() => {
component.layoutType = LAYOUT_LIST;
});
it('should display a LIST when configured to', () => {
fixture.detectChanges();
expect(component.isGrid()).toBe(false);
expect(component.isList()).toBe(true);
});
it('should display list when layout type is LIST', () => {
fixture.detectChanges();
const appListElement = fixture.nativeElement.querySelectorAll('.adf-app-list');
const appListItemElement = fixture.nativeElement.querySelectorAll('.adf-app-list-item');
const appName = fixture.nativeElement.querySelector('.adf-app-list-item-text');
expect(appListElement).toBeDefined();
expect(appListElement).not.toBeNull();
expect(appListItemElement.length).toEqual(3);
expect(component.isGrid()).toBe(false);
expect(component.isList()).toBe(true);
expect(appName.innerText.trim()).toEqual(fakeApplicationInstance[0].name);
});
it('should throw an exception on init if unknown type configured', () => {
component.layoutType = 'unknown';
expect(component.ngOnInit).toThrowError();
});
});
it('should emit a click event when app selected', () => {
spyOn(component.appClick, 'emit');
fixture.detectChanges();
const onAppClick = fixture.nativeElement.querySelector('.adf-app-listgrid-item-card');
onAppClick.click();
expect(component.appClick.emit).toHaveBeenCalledWith(fakeApplicationInstance[0]);
});
describe('Custom CustomEmptyAppListCloudTemplateComponent', () => {
let customFixture: ComponentFixture<CustomEmptyAppListCloudTemplateComponent>;
beforeEach(() => {
getAppsSpy.and.returnValue(of([]));
customFixture = TestBed.createComponent(CustomEmptyAppListCloudTemplateComponent);
});
afterEach(() => {
customFixture.destroy();
});
it('should render the custom empty template', async () => {
customFixture.detectChanges();
await customFixture.whenStable();
const title = customFixture.nativeElement.querySelector('#custom-id');
expect(title.innerText).toBe('No Apps Found');
});
});
});
@@ -0,0 +1,133 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CustomEmptyContentTemplateDirective, EmptyContentComponent } from '@alfresco/adf-core';
import { AfterContentInit, Component, ContentChild, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { Observable, of, Subject } from 'rxjs';
import { AppsProcessCloudService } from '../../services/apps-process-cloud.service';
import { ApplicationInstanceModel } from '../../models/application-instance.model';
import { catchError } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { AppDetailsCloudComponent } from '../app-details-cloud/app-details-cloud.component';
import { MatIconModule } from '@angular/material/icon';
import { MatLineModule } from '@angular/material/core';
import { MatListModule } from '@angular/material/list';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
export const LAYOUT_LIST: string = 'LIST';
export const LAYOUT_GRID: string = 'GRID';
export const DEPLOYED_STATUS: string = 'DEPLOYED';
/** @deprecated this component will be removed because it's unused */
@Component({
selector: 'adf-cloud-app-list',
imports: [
CommonModule,
TranslatePipe,
AppDetailsCloudComponent,
MatIconModule,
MatLineModule,
MatListModule,
EmptyContentComponent,
MatProgressSpinnerModule
],
templateUrl: './app-list-cloud.component.html',
styleUrls: ['./app-list-cloud.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AppListCloudComponent implements OnInit, AfterContentInit {
@ContentChild(CustomEmptyContentTemplateDirective)
emptyCustomContent: CustomEmptyContentTemplateDirective;
/**
* Defines the layout of the apps. There are two possible
* values, "GRID" and "LIST".
*/
@Input()
layoutType: string = LAYOUT_GRID;
/** Emitted when an app entry is clicked. */
@Output()
appClick = new EventEmitter<ApplicationInstanceModel>();
apps$: Observable<any>;
loadingError$ = new Subject<boolean>();
hasEmptyCustomContentTemplate: boolean = false;
constructor(private appsProcessCloudService: AppsProcessCloudService) {}
ngOnInit() {
if (!this.isValidType()) {
this.setDefaultLayoutType();
}
this.apps$ = this.appsProcessCloudService.getDeployedApplicationsByStatus(DEPLOYED_STATUS).pipe(
catchError(() => {
this.loadingError$.next(true);
return of();
})
);
}
ngAfterContentInit() {
if (this.emptyCustomContent) {
this.hasEmptyCustomContentTemplate = true;
}
}
onSelectApp(app: ApplicationInstanceModel): void {
this.appClick.emit(app);
}
/**
* Check if the value of the layoutType property is an allowed value
*
* @returns `true` if layout type is valid, otherwise `false`
*/
isValidType(): boolean {
if (this.layoutType && (this.layoutType === LAYOUT_LIST || this.layoutType === LAYOUT_GRID)) {
return true;
}
return false;
}
/**
* Assign the default value to LayoutType
*/
setDefaultLayoutType(): void {
this.layoutType = LAYOUT_GRID;
}
/**
* Check if the layout type is LIST
*
* @returns `true` if the layout is list, otherwise `false`
*/
isList(): boolean {
return this.layoutType === LAYOUT_LIST;
}
/**
* Check if the layout type is GRID
*
* @returns `true` if layout is grid, otherwise `false`
*/
isGrid(): boolean {
return this.layoutType === LAYOUT_GRID;
}
}
@@ -15,6 +15,9 @@
* limitations under the License.
*/
export const DEFAULT_APP_INSTANCE_THEME = 'theme-2';
export const DEFAULT_APP_INSTANCE_ICON = 'favorite_border';
export interface ApplicationInstanceModel {
name?: string;
displayName?: string;
@@ -0,0 +1,7 @@
<mat-form-field class="adf-form-definition-selector">
<mat-label>{{'ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.FORM' | translate}}</mat-label>
<mat-select class="adf-form-selector-dropdown" (selectionChange)="onSelect($event)">
<mat-option [value]="''">{{'ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.NONE' | translate}}</mat-option>
<mat-option *ngFor="let form of forms$ | async" [value]="form.id">{{ form.name }}</mat-option>
</mat-select>
</mat-form-field>
@@ -0,0 +1,5 @@
.adf {
&-form-definition-selector {
width: 100%;
}
}
@@ -0,0 +1,79 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormDefinitionSelectorCloudComponent } from './form-definition-selector-cloud.component';
import { of } from 'rxjs';
import { FormDefinitionSelectorCloudService } from '../services/form-definition-selector-cloud.service';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing';
import { NoopTranslateModule } from '@alfresco/adf-core';
describe('FormDefinitionCloudComponent', () => {
let fixture: ComponentFixture<FormDefinitionSelectorCloudComponent>;
let service: FormDefinitionSelectorCloudService;
let getFormsSpy: jasmine.Spy;
let loader: HarnessLoader;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopTranslateModule, FormDefinitionSelectorCloudComponent]
});
fixture = TestBed.createComponent(FormDefinitionSelectorCloudComponent);
service = TestBed.inject(FormDefinitionSelectorCloudService);
getFormsSpy = spyOn(service, 'getStandAloneTaskForms').and.returnValue(of([{ id: 'fake-form', name: 'fakeForm' } as any]));
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should load the forms by default', async () => {
const selectElement = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-form-selector-dropdown' }));
await selectElement.open();
const options = await selectElement.getOptions();
expect(options.length).toBe(2);
expect(await options[0].getText()).toBe('ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.NONE');
expect(await options[1].getText()).toBe('fakeForm');
expect(getFormsSpy).toHaveBeenCalled();
});
it('should load only None option when no forms exist', async () => {
getFormsSpy.and.returnValue(of([]));
const selectElement = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-form-selector-dropdown' }));
await selectElement.open();
const options = await selectElement.getOptions();
expect(options.length).toBe(1);
});
it('should not preselect any form by default', async () => {
const selectElement = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-form-selector-dropdown' }));
expect(await selectElement.getValueText()).toBe('');
});
it('should display the name of the form that is selected', async () => {
const selectElement = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-form-selector-dropdown' }));
await selectElement.open();
const options = await selectElement.getOptions();
await options[1].click();
expect(await selectElement.getValueText()).toBe('fakeForm');
});
});
@@ -0,0 +1,53 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { FormDefinitionSelectorCloudService } from '../services/form-definition-selector-cloud.service';
import { MatSelectChange, MatSelectModule } from '@angular/material/select';
import { FormRepresentation } from '../../services/form-fields.interfaces';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
/** @deprecated this component will be removed because it's unused */
@Component({
selector: 'adf-cloud-form-definition-selector',
imports: [CommonModule, TranslatePipe, MatSelectModule],
templateUrl: './form-definition-selector-cloud.component.html',
styleUrls: ['./form-definition-selector-cloud.component.scss']
})
export class FormDefinitionSelectorCloudComponent implements OnInit {
/** Name of the application. If specified, this shows the users who have access to the app. */
@Input()
appName: string = '';
/** Emitted when a form is selected. */
@Output()
selectForm: EventEmitter<string> = new EventEmitter<string>();
forms$: Observable<FormRepresentation[]>;
constructor(private formDefinitionCloudService: FormDefinitionSelectorCloudService) {}
ngOnInit(): void {
this.forms$ = this.formDefinitionCloudService.getStandAloneTaskForms(this.appName);
}
onSelect(event: MatSelectChange) {
this.selectForm.emit(event.value);
}
}
@@ -0,0 +1,45 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const mockFormRepresentations = [
{
formRepresentation: {
id: 'form-de8895be-d0d7-4434-beef-559b15305d72',
name: 'Form 1',
description: '',
version: 0,
standalone: true
}
},
{
formRepresentation: {
id: 'form-de8895be-d0d7-4434-beef-fgr34ttgrtgd',
name: 'Form 2',
description: '',
version: 0,
standalone: false
}
},
{
formRepresentation: {
id: 'form-de8895be-d0d7-4434-beef-53453453452',
name: 'Form 3',
description: '',
version: 0
}
}
];
@@ -0,0 +1,55 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { FormDefinitionSelectorCloudService } from './form-definition-selector-cloud.service';
import { mockFormRepresentations } from '../mocks/form-representation.mock';
import { AdfHttpClient } from '@alfresco/adf-core/api';
import { NoopTranslateModule } from '@alfresco/adf-core';
describe('Form Definition Selector Cloud Service', () => {
let service: FormDefinitionSelectorCloudService;
let adfHttpClient: AdfHttpClient;
const appName = 'app-name';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopTranslateModule]
});
service = TestBed.inject(FormDefinitionSelectorCloudService);
adfHttpClient = TestBed.inject(AdfHttpClient);
spyOn(adfHttpClient, 'request').and.returnValue(Promise.resolve(mockFormRepresentations));
});
it('should fetch all the forms when getForms is called', (done) => {
service.getForms(appName).subscribe((result) => {
expect(result).toBeDefined();
expect(result.length).toBe(3);
done();
});
});
it('should fetch only standalone enabled forms when getStandaloneTaskForms is called', (done) => {
service.getStandAloneTaskForms(appName).subscribe((result) => {
expect(result).toBeDefined();
expect(result.length).toBe(2);
expect(result[0].name).toBe('Form 1');
expect(result[1].name).toBe('Form 3');
done();
});
});
});
@@ -0,0 +1,53 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { from, Observable } from 'rxjs';
import { BaseCloudService } from '../../services/base-cloud.service';
import { FormRepresentation } from '../../services/form-fields.interfaces';
import { FormDefinitionSelectorCloudServiceInterface } from './form-definition-selector-cloud.service.interface';
/** @deprecated this service will be removed because it's component is unused */
@Injectable({
providedIn: 'root'
})
export class FormDefinitionSelectorCloudService extends BaseCloudService implements FormDefinitionSelectorCloudServiceInterface {
/**
* Get all forms of an app.
*
* @param appName Name of the application
* @returns Details of the forms
*/
getForms(appName: string): Observable<FormRepresentation[]> {
const url = `${this.getBasePath(appName)}/form/v1/forms`;
return this.get(url).pipe(map((data: any) => data.map((formData: any) => formData.formRepresentation)));
}
/**
* Get all forms of an app.
*
* @param appName Name of the application
* @returns Details of the forms
*/
getStandAloneTaskForms(appName: string): Observable<FormRepresentation[]> {
return from(this.getForms(appName)).pipe(
map((data: any) => data.filter((formData: any) => formData.standalone || formData.standalone === undefined))
);
}
}
@@ -17,6 +17,7 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { provideTranslations } from '@alfresco/adf-core';
import { APP_LIST_CLOUD_DIRECTIVES } from './app/app-list-cloud.module';
import { TaskCloudModule } from './task/task-cloud.module';
import { ProcessCloudModule } from './process/process-cloud.module';
import { FORM_CLOUD_DIRECTIVES } from './form/form-cloud.module';
@@ -27,7 +28,12 @@ import { PeopleCloudComponent } from './people/components/people-cloud.component
import { provideCloudFormRenderer, provideCloudPreferences } from './providers';
import { TaskListCloudService } from './task/task-list/services/task-list-cloud.service';
export const PROCESS_SERVICES_CLOUD_DIRECTIVES = [...FORM_CLOUD_DIRECTIVES, ...TASK_FORM_CLOUD_DIRECTIVES, PeopleCloudComponent] as const;
export const PROCESS_SERVICES_CLOUD_DIRECTIVES = [
...APP_LIST_CLOUD_DIRECTIVES,
...FORM_CLOUD_DIRECTIVES,
...TASK_FORM_CLOUD_DIRECTIVES,
PeopleCloudComponent
] as const;
/**
* @deprecated this module is deprecated and will be removed in the future versions