Merge branch 'develop' into dependabot/npm_and_yarn/develop/stylelint-16.19.1

This commit is contained in:
Eugenio Romano 2025-05-26 15:35:41 +02:00 committed by GitHub
commit 07e6d52115
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 693 additions and 589 deletions

View File

@ -26,7 +26,7 @@ runs:
cache-dependency-path: package-lock.json
- name: get latest tag sha
id: tag-sha
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@8cd8c3798c79d10540e2876ad7cf2a5311cbd1ae # v8.20.0
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@95f68dc050fba62b3331d9b47bc659526cdfb343 # v8.21.1
# CACHE
- name: Node Modules cache
id: node-modules-cache

View File

@ -30,7 +30,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
# Override language selection by uncommenting this and choosing your languages
with:
languages: javascript
@ -39,7 +39,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -53,4 +53,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18

View File

@ -64,7 +64,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@2d6823da4039243036c86d76f503c84e2ded2517 # v3.0.24
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@fc87bb5b5a97953d987372e74478de634726b3e5 # v3.0.25
- name: Check package-lock.json version
run: |
@ -84,10 +84,10 @@ jobs:
fetch-depth: 0
- name: Get branch name
uses: Alfresco/alfresco-build-tools/.github/actions/get-branch-name@8cd8c3798c79d10540e2876ad7cf2a5311cbd1ae # v8.20.0
uses: Alfresco/alfresco-build-tools/.github/actions/get-branch-name@95f68dc050fba62b3331d9b47bc659526cdfb343 # v8.21.1
- name: Save commit message
uses: Alfresco/alfresco-build-tools/.github/actions/get-commit-message@8cd8c3798c79d10540e2876ad7cf2a5311cbd1ae # v8.20.0
uses: Alfresco/alfresco-build-tools/.github/actions/get-commit-message@95f68dc050fba62b3331d9b47bc659526cdfb343 # v8.21.1
- name: ci:force flag parser
shell: bash

View File

@ -23,10 +23,10 @@ Manages tags in Content Services.
- _nodeId:_ `string` - Id of node to which tags should be assigned.
- _tags:_ `TagBody[]` - List of tags to create and assign or just assign if they already exist.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagPaging`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagPaging.md)`|`[`TagEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagEntry.md)`>` - Just linked tags to node or single tag if linked only one tag.
- **createTags**(tags: `TagBody[]`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagEntry.md)`[]>`<br/>
- **createTags**(tags: `TagBody[]`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagEntry`](../../../lib/js-api/src/api/content-rest-api/docs/TagsApi.md#TagEntry) `|` [`TagPaging`](../../../lib/js-api/src/api/content-rest-api/docs/TagsApi.md#TagPaging)`>`<br/>
Creates tags.
- _tags:_ `TagBody[]` - list of tags to create.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagEntry.md)`[]>` - Created tags.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagEntry`](../../../lib/js-api/src/api/content-rest-api/docs/TagsApi.md#TagEntry) `|` [`TagPaging`](../../../lib/js-api/src/api/content-rest-api/docs/TagsApi.md#TagPaging)`>` - Created tags.
- **deleteTag**(tagId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<void>`<br/>
Deletes a tag with tagId. This will cause the tag to be removed from all nodes. You must have admin rights to delete a tag.
- _tagId:_ `string` - of the tag to be deleted

View File

@ -89,7 +89,7 @@ describe('TagService', () => {
describe('createTags', () => {
it('should call createTags on tagsApi', () => {
spyOn(service.tagsApi, 'createTags').and.returnValue(Promise.resolve([]));
spyOn(service.tagsApi, 'createTags').and.returnValue(Promise.resolve({}));
const tag1 = new TagBody();
tag1.tag = 'Some tag 1';
const tag2 = new TagBody();
@ -101,19 +101,17 @@ describe('TagService', () => {
});
it('should emit refresh when tags creation is success', async () => {
const tags: TagEntry[] = [
{
entry: {
id: 'Some id 1',
tag: 'Some tag 1'
}
const tag: TagEntry = {
entry: {
id: 'Some id 1',
tag: 'Some tag 1'
}
];
};
spyOn(service.refresh, 'emit');
spyOn(service.tagsApi, 'createTags').and.returnValue(Promise.resolve(tags));
spyOn(service.tagsApi, 'createTags').and.returnValue(Promise.resolve(tag));
await service.createTags([]).toPromise();
expect(service.refresh.emit).toHaveBeenCalledWith(tags);
expect(service.refresh.emit).toHaveBeenCalledWith(tag);
});
});

View File

@ -99,7 +99,7 @@ export class TagService {
* @param tags list of tags to create.
* @returns Created tags.
*/
createTags(tags: TagBody[]): Observable<TagEntry[]> {
createTags(tags: TagBody[]): Observable<TagEntry | TagPaging> {
return from(this.tagsApi.createTags(tags)).pipe(tap((tagEntries) => this.refresh.emit(tagEntries)));
}

View File

@ -78,7 +78,6 @@ describe('DateFnsUtils', () => {
it('should parse alternative ISO datetime', () => {
const result = DateFnsUtils.parseDate('1982-03-13T10:00:000Z', `yyyy-MM-dd'T'HH:mm:sssXXX`);
expect(result.toISOString()).toBe('1982-03-13T10:00:00.000Z');
});

View File

@ -18,6 +18,7 @@
import { Directive, EventEmitter, Input, Output } from '@angular/core';
import { ThemePalette } from '@angular/material/core';
import { FormFieldModel, FormFieldValidator, FormModel, FormOutcomeEvent, FormOutcomeModel } from './widgets';
import { isOutcomeButtonVisible } from './helpers/buttons-visibility';
@Directive({
standalone: true
@ -103,10 +104,6 @@ export abstract class FormBaseComponent {
*/
formStyle: string = '';
get hasVisibleOutcomes(): boolean {
return this.form?.outcomes?.some((outcome) => this.isOutcomeButtonVisible(outcome, this.form.readOnly));
}
get form(): FormModel {
return this._form;
}
@ -169,22 +166,7 @@ export abstract class FormBaseComponent {
}
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome?.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return this.showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
return isOutcomeButtonVisible(outcome, { isFormReadOnly, showCompleteButton: this.showCompleteButton, showSaveButton: this.showSaveButton });
}
/**

View File

@ -73,6 +73,7 @@ export class FormFieldComponent implements OnInit, OnDestroy {
if (w.adf === undefined) {
w.adf = {};
}
const originalField = this.getField();
if (originalField) {
const customTemplate = this.field.form.customFieldTemplates[originalField.type];

View File

@ -4,9 +4,11 @@
<div *ngIf="hasTabs()" class="alfresco-tabs-widget">
<mat-tab-group>
<mat-tab *ngFor="let tab of visibleTabs()" [label]="tab.title | translate ">
<div class="adf-form-tab-content">
<ng-template *ngTemplateOutlet="render; context: { fieldToRender: tab.fields }" />
</div>
<ng-template matTabContent>
<div class="adf-form-tab-content">
<ng-template *ngTemplateOutlet="render; context: { fieldToRender: tab.fields }" />
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>
@ -40,7 +42,8 @@
<section class="adf-grid-list-column-view" *ngIf="currentRootElement?.isExpanded">
<div class="adf-grid-list-single-column"
*ngFor="let column of currentRootElement?.columns"
[style.width.%]="getColumnWidth(currentRootElement)">
[style.width.%]="getColumnWidth(currentRootElement)"
>
<ng-container *ngFor="let field of column?.fields">
<ng-container *ngIf="field.type === 'section'; else formField">
<adf-form-section [field]="field"/>
@ -61,9 +64,11 @@
</div>
</ng-template>
</div>
<div *ngIf="currentRootElement.type === 'dynamic-table'" class="adf-container-widget">
<adf-form-field [field]="currentRootElement" />
</div>
<div class="adf-container-widget"
*ngIf="currentRootElement.type === 'readonly' && currentRootElement.field.params.field.type === 'dynamic-table'">
<adf-form-field [field]="currentRootElement.field"/>

View File

@ -15,11 +15,9 @@
.alfresco-tabs-widget {
width: 100%;
position: absolute;
.adf-form-tab-group {
width: 100%;
z-index: 9999;
}
#{ms.$mat-tab-body} {
@ -30,16 +28,17 @@
z-index: 10;
margin: 0;
background-color: white;
position: sticky;
/* stylelint-disable-next-line value-no-vendor-prefix */
position: -webkit-sticky; /* macOS/iOS Safari */
position: absolute;
width: 96%;
/* stylelint-disable-next-line declaration-no-important */
margin-left: 0 !important;
/* stylelint-disable-next-line declaration-no-important */
margin-right: 10px !important;
top: 0;
}
#{ms.$mat-tab-body-wrapper} {
padding-top: 16px;
padding-top: 5%;
}
}

View File

@ -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.
*/
import { FormOutcomeModel } from '../widgets';
interface IsOutcomeButtonVisibleProps {
isFormReadOnly: boolean;
showCompleteButton: boolean;
showSaveButton: boolean;
}
export const isOutcomeButtonVisible = (outcome: FormOutcomeModel, props: IsOutcomeButtonVisibleProps): boolean => {
const { isFormReadOnly, showCompleteButton, showSaveButton } = props;
if (outcome?.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return showCompleteButton;
}
if (isFormReadOnly) {
return outcome.isSelected;
}
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return showSaveButton;
}
if (outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return false;
}
return true;
}
return false;
};

View File

@ -358,7 +358,7 @@ export class FormModel implements ProcessFormModel {
this.handleSectionField(field, formFieldModel);
} else if (this.isContainerField(field)) {
this.handleContainerField(field, formFieldModel);
} else {
} else if (this.isFormField(field)) {
this.handleSingleField(field, formFieldModel);
}
});
@ -368,6 +368,10 @@ export class FormModel implements ProcessFormModel {
return field instanceof ContainerModel;
}
private isFormField(field: ContainerModel | FormFieldModel): field is FormFieldModel {
return field instanceof FormFieldModel;
}
private isSectionField(field: ContainerModel | FormFieldModel): field is FormFieldModel {
return field.type === FormFieldTypes.SECTION;
}

View File

@ -23,6 +23,7 @@ export * from './components/form-renderer.component';
export * from './components/widgets';
export * from './components/middlewares/middleware';
export * from './components/middlewares/decimal-middleware.service';
export * from './components/helpers/buttons-visibility';
export * from './services/form-rendering.service';
export * from './services/form.service';

View File

@ -18,7 +18,7 @@
"COMPLETE": "Abschließen",
"CANCEL": "Abbrechen",
"CLAIM": "Beanspruchen",
"UNCLAIM": "Anspruch aufheben",
"UNCLAIM": "Freigeben ",
"START PROCESS": "Prozess starten",
"DATA_LOADING": "Daten werden geladen",
"CLOSE": "Schließen",

View File

@ -1,95 +1,105 @@
<div *ngIf="(viewerType === 'media' || viewerType === 'pdf' || viewerType === 'image') ? isLoading || !isContentReady : isLoading"
class="adf-viewer-render-main-loader">
<div *ngIf="isLoading$ | async" class="adf-viewer-render-main-loader">
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-render-content-container">
<div class="adf-viewer-render__loading-screen ">
<div class="adf-viewer-render__loading-screen">
<h2>{{ 'ADF_VIEWER.LOADING' | translate }}</h2>
<div>
<mat-spinner class="adf-viewer-render__loading-screen__spinner"/>
<mat-spinner class="adf-viewer-render__loading-screen__spinner" />
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!isLoading"
class="adf-viewer-render-main">
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-render-content-container" [ngSwitch]="viewerType">
<ng-container *ngSwitchCase="'external'">
<adf-preview-extension *ngIf="!!externalViewer"
[id]="externalViewer.component"
[url]="urlFile"
[extension]="externalViewer.fileExtension"
[nodeId]="nodeId"
[attr.data-automation-id]="externalViewer.component" />
</ng-container>
<ng-container *ngSwitchCase="'pdf'">
<adf-pdf-viewer [thumbnailsTemplate]="thumbnailsTemplate"
[allowThumbnails]="allowThumbnails"
[blobFile]="blobFile"
[urlFile]="urlFile"
[fileName]="internalFileName"
[cacheType]="cacheTypeForContent"
(pagesLoaded)="isContentReady = true"
(close)="onClose()"
(error)="onUnsupportedFile()" />
</ng-container>
<ng-container *ngSwitchCase="'image'">
<adf-img-viewer [urlFile]="urlFile"
[readOnly]="readOnly"
[fileName]="internalFileName"
[allowedEditActions]="allowedEditActions"
[blobFile]="blobFile"
(error)="onUnsupportedFile()"
(submit)="onSubmitFile($event)"
(imageLoaded)="isContentReady = true"
(isSaving)="isSaving.emit($event)"
/>
</ng-container>
<ng-container *ngSwitchCase="'media'">
<adf-media-player id="adf-mdedia-player"
[urlFile]="urlFile"
[tracks]="tracks"
[mimeType]="mimeType"
[blobFile]="blobFile"
[fileName]="internalFileName"
(error)="onUnsupportedFile()"
(canPlay)="isContentReady = true"/>
</ng-container>
<ng-container *ngSwitchCase="'text'">
<adf-txt-viewer [urlFile]="urlFile"
[blobFile]="blobFile" />
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<ng-container *ngFor="let ext of viewerExtensions">
<adf-preview-extension *ngIf="checkExtensions(ext.fileExtension)"
[id]="ext.component"
[url]="urlFile"
[extension]="extension"
[nodeId]="nodeId"
[attr.data-automation-id]="ext.component" />
<ng-container *ngIf="urlFile || blobFile">
<div [hidden]="isLoading$ | async" class="adf-viewer-render-main">
<div class="adf-viewer-render-layout-content adf-viewer__fullscreen-container">
<div class="adf-viewer-render-content-container" [ngSwitch]="viewerType">
<ng-container *ngSwitchCase="'external'">
<adf-preview-extension
*ngIf="!!externalViewer"
[id]="externalViewer.component"
[url]="urlFile"
[extension]="externalViewer.fileExtension"
[nodeId]="nodeId"
[attr.data-automation-id]="externalViewer.component"
/>
</ng-container>
<ng-container *ngFor="let extensionTemplate of extensionTemplates">
<span *ngIf="extensionTemplate.isVisible" class="adf-viewer-render-custom-content">
<ng-template [ngTemplateOutlet]="extensionTemplate.template"
[ngTemplateOutletContext]="{ urlFile: urlFile, extension: extension }" />
</span>
<ng-container *ngSwitchCase="'pdf'">
<adf-pdf-viewer
[thumbnailsTemplate]="thumbnailsTemplate"
[allowThumbnails]="allowThumbnails"
[blobFile]="blobFile"
[urlFile]="urlFile"
[fileName]="internalFileName"
[cacheType]="cacheTypeForContent"
(pagesLoaded)="markAsLoaded()"
(close)="onClose()"
(error)="onUnsupportedFile()"
/>
</ng-container>
</ng-container>
<ng-container *ngSwitchDefault>
<adf-viewer-unknown-format [customError]="customError"/>
</ng-container>
<ng-container *ngSwitchCase="'image'">
<adf-img-viewer
[urlFile]="urlFile"
[readOnly]="readOnly"
[fileName]="internalFileName"
[allowedEditActions]="allowedEditActions"
[blobFile]="blobFile"
(error)="onUnsupportedFile()"
(submit)="onSubmitFile($event)"
(imageLoaded)="markAsLoaded()"
(isSaving)="isSaving.emit($event)"
/>
</ng-container>
<ng-container *ngSwitchCase="'media'">
<adf-media-player
id="adf-mdedia-player"
[urlFile]="urlFile"
[tracks]="tracks"
[mimeType]="mimeType"
[blobFile]="blobFile"
[fileName]="internalFileName"
(error)="onUnsupportedFile()"
(canPlay)="markAsLoaded()"
/>
</ng-container>
<ng-container *ngSwitchCase="'text'">
<adf-txt-viewer [urlFile]="urlFile" [blobFile]="blobFile" />
</ng-container>
<ng-container *ngSwitchCase="'custom'">
<ng-container *ngFor="let ext of viewerExtensions">
<adf-preview-extension
*ngIf="checkExtensions(ext.fileExtension)"
[id]="ext.component"
[url]="urlFile"
[extension]="extension"
[nodeId]="nodeId"
[attr.data-automation-id]="ext.component"
/>
</ng-container>
<ng-container *ngFor="let extensionTemplate of extensionTemplates">
<span *ngIf="extensionTemplate.isVisible" class="adf-viewer-render-custom-content">
<ng-template
[ngTemplateOutlet]="extensionTemplate.template"
[ngTemplateOutletContext]="{ urlFile: urlFile, extension: extension }"
/>
</span>
</ng-container>
</ng-container>
<ng-container *ngSwitchDefault>
<adf-viewer-unknown-format [customError]="customError" />
</ng-container>
</div>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="viewerTemplateExtensions">
<ng-template [ngTemplateOutlet]="viewerTemplateExtensions" [ngTemplateOutletInjector]="injector" />
</ng-container>

View File

@ -24,7 +24,7 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { NoopTranslateModule, UnitTestingUtils } from '../../../testing';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { ViewerRenderComponent } from './viewer-render.component';
import { ImgViewerComponent, MediaPlayerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
import { ImgViewerComponent, MediaPlayerComponent, PdfViewerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
@ -483,14 +483,16 @@ describe('ViewerComponent', () => {
describe('Spinner', () => {
const getMainLoader = (): DebugElement => testingUtils.getByCSS('.adf-viewer-render-main-loader');
it('should show spinner when isLoading is true', () => {
component.isLoading = true;
fixture.detectChanges();
expect(getMainLoader()).not.toBeNull();
it('should not show spinner by default', (done) => {
component.isLoading$.subscribe((isLoading) => {
fixture.detectChanges();
expect(isLoading).toBeFalse();
expect(getMainLoader()).toBeNull();
done();
});
});
it('should show spinner until content is ready when viewerType is media', () => {
component.isLoading = false;
it('should display spinner when viewerType is media', () => {
component.urlFile = 'some-file.mp4';
component.ngOnChanges();
@ -506,24 +508,21 @@ describe('ViewerComponent', () => {
expect(component.viewerType).toBe('media');
});
// eslint-disable-next-line ban/ban
xit('should show spinner until content is ready when viewerType is pdf', () => {
component.isLoading = false;
it('should display spinner when viewerType is pdf', () => {
component.urlFile = 'some-url.pdf';
expect(getMainLoader()).toBeNull();
component.ngOnChanges();
fixture.detectChanges();
expect(getMainLoader()).not.toBeNull();
const imgViewer = testingUtils.getByDirective(PdfViewerComponent);
imgViewer.triggerEventHandler('pagesLoaded', null);
fixture.detectChanges();
expect(getMainLoader()).toBeNull();
expect(component.viewerType).toBe('pdf');
});
it('should show spinner until content is ready when viewerType is image', () => {
component.isLoading = false;
it('should display spinner when viewerType is image', () => {
component.urlFile = 'some-url.png';
component.ngOnChanges();
@ -537,16 +536,5 @@ describe('ViewerComponent', () => {
expect(getMainLoader()).toBeNull();
expect(component.viewerType).toBe('image');
});
it('should not show spinner when isLoading = false and isContentReady = false for other viewer types', () => {
component.isLoading = false;
component.urlFile = 'some-url.txt';
component.ngOnChanges();
fixture.detectChanges();
expect(getMainLoader()).toBeNull();
expect(component.isContentReady).toBeFalse();
});
});
});

View File

@ -16,7 +16,7 @@
*/
import { AppExtensionService, ExtensionsModule, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { NgForOf, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet } from '@angular/common';
import { AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet } from '@angular/common';
import { Component, EventEmitter, Injector, Input, OnChanges, OnInit, Output, TemplateRef, ViewEncapsulation } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@ -28,6 +28,9 @@ import { MediaPlayerComponent } from '../media-player/media-player.component';
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
import { TxtViewerComponent } from '../txt-viewer/txt-viewer.component';
import { UnknownFormatComponent } from '../unknown-format/unknown-format.component';
import { BehaviorSubject } from 'rxjs';
type ViewerType = 'media' | 'image' | 'pdf' | 'external' | 'text' | 'custom' | 'unknown';
@Component({
selector: 'adf-viewer-render',
@ -50,7 +53,8 @@ import { UnknownFormatComponent } from '../unknown-format/unknown-format.compone
UnknownFormatComponent,
ExtensionsModule,
NgForOf,
NgSwitchDefault
NgSwitchDefault,
AsyncPipe
],
providers: [ViewUtilService]
})
@ -86,10 +90,6 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
@Input()
fileName: string;
/** Override loading status */
@Input()
isLoading = false;
/** Enable when where is possible the editing functionalities */
@Input()
readOnly = true;
@ -141,8 +141,8 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
extensionsSupportedByTemplates: string[] = [];
extension: string;
internalFileName: string;
viewerType: string = 'unknown';
isContentReady = false;
viewerType: ViewerType = 'unknown';
readonly isLoading$ = new BehaviorSubject(false);
/**
* Returns a list of the active Viewer content extensions.
@ -182,12 +182,10 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
ngOnInit() {
this.cacheTypeForContent = 'no-cache';
this.setDefaultLoadingState();
}
ngOnChanges() {
this.isContentReady = false;
this.isLoading = !this.blobFile && !this.urlFile;
if (this.blobFile) {
this.setUpBlobData();
} else if (this.urlFile) {
@ -195,9 +193,13 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
}
}
markAsLoaded() {
this.isLoading$.next(false);
}
private setUpBlobData() {
this.internalFileName = this.fileName;
this.viewerType = this.viewUtilService.getViewerTypeByMimeType(this.blobFile.type);
this.viewerType = this.viewUtilService.getViewerTypeByMimeType(this.blobFile.type) as ViewerType;
this.extensionChange.emit(this.blobFile.type);
this.scrollTop();
@ -206,7 +208,7 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
private setUpUrlFile() {
this.internalFileName = this.fileName ? this.fileName : this.viewUtilService.getFilenameFromUrl(this.urlFile);
this.extension = this.viewUtilService.getFileExtension(this.internalFileName);
this.viewerType = this.viewUtilService.getViewerType(this.extension, this.mimeType, this.extensionsSupportedByTemplates);
this.viewerType = this.viewUtilService.getViewerType(this.extension, this.mimeType, this.extensionsSupportedByTemplates) as ViewerType;
this.extensionChange.emit(this.extension);
this.scrollTop();
@ -235,4 +237,14 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
onClose() {
this.close.next(true);
}
private canBePreviewed(): boolean {
return this.viewerType === 'media' || this.viewerType === 'pdf' || this.viewerType === 'image';
}
private setDefaultLoadingState() {
if (this.canBePreviewed()) {
this.isLoading$.next(true);
}
}
}

View File

@ -209,9 +209,9 @@ export class TagsApi extends BaseApi {
/**
* Create specified by **tags** list of tags.
* @param tags List of tags to create.
* @returns Promise<TagEntry[]>
* @returns Promise<TagEntry | TagPaging>
*/
createTags(tags: TagBody[]): Promise<TagEntry[]> {
createTags(tags: TagBody[]): Promise<TagEntry | TagPaging> {
throwIfNotDefined(tags, 'tags');
return this.post({

View File

@ -240,7 +240,7 @@ Create specified by **tags** list of tags.
|----------|-----------------------|-------------------------|
| **tags** | [TagBody[]](#TagBody) | List of tags to create. |
**Return type**: [TagEntry[]](#TagEntry)
**Return type**: [TagEntry](#TagEntry) | [TagPaging](#TagPaging)
**Example**

View File

@ -16,7 +16,7 @@
*/
import assert from 'assert';
import { AlfrescoApi, TagBody, TagEntry, TagsApi } from '../../src';
import { AlfrescoApi, TagBody, TagEntry, TagPaging, TagsApi } from '../../src';
import { EcmAuthMock, TagMock } from '../mockObjects';
describe('Tags', () => {
@ -105,10 +105,10 @@ describe('Tags', () => {
describe('createTags', () => {
it('should return created tags', (done) => {
tagMock.createTags201Response();
tagsApi.createTags([new TagBody(), new TagBody()]).then((tags) => {
assert.equal(tags.length, 2);
assert.equal(tags[0].entry.tag, 'tag-test-1');
assert.equal(tags[1].entry.tag, 'tag-test-2');
tagsApi.createTags([new TagBody(), new TagBody()]).then((tags: TagPaging) => {
assert.equal(tags.list.entries.length, 2);
assert.equal(tags.list.entries[0].entry.tag, 'tag-test-1');
assert.equal(tags.list.entries[1].entry.tag, 'tag-test-2');
done();
});
});

View File

@ -65,7 +65,7 @@ export class TagMock extends BaseMock {
createTags201Response(): void {
nock(this.host, { encodedQueryParams: true })
.post('/alfresco/api/-default-/public/alfresco/versions/1/tags')
.reply(201, [this.mockTagEntry(), this.mockTagEntry('tag-test-2', 'd79bdbd0-9f55-45bb-9521-811e15bf48f6')]);
.reply(201, this.getPaginatedListOfTags());
}
get201ResponseForAssigningTagsToNode(body: TagBody[]): void {

View File

@ -78,8 +78,7 @@
<mat-card-content class="adf-form-container-card-content">
<adf-form-renderer [formDefinition]="form" [readOnly]="readOnly" />
</mat-card-content>
<div class="adf-cloud-form-content-card-actions">
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-form-mat-card-actions" align="end">
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-cloud-form-content-card-actions" align="end">
<mat-checkbox
id="adf-form-open-next-task"
*ngIf="showNextTaskCheckbox && showCompleteButton"
@ -97,13 +96,13 @@
mat-button
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
class="adf-cloud-form-custom-outcome-button"
(click)="onOutcomeClicked(outcome)"
>
{{ outcome.name | translate | uppercase }}
</button>
</ng-container>
</mat-card-actions>
</div>
</div>
</mat-card>
</div>

View File

@ -72,14 +72,16 @@
}
&-content-card {
padding-bottom: 4em;
padding-bottom: 2em;
overflow-y: auto;
position: static;
height: 70%;
&-fullscreen {
padding: 0;
height: 100%;
width: 100%;
position: relative;
&-container {
display: flex;
@ -92,14 +94,6 @@
}
}
}
&-actions {
position: fixed;
bottom: 0;
width: -webkit-fill-available;
z-index: 1;
background-color: white;
}
}
&-sidebars {

View File

@ -670,12 +670,36 @@ describe('FormCloudComponent', () => {
done();
});
const formValues: any[] = [];
const formValues: TaskVariableCloud[] = [
{
name: 'var1',
value: 'value1',
id: 'var1',
type: 'string',
hasValue: () => true
}
];
const change = new SimpleChange(null, formValues, false);
formComponent.data = formValues;
formComponent.ngOnChanges({ data: change });
});
it('should not change form if custom form values is empty array', () => {
const formModel = new FormModel({
id: 'id',
taskId: 'task-id',
fields: [{ id: 'field1' }, { id: 'field2' }]
});
formComponent.form = formModel;
const formValues: TaskVariableCloud[] = [];
const change = new SimpleChange(null, formValues, false);
formComponent.ngOnChanges({ data: change });
expect(formComponent.form).toEqual(formModel);
});
it('should save task form and raise corresponding event', () => {
spyOn(formCloudService, 'saveTaskForm').and.callFake(
() =>

View File

@ -242,13 +242,14 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
return;
}
const data = changes['data'];
if (data?.currentValue) {
const data = changes['data']?.currentValue;
if (data?.length > 0) {
this.refreshFormData();
return;
}
const formRepresentation = changes['form'];
if (formRepresentation?.currentValue) {
this.form = formRepresentation.currentValue;
this.onFormLoaded(this.form);

View File

@ -312,7 +312,7 @@
"ADF_CLOUD_TASK_HEADER": {
"BUTTON": {
"CLAIM": "Beanspruchen",
"RELEASE": "Anspruch aufheben"
"RELEASE": "Freigeben"
},
"PROPERTIES": {
"TASK_NAME": "Aufgabe",
@ -373,7 +373,7 @@
"COMPLETE": "Abschließen",
"CANCEL": "Abbrechen",
"CLAIM": "Beanspruchen",
"UNCLAIM": "Anspruch aufheben"
"UNCLAIM": "Freigeben "
}
},
"ERROR": {

View File

@ -17,13 +17,16 @@
class="adf-process-input-container"
floatLabel="always"
*ngIf="showSelectProcessDropdown"
data-automation-id="adf-select-cloud-process-dropdown">
data-automation-id="adf-select-cloud-process-dropdown"
>
<mat-label class="adf-start-process-input-label">{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.TYPE' | translate }}</mat-label>
<input
matInput
formControlName="processDefinition"
[matAutocomplete]="auto"
id="processDefinitionName">
id="processDefinitionName"
>
<div class="adf-process-input-autocomplete">
<mat-autocomplete
#auto="matAutocomplete"
@ -37,6 +40,7 @@
{{ getProcessDefinitionValue(processDef) }}
</mat-option>
</mat-autocomplete>
<button
id="adf-select-process-dropdown"
title="{{'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.SELECT_PROCESS_DROPDOWN' | translate}}"
@ -44,6 +48,7 @@
(click)="displayDropdown($event)">
<mat-icon>arrow_drop_down</mat-icon>
</button>
</div>
<mat-error
*ngIf="processDefinition.hasError('required')"
@ -79,8 +84,8 @@
[data]="resolvedValues"
[formId]="processDefinitionCurrent.formKey"
[displayModeConfigurations]="displayModeConfigurations"
[showSaveButton]="false"
[showCompleteButton]="false"
[showSaveButton]="showSaveButton"
[showCompleteButton]="showCompleteButton"
[showRefreshButton]="false"
[showValidationIcon]="false"
[showTitle]="false"

View File

@ -35,7 +35,8 @@ import {
FormModel,
InplaceFormInputComponent,
LocalizedDatePipe,
TranslationService
TranslationService,
isOutcomeButtonVisible
} from '@alfresco/adf-core';
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';
import { MatAutocompleteModule, MatAutocompleteTrigger } from '@angular/material/autocomplete';
@ -95,8 +96,6 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
@ViewChild(MatAutocompleteTrigger)
inputAutocomplete: MatAutocompleteTrigger;
@ViewChild('startForm') startForm: FormCloudComponent;
/** (required) Name of the app. */
@Input()
appName: string = '';
@ -207,6 +206,9 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
private readonly hasVisibleOutcomesSubject = new BehaviorSubject<boolean>(false);
private readonly dialog = inject(MatDialog);
showSaveButton = false;
showCompleteButton = false;
get isProcessFormValid(): boolean {
if (this.hasForm && this.isFormCloudLoaded) {
return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting;
@ -256,6 +258,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
.subscribe((processDefinitionName) => {
this.selectProcessDefinitionByProcessDefinitionName(processDefinitionName);
});
this.showStartProcessButton$ = combineLatest([this.displayStartSubject, this.hasVisibleOutcomesSubject]).pipe(
map(([displayStart, hasVisibleOutcomes]) => (displayStart !== null ? displayStart === 'true' : !hasVisibleOutcomes))
);
@ -284,9 +287,14 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
this.isFormCloudLoaded = true;
this.formCloud = form;
if (this.startForm) {
this.hasVisibleOutcomesSubject.next(this.startForm.hasVisibleOutcomes);
}
const anyOutcomeVisible = form?.outcomes?.some((outcome) =>
isOutcomeButtonVisible(outcome, {
isFormReadOnly: form.readOnly,
showCompleteButton: this.showCompleteButton,
showSaveButton: this.showSaveButton
})
);
this.hasVisibleOutcomesSubject.next(anyOutcomeVisible);
}
private getMaxNameLength(): number {

View File

@ -21,6 +21,7 @@ import { ScreenRenderingService } from '../../../services/public-api';
import { MatCardModule } from '@angular/material/card';
import { UserTaskCustomUi } from '../../models/screen-cloud.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCheckboxChange } from '@angular/material/checkbox';
@Component({
selector: 'adf-cloud-task-screen',
@ -68,13 +69,21 @@ export class TaskScreenCloudComponent implements OnInit {
@Input()
rootProcessInstanceId: string = '';
/** Whether the `Open next task` checkbox is checked by default or not. */
@Input()
isNextTaskCheckboxChecked = false;
/** Toggle rendering of the `Open next task` checkbox. */
@Input()
showNextTaskCheckbox = false;
/** Emitted when the task is saved. */
@Output()
taskSaved = new EventEmitter();
/** Emitted when the task is completed. */
@Output()
taskCompleted = new EventEmitter();
taskCompleted = new EventEmitter<any>();
/** Emitted when there is an error. */
@Output()
@ -92,6 +101,10 @@ export class TaskScreenCloudComponent implements OnInit {
@Output()
unclaimTask = new EventEmitter<any>();
/** Emitted when the `Open next task` checkbox was toggled. */
@Output()
nextTaskCheckboxCheckedChanged = new EventEmitter<MatCheckboxChange>();
@ViewChild('container', { read: ViewContainerRef, static: true })
container: ViewContainerRef;
@ -140,6 +153,12 @@ export class TaskScreenCloudComponent implements OnInit {
if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'rootProcessInstanceId')) {
this.componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId);
}
if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showNextTaskCheckbox')) {
this.componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox);
}
if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'isNextTaskCheckboxChecked')) {
this.componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked);
}
}
subscribeToOutputs() {
@ -147,7 +166,9 @@ export class TaskScreenCloudComponent implements OnInit {
this.componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit());
}
if (this.componentRef.instance?.taskCompleted) {
this.componentRef.instance.taskCompleted.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskCompleted.emit());
this.componentRef.instance.taskCompleted
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((openNextTask) => this.taskCompleted.emit(openNextTask));
}
if (this.componentRef.instance?.error) {
this.componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data));
@ -162,6 +183,11 @@ export class TaskScreenCloudComponent implements OnInit {
if (this.componentRef.instance?.cancelTask) {
this.componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data));
}
if (this.componentRef.instance?.nextTaskCheckboxCheckedChanged) {
this.componentRef.instance.nextTaskCheckboxCheckedChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => this.nextTaskCheckboxCheckedChanged.emit(data));
}
}
switchToDisplayMode(newDisplayMode?: string) {

View File

@ -27,11 +27,14 @@ export interface UserTaskCustomUi {
showCancelButton: boolean;
taskName: string;
taskId: string;
isNextTaskCheckboxChecked: boolean;
showNextTaskCheckbox: boolean;
cancelTask: EventEmitter<any>;
claimTask: EventEmitter<any>;
error: EventEmitter<any>;
switchToDisplayMode?: (newDisplayMode?: string) => void;
taskCompleted: EventEmitter<string>;
taskCompleted: EventEmitter<any>;
taskSaved: EventEmitter<string>;
unclaimTask: EventEmitter<any>;
nextTaskCheckboxCheckedChanged: EventEmitter<any>;
}

View File

@ -1,4 +1,4 @@
<div class="adf-user-task-cloud-container">
<div class="adf-user-task-cloud-container">
<div *ngIf="!loading; else loadingTemplate">
<ng-container [ngSwitch]="taskType">
<ng-container *ngSwitchCase="taskTypeEnum.Form">
@ -38,12 +38,16 @@
[showCancelButton]="showCancelButton"
[taskName]="taskDetails.name"
[taskId]="taskId"
[showNextTaskCheckbox]="showNextTaskCheckbox && canCompleteTask()"
[isNextTaskCheckboxChecked]="isNextTaskCheckboxChecked"
(cancelTask)="onCancelClick()"
(claimTask)="onClaimTask()"
(error)="onError($event)"
(taskCompleted)="onCompleteTask()"
(taskCompleted)="onCompleteTask($event)"
(taskSaved)="onFormSaved()"
(unclaimTask)="onUnclaimTask()"
(nextTaskCheckboxCheckedChanged)="onNextTaskCheckboxCheckedChanged($event)"
/>
</ng-container>

View File

@ -334,7 +334,7 @@ describe('UserTaskCloudComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(component.taskCompleted.emit).toHaveBeenCalledOnceWith('task1');
expect(component.taskCompleted.emit).toHaveBeenCalledOnceWith(false);
});
it('should emit taskClaimed when task is claimed', async () => {

View File

@ -150,7 +150,7 @@ export class UserTaskCloudComponent implements OnInit, OnChanges {
/** Emitted when the task is completed. */
@Output()
taskCompleted = new EventEmitter<string>();
taskCompleted = new EventEmitter<boolean>();
candidateUsers: string[] = [];
candidateGroups: string[] = [];
@ -242,9 +242,9 @@ export class UserTaskCloudComponent implements OnInit, OnChanges {
this.taskClaimed.emit(this.taskId);
}
onCompleteTask(): void {
onCompleteTask(openNextTask: boolean = false): void {
this.loadTask();
this.taskCompleted.emit(this.taskId);
this.taskCompleted.emit(openNextTask);
}
onCompleteTaskForm(): void {

View File

@ -73,7 +73,7 @@
"BUTTON": {
"COMPLETE": "Abschließen",
"CLAIM": "Beanspruchen",
"UNCLAIM": "Anspruch aufheben",
"UNCLAIM": "Freigeben ",
"DRAG-ATTACHMENT": "Dateien zum Hochladen ablegen",
"UPLOAD-ATTACHMENT": "Anhang hochladen"
},
@ -343,7 +343,7 @@
"COMPLETE": "Abschließen",
"CANCEL": "Abbrechen",
"CLAIM": "Beanspruchen",
"UNCLAIM": "Anspruch aufheben"
"UNCLAIM": "Freigeben "
}
},
"COMPLETED_TASK": {

742
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -69,7 +69,7 @@
"devDependencies": {
"@alfresco/eslint-plugin-eslint-angular": "file:lib/eslint-angular",
"@angular-devkit/architect": "0.1802.13",
"@angular-devkit/build-angular": "18.2.14",
"@angular-devkit/build-angular": "18.2.19",
"@angular-devkit/core": "18.2.13",
"@angular-devkit/schematics": "18.2.13",
"@angular-eslint/eslint-plugin": "17.0.1",