ACS-11224 Migrate upload components to Angular control flow (#11691)

This commit is contained in:
Denys Vuika
2026-02-26 14:54:25 +00:00
committed by GitHub
parent 68d174c3e6
commit 3535fa3d89
44 changed files with 1072 additions and 1557 deletions
@@ -1,74 +1,79 @@
<nav
*ngIf="folderNode"
data-automation-id="breadcrumb"
class="adf-breadcrumb-container"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate"
>
<button
*ngIf="hasPreviousNodes()"
tabindex="0"
class="adf-breadcrumb-dropdown-trigger"
(click)="open()"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.DROPDOWN' | translate"
@if (folderNode) {
<nav
data-automation-id="breadcrumb"
class="adf-breadcrumb-container"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate"
>
<div class="adf-breadcrumb-dropdown-trigger-icon">
<mat-icon [class.adf-isRoot]="!hasPreviousNodes()" adf-icon="folder" />
<mat-icon [class.adf-isRoot]="!hasPreviousNodes()" class="adf-breadcrumb-dropdown-trigger-arrow" adf-icon="arrow_drop_down" />
</div>
</button>
@if (hasPreviousNodes()) {
<button
tabindex="0"
class="adf-breadcrumb-dropdown-trigger"
(click)="open()"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.DROPDOWN' | translate"
>
<div class="adf-breadcrumb-dropdown-trigger-icon">
<mat-icon [class.adf-isRoot]="!hasPreviousNodes()" adf-icon="folder" />
<mat-icon [class.adf-isRoot]="!hasPreviousNodes()" class="adf-breadcrumb-dropdown-trigger-arrow" adf-icon="arrow_drop_down" />
</div>
</button>
<mat-select
#dropdown
class="adf-breadcrumb-dropdown-path"
tabindex="-1"
role="button"
>
@for (node of previousNodes; track node.id) {
<mat-option
(click)="onRoutePathClick(node, $event)"
(onSelectionChange)="onRouteClick(node)"
class="adf-breadcrumb-path-option"
[disabled]="readOnly"
>
{{ node.name ?? '' | translate }}
</mat-option>
}
</mat-select>
}
<mat-select
#dropdown
*ngIf="hasPreviousNodes()"
class="adf-breadcrumb-dropdown-path"
tabindex="-1"
role="button"
>
<mat-option
*ngFor="let node of previousNodes"
(click)="onRoutePathClick(node, $event)"
(onSelectionChange)="onRouteClick(node)"
class="adf-breadcrumb-path-option"
[disabled]="readOnly"
@for (item of lastNodes; track item.id; let last = $last) {
<div
[class.adf-active]="last"
title="{{ item.name ?? '' | translate }}"
class="adf-breadcrumb-item">
@if (breadcrumbItemIsAnchor(last)) {
<a
href="#"
[attr.data-automation-id]="'breadcrumb_' + item.name"
class="adf-breadcrumb-item-anchor"
(click)="onRoutePathClick(item, $event)"
>
{{ item.name ?? '' | translate }}
</a>
} @else {
<div class="adf-breadcrumb-item-current" aria-current="location">
{{ (selectedRowItemsCount < 1 ? item.name ?? '' : 'BREADCRUMB.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}
</div>
}
@if (!last) {
<mat-icon class="adf-breadcrumb-item-chevron" adf-icon="chevron_right" />
}
</div>
}
</nav>
@if (!folderNode && hasRoot) {
<nav
data-automation-id="breadcrumb"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate"
>
{{ node.name | translate }}
</mat-option>
</mat-select>
<div
*ngFor="let item of lastNodes; let last = last"
[class.adf-active]="last"
[ngSwitch]="breadcrumbItemIsAnchor(last)"
title="{{ item.name | translate }}"
class="adf-breadcrumb-item">
<a
*ngSwitchCase="true"
href="#"
[attr.data-automation-id]="'breadcrumb_' + item.name"
class="adf-breadcrumb-item-anchor"
(click)="onRoutePathClick(item, $event)"
>
{{ item.name | translate }}
</a>
<div *ngSwitchDefault class="adf-breadcrumb-item-current" aria-current="location">
{{ (selectedRowItemsCount < 1 ? item.name : 'BREADCRUMB.HEADER.SELECTED') | translate: { count: selectedRowItemsCount } }}
</div>
<mat-icon class="adf-breadcrumb-item-chevron" *ngIf="!last" adf-icon="chevron_right" />
</div>
</nav>
<nav
*ngIf="!folderNode && hasRoot"
data-automation-id="breadcrumb"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate"
>
<div class="adf-breadcrumb-item adf-active" role="group">
<div class="adf-breadcrumb-item-current">
{{ root | translate }}
</div>
</div>
</nav>
<div class="adf-breadcrumb-item adf-active" role="group">
<div class="adf-breadcrumb-item-current">
{{ root ?? '' | translate }}
</div>
</div>
</nav>
}
}
@@ -31,7 +31,6 @@ import {
import { MatSelect, MatSelectModule } from '@angular/material/select';
import { Node, PathElement } from '@alfresco/js-api';
import { DocumentListComponent } from '../document-list/components/document-list.component';
import { CommonModule } from '@angular/common';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { IconModule } from '@alfresco/adf-core';
@@ -39,7 +38,7 @@ import { LiveAnnouncer } from '@angular/cdk/a11y';
@Component({
selector: 'adf-breadcrumb',
imports: [CommonModule, IconModule, TranslatePipe, MatSelectModule],
imports: [IconModule, TranslatePipe, MatSelectModule],
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
@@ -1,29 +0,0 @@
/*!
* @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 { NgModule } from '@angular/core';
import { BreadcrumbComponent } from './breadcrumb.component';
import { DropdownBreadcrumbComponent } from './dropdown-breadcrumb.component';
export const BREADCRUMB_DIRECTIVES = [BreadcrumbComponent, DropdownBreadcrumbComponent] as const;
/** @deprecated use `...BREADCRUMB_DIRECTIVES` instead */
@NgModule({
imports: [...BREADCRUMB_DIRECTIVES],
exports: [...BREADCRUMB_DIRECTIVES]
})
export class BreadcrumbModule {}
@@ -1,48 +1,47 @@
<nav *ngIf="route.length > 0" class="adf-dropdown-breadcrumb-container"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate">
@if (route.length > 0) {
<nav class="adf-dropdown-breadcrumb-container"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate">
<button
id="dropdown-breadcrumb-button"
[tabindex]="hasPreviousNodes() ? 0 : -1"
class="adf-dropdown-breadcrumb-trigger"
(click)="open()"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.DROPDOWN' | translate:{ folderName: currentNode?.name }"
[attr.aria-haspopup]="hasPreviousNodes() ? 'true' : null"
[attr.aria-expanded]="dropdown?.panelOpen || false"
data-automation-id="dropdown-breadcrumb-trigger">
<mat-icon class="adf-dropdown-breadcrumb-icon" [class.adf-isRoot]="!hasPreviousNodes()" adf-icon="folder" />
id="dropdown-breadcrumb-button"
[tabindex]="hasPreviousNodes() ? 0 : -1"
class="adf-dropdown-breadcrumb-trigger"
(click)="open()"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.DROPDOWN' | translate:{ folderName: currentNode?.name }"
[attr.aria-haspopup]="hasPreviousNodes() ? 'true' : null"
[attr.aria-expanded]="dropdown?.panelOpen || false"
data-automation-id="dropdown-breadcrumb-trigger">
<mat-icon class="adf-dropdown-breadcrumb-icon" [class.adf-isRoot]="!hasPreviousNodes()" adf-icon="folder" />
</button>
<mat-icon class="adf-dropdown-breadcrumb-icon adf-dropdown-breadcrumb-item-chevron" adf-icon="chevron_right" />
<div class="adf-dropdown-breadcrumb-path">
@if (hasPreviousNodes()) {
<mat-select
#dropdown
*ngIf="hasPreviousNodes()"
panelClass="adf-breadcrumb-dropdown-panel"
class="adf-dropdown-breadcrumb-path-select"
tabindex="-1"
role="button"
data-automation-id="dropdown-breadcrumb-path"
aria-labelledby="dropdown-breadcrumb-button">
#dropdown
panelClass="adf-breadcrumb-dropdown-panel"
class="adf-dropdown-breadcrumb-path-select"
tabindex="-1"
role="button"
data-automation-id="dropdown-breadcrumb-path"
aria-labelledby="dropdown-breadcrumb-button">
@for (node of previousNodes; track node.id) {
<mat-option
*ngFor="let node of previousNodes;"
(click)="onRoutePathClick(node, $event)"
(onSelectionChange)="onRouteClick(node)"
class="adf-dropdown-breadcrumb-path-option adf-dropdown-breadcrumb-path-option-node"
data-automation-class="dropdown-breadcrumb-path-option"
[disabled]="readOnly">
{{ node.name | translate }}
(click)="onRoutePathClick(node, $event)"
(onSelectionChange)="onRouteClick(node)"
class="adf-dropdown-breadcrumb-path-option adf-dropdown-breadcrumb-path-option-node"
data-automation-class="dropdown-breadcrumb-path-option"
[disabled]="readOnly">
{{ node.name ?? '' | translate }}
</mat-option>
}
</mat-select>
}
</div>
<span
class="adf-current-folder"
[class.adf-isRoot]="!hasPreviousNodes()"
data-automation-id="current-folder"
aria-current="location">{{ currentNode.name }}
class="adf-current-folder"
[class.adf-isRoot]="!hasPreviousNodes()"
data-automation-id="current-folder"
aria-current="location">{{ currentNode.name }}
</span>
</nav>
</nav>
}
@@ -19,13 +19,13 @@ import { Component, OnChanges, ViewChild, ViewEncapsulation } from '@angular/cor
import { MatSelect, MatSelectModule } from '@angular/material/select';
import { PathElement, Node } from '@alfresco/js-api';
import { BreadcrumbComponent } from './breadcrumb.component';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { IconModule } from '@alfresco/adf-core';
@Component({
selector: 'adf-dropdown-breadcrumb',
imports: [CommonModule, IconModule, MatSelectModule, TranslatePipe],
imports: [IconModule, MatSelectModule, TranslatePipe],
templateUrl: './dropdown-breadcrumb.component.html',
styleUrls: ['./dropdown-breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
@@ -17,5 +17,3 @@
export * from './breadcrumb.component';
export * from './dropdown-breadcrumb.component';
export * from './breadcrumb.module';
@@ -1,39 +0,0 @@
/*!
* @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 { Person } from '@alfresco/js-api';
export const fakeEcmUser: Person = {
id: 'fake-id',
firstName: 'fake-ecm-first-name',
lastName: 'fake-ecm-last-name',
description: 'i am a fake user for test',
avatarId: 'fake-avatar-id',
email: 'fakeEcm@ecmUser.com',
skypeId: 'fake-skype-id',
googleId: 'fake-googleId-id',
instantMessageId: 'fake-instantMessageId-id',
company: null,
jobTitle: 'job-ecm-test',
location: 'fake location',
mobile: '000000000',
telephone: '11111111',
statusUpdatedAt: new Date(),
userStatus: 'active',
enabled: true,
emailNotificationsEnabled: true
};
@@ -41,4 +41,3 @@ export * from './models/allowable-operations.enum';
export * from './interfaces/search-configuration.interface';
export * from './interfaces/saved-search.interface';
export * from './interfaces/saved-searches-strategy.interface';
export * from './mocks/ecm-user.service.mock';
@@ -15,17 +15,37 @@
* limitations under the License.
*/
import { fakeEcmUser } from '../mocks/ecm-user.service.mock';
import { RedirectAuthService } from '@alfresco/adf-core';
import { PeopleContentQueryRequestModel, PeopleContentService } from './people-content.service';
import { TestBed } from '@angular/core/testing';
import { PersonPaging } from '@alfresco/js-api';
import { Person, PersonPaging } from '@alfresco/js-api';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { EMPTY, firstValueFrom, of } from 'rxjs';
import { AlfrescoApiService } from '../../services';
import { AlfrescoApiServiceMock } from '../../mock';
const fakeEcmUser: Person = {
id: 'fake-id',
firstName: 'fake-ecm-first-name',
lastName: 'fake-ecm-last-name',
description: 'i am a fake user for test',
avatarId: 'fake-avatar-id',
email: 'fakeEcm@ecmUser.com',
skypeId: 'fake-skype-id',
googleId: 'fake-googleId-id',
instantMessageId: 'fake-instantMessageId-id',
company: null,
jobTitle: 'job-ecm-test',
location: 'fake location',
mobile: '000000000',
telephone: '11111111',
statusUpdatedAt: new Date(),
userStatus: 'active',
enabled: true,
emailNotificationsEnabled: true
};
export const fakeEcmUser2 = {
id: 'another-fake-id',
firstName: 'another-fake-first-name',
@@ -76,7 +76,7 @@
<div>
<ng-container *ngIf="isUploadEnabled()">
<adf-upload-button
[staticTitle]="'FORM.FIELD.UPLOAD' | translate "
[staticTitle]="'FORM.FIELD.UPLOAD' | translate"
[multipleFiles]="isMultipleSelection()"
[rootFolderId]="currentDirectoryId"
[disabled]="isNotAllowedToUpload()"
+2 -32
View File
@@ -21,7 +21,6 @@ import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimep
import { CONTENT_TAG_DIRECTIVES } from './tag/tag.module';
import { DOCUMENT_LIST_DIRECTIVES } from './document-list/document-list.module';
import { CONTENT_SEARCH_DIRECTIVES } from './search/search.module';
import { BREADCRUMB_DIRECTIVES } from './breadcrumb/breadcrumb.module';
import { CONTENT_VERSION_DIRECTIVES } from './version-manager/version-manager.module';
import { CONTENT_NODE_SELECTOR_DIRECTIVES } from './content-node-selector/content-node-selector.module';
import { CONTENT_NODE_SHARE_DIRECTIVES } from './content-node-share/content-node-share.module';
@@ -34,17 +33,8 @@ import { versionCompatibilityFactory } from './version-compatibility/version-com
import { VersionCompatibilityService } from './version-compatibility/version-compatibility.service';
import { contentAuthLoaderFactory } from './auth-loader/content-auth-loader-factory';
import { ContentAuthLoaderService } from './auth-loader/content-auth-loader.service';
import { CategoriesManagementComponent } from './category';
import { TreeComponent } from './tree';
import { NewVersionUploaderDialogComponent } from './new-version-uploader';
import { VersionCompatibilityDirective } from './version-compatibility';
import { CONTENT_UPLOAD_DIRECTIVES } from './upload';
import { TreeViewComponent } from './tree-view';
import { NodeCommentsComponent } from './node-comments';
import { AlfrescoViewerComponent } from './viewer';
import { ContentTypeDialogComponent } from './content-type';
import { MaterialModule } from './material.module';
import { AlfrescoIconComponent } from './alfresco-icon/alfresco-icon.component';
import { AlfrescoApiService } from './services/alfresco-api.service';
import { AlfrescoApiNoAuthService } from './api-factories/alfresco-api-no-auth.service';
import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-factories/alfresco-api-v2-loader.service';
@@ -59,23 +49,13 @@ import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-facto
...CONTENT_SEARCH_DIRECTIVES,
...DOCUMENT_LIST_DIRECTIVES,
...CONTENT_UPLOAD_DIRECTIVES,
...BREADCRUMB_DIRECTIVES,
...CONTENT_NODE_SELECTOR_DIRECTIVES,
...CONTENT_NODE_SHARE_DIRECTIVES,
...CONTENT_METADATA_DIRECTIVES,
...CONTENT_DIRECTIVES,
...CONTENT_PERMISSION_MANAGER_DIRECTIVES,
...CONTENT_VERSION_DIRECTIVES,
TreeViewComponent,
ContentTypeDialogComponent,
...ASPECT_LIST_DIRECTIVES,
VersionCompatibilityDirective,
NodeCommentsComponent,
TreeComponent,
AlfrescoViewerComponent,
CategoriesManagementComponent,
NewVersionUploaderDialogComponent,
AlfrescoIconComponent
...ASPECT_LIST_DIRECTIVES
],
providers: [provideTranslations('adf-content-services', 'assets/adf-content-services')],
exports: [
@@ -84,7 +64,6 @@ import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-facto
...DOCUMENT_LIST_DIRECTIVES,
...CONTENT_UPLOAD_DIRECTIVES,
...CONTENT_SEARCH_DIRECTIVES,
...BREADCRUMB_DIRECTIVES,
...CONTENT_NODE_SELECTOR_DIRECTIVES,
...CONTENT_NODE_SHARE_DIRECTIVES,
...CONTENT_METADATA_DIRECTIVES,
@@ -92,16 +71,7 @@ import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-facto
...CONTENT_DIRECTIVES,
...CONTENT_PERMISSION_MANAGER_DIRECTIVES,
...CONTENT_VERSION_DIRECTIVES,
TreeViewComponent,
...ASPECT_LIST_DIRECTIVES,
ContentTypeDialogComponent,
VersionCompatibilityDirective,
NodeCommentsComponent,
TreeComponent,
AlfrescoViewerComponent,
CategoriesManagementComponent,
NewVersionUploaderDialogComponent,
AlfrescoIconComponent
...ASPECT_LIST_DIRECTIVES
]
})
export class ContentModule {
@@ -1,116 +1,117 @@
<div *ngIf="canShowDialog()"
role="dialog"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG'| translate"
tabindex="0"
class="adf-upload-dialog"
id="upload-dialog"
[class.adf-upload-dialog--minimized]="isDialogMinimized">
@if (canShowDialog()) {
<div
#uploadDialog
role="dialog"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG'| translate"
tabindex="0"
class="adf-upload-dialog"
id="upload-dialog"
[class.adf-upload-dialog--minimized]="isDialogMinimized">
<header class="adf-upload-dialog__header">
<button
mat-icon-button
class="adf-upload-dialog__header-button"
[attr.data-automation-id]="'adf-upload-dialog__toggle-minimize'"
[attr.aria-label]="(isDialogMinimized ?
'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG_MAXIMIZE':
'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG_MINIMIZE') | translate"
[disabled]="isConfirmation"
[attr.aria-expanded]="!isDialogMinimized"
(click)="toggleMinimized()">
<mat-icon class="adf-upload-dialog__header-button-icon" [adf-icon]="isDialogMinimized ? 'keyboard_arrow_up' : 'keyboard_arrow_down'" />
</button>
<button
mat-icon-button
class="adf-upload-dialog__header-button"
[attr.data-automation-id]="'adf-upload-dialog__toggle-minimize'"
[attr.aria-label]="(isDialogMinimized ?
'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG_MAXIMIZE':
'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG_MINIMIZE') | translate"
[disabled]="isConfirmation"
[attr.aria-expanded]="!isDialogMinimized"
(click)="toggleMinimized()">
<mat-icon class="adf-upload-dialog__header-button-icon" [adf-icon]="isDialogMinimized ? 'keyboard_arrow_up' : 'keyboard_arrow_down'" />
</button>
<span
class="adf-upload-dialog__title"
*ngIf="!uploadList.isUploadCancelled()">
@if (uploadList.isUploadCancelled()) {
<span class="adf-upload-dialog__title">
{{ 'FILE_UPLOAD.MESSAGES.UPLOAD_CANCELED' | translate }}
</span>
} @else {
<span class="adf-upload-dialog__title">
{{ 'FILE_UPLOAD.MESSAGES.UPLOAD_PROGRESS'
| translate: {
completed: totalCompleted,
total: filesUploadingList.length
}
}}
| translate: {
completed: totalCompleted,
total: filesUploadingList.length
}
}}
</span>
<span
class="adf-upload-dialog__title"
*ngIf="uploadList.isUploadCancelled()">
{{ 'FILE_UPLOAD.MESSAGES.UPLOAD_CANCELED' | translate }}
</span>
</header>
<section class="adf-upload-dialog__info"
*ngIf="totalErrors">
{{
(totalErrors > 1
? 'FILE_UPLOAD.MESSAGES.UPLOAD_ERRORS'
: 'FILE_UPLOAD.MESSAGES.UPLOAD_ERROR')
| translate: { total: totalErrors }
}}
}
</header>
@if (totalErrors) {
<section class="adf-upload-dialog__info">
{{
(totalErrors > 1
? 'FILE_UPLOAD.MESSAGES.UPLOAD_ERRORS'
: 'FILE_UPLOAD.MESSAGES.UPLOAD_ERROR')
| translate: { total: totalErrors }
}}
</section>
<section class="adf-upload-dialog__content"
[class.adf-upload-dialog--padding]="isConfirmation" aria-live="polite">
<adf-file-uploading-list
class="adf-file-uploading-list"
[class.adf-upload-dialog--hide]="isConfirmation"
#uploadList
[files]="filesUploadingList">
<ng-template let-file="$implicit">
<adf-file-uploading-list-row
[file]="file"
(cancel)="uploadList.cancelFile(file)" />
</ng-template>
</adf-file-uploading-list>
<div
aria-live="polite"
aria-labelledby="confirmationTitle"
aria-describedby="confirmationDescription"
class="adf-upload-dialog__confirmation"
[class.adf-upload-dialog--hide]="!isConfirmation">
<p role="heading" aria-level="2" id="confirmationTitle" class="adf-upload-dialog__confirmation--title">
{{ 'ADF_FILE_UPLOAD.CONFIRMATION.MESSAGE.TITLE' | translate }}
</p>
<p id="confirmationDescription" class="adf-upload-dialog__confirmation--text">
{{ 'ADF_FILE_UPLOAD.CONFIRMATION.MESSAGE.TEXT' | translate }}
</p>
</div>
</section>
<footer class="adf-upload-dialog__actions" *ngIf="!isConfirmation">
}
<section class="adf-upload-dialog__content"
[class.adf-upload-dialog--padding]="isConfirmation" aria-live="polite">
<adf-file-uploading-list
class="adf-file-uploading-list"
[class.adf-upload-dialog--hide]="isConfirmation"
#uploadList
[files]="filesUploadingList">
<ng-template let-file="$implicit">
<adf-file-uploading-list-row
[file]="file"
(cancel)="uploadList.cancelFile(file)" />
</ng-template>
</adf-file-uploading-list>
<div
aria-live="polite"
aria-labelledby="confirmationTitle"
aria-describedby="confirmationDescription"
class="adf-upload-dialog__confirmation"
[class.adf-upload-dialog--hide]="!isConfirmation">
<p role="heading" aria-level="2" id="confirmationTitle" class="adf-upload-dialog__confirmation--title">
{{ 'ADF_FILE_UPLOAD.CONFIRMATION.MESSAGE.TITLE' | translate }}
</p>
<p id="confirmationDescription" class="adf-upload-dialog__confirmation--text">
{{ 'ADF_FILE_UPLOAD.CONFIRMATION.MESSAGE.TEXT' | translate }}
</p>
</div>
</section>
@if (!isConfirmation) {
<footer class="adf-upload-dialog__actions">
@if (canShowCancelAll()) {
<button
id="adf-upload-dialog-cancel-all"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.STOP_ALL' | translate"
color="primary"
mat-button
*ngIf="canShowCancelAll()"
(click)="toggleConfirmation()"
id="adf-upload-dialog-cancel-all"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.STOP_ALL' | translate"
color="primary"
mat-button
(click)="toggleConfirmation()"
>{{ 'ADF_FILE_UPLOAD.BUTTON.STOP_ALL' | translate }}</button>
}
@if (canCloseDialog()) {
<button
id="adf-upload-dialog-close"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG_CLOSE' | translate"
*ngIf="canCloseDialog()"
mat-button
color="primary"
(click)="close()"
id="adf-upload-dialog-close"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.DIALOG_CLOSE' | translate"
mat-button
color="primary"
(click)="close()"
>{{ 'ADF_FILE_UPLOAD.BUTTON.CLOSE' | translate }}</button>
}
</footer>
}
@if (isConfirmation) {
<footer class="adf-upload-dialog__actions"
*ngIf="isConfirmation" cdkTrapFocus cdkTrapFocusAutoCapture>
<button
id="adf-upload-dialog-cancel"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.CONFIRMATION.CANCEL' | translate"
mat-button
(click)="cancelAllUploads()"
>{{ 'ADF_FILE_UPLOAD.CONFIRMATION.BUTTON.CANCEL' | translate }}</button>
<button
id="adf-upload-dialog-confirm"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.CONFIRMATION.CONTINUE' | translate"
mat-button
color="primary"
(click)="toggleConfirmation()"
>{{ 'ADF_FILE_UPLOAD.CONFIRMATION.BUTTON.CONTINUE' | translate }}</button>
cdkTrapFocus cdkTrapFocusAutoCapture>
<button
id="adf-upload-dialog-cancel"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.CONFIRMATION.CANCEL' | translate"
mat-button
(click)="cancelAllUploads()"
>{{ 'ADF_FILE_UPLOAD.CONFIRMATION.BUTTON.CANCEL' | translate }}</button>
<button
id="adf-upload-dialog-confirm"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.CONFIRMATION.CONTINUE' | translate"
mat-button
color="primary"
(click)="toggleConfirmation()"
>{{ 'ADF_FILE_UPLOAD.CONFIRMATION.BUTTON.CONTINUE' | translate }}</button>
</footer>
}
</div>
}
@@ -38,7 +38,7 @@ import { delay } from 'rxjs/operators';
import { UploadService } from '../../common/services/upload.service';
import { FileModel, FileUploadStatus } from '../../common/models/file.model';
import { FileUploadCompleteEvent, FileUploadDeleteEvent } from '../../common/events/file.event';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { TranslatePipe } from '@ngx-translate/core';
import { FileUploadingListRowComponent } from './file-uploading-list-row.component';
@@ -47,7 +47,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'adf-file-uploading-dialog',
imports: [CommonModule, MatButtonModule, TranslatePipe, IconModule, FileUploadingListComponent, FileUploadingListRowComponent, A11yModule],
imports: [MatButtonModule, TranslatePipe, IconModule, FileUploadingListComponent, FileUploadingListRowComponent, A11yModule],
templateUrl: './file-uploading-dialog.component.html',
styleUrls: ['./file-uploading-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
@@ -56,7 +56,9 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
private readonly uploadService = inject(UploadService);
private readonly changeDetector = inject(ChangeDetectorRef);
private readonly userPreferencesService = inject(UserPreferencesService);
private readonly elementRef = inject(ElementRef);
@ViewChild('uploadDialog', { read: ElementRef })
private readonly uploadDialogRef: ElementRef<HTMLElement> | undefined;
/** Dialog direction. Can be 'ltr' or 'rtl. */
private direction: Direction = 'ltr';
@@ -70,11 +72,11 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
/** Makes the dialog always visible even when there are no uploads. */
@Input()
alwaysVisible: boolean = false;
alwaysVisible = false;
/** Emitted when a file in the list has an error. */
@Output()
error: EventEmitter<any> = new EventEmitter();
error = new EventEmitter();
@HostBinding('attr.adfUploadDialogRight')
public get isPositionRight(): boolean {
@@ -98,10 +100,7 @@ export class FileUploadingDialogComponent implements OnInit, OnDestroy {
ngOnInit() {
this.dialogActive.pipe(delay(100), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
const element: any = this.elementRef.nativeElement.querySelector('#upload-dialog');
if (element) {
element.focus();
}
this.uploadDialogRef?.nativeElement?.focus();
});
this.uploadService.queueChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((fileList) => {
@@ -1,92 +1,113 @@
<div class="adf-file-uploading-row" [attr.aria-label]="file.name + ' ' + file.status">
<mat-icon *ngIf="mimeType === 'default'" matListItemIcon class="adf-file-uploading-row__type" adf-icon="insert_drive_file" />
@if (mimeType === 'default') {
<mat-icon matListItemIcon class="adf-file-uploading-row__type" adf-icon="insert_drive_file" />
} @else {
<mat-icon svgIcon="adf:{{ mimeType }}" aria-hidden="true" />
}
<mat-icon *ngIf="mimeType !== 'default'" svgIcon="adf:{{ mimeType }}" aria-hidden="true" />
<span
class="adf-file-uploading-row__name"
title="{{ file.name }}">
{{ file.name }}
</span>
<span
class="adf-file-uploading-row__name"
title="{{ file.name }}">
{{ file.name }}
</span>
<span *ngIf="isUploadVersion()" class="adf-file-uploading-row__version" tabindex="0" >
<mat-chip color="primary"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.VERSION' | translate: { version: versionNumber }"
[title]="'version' + versionNumber"
>{{ versionNumber }}</mat-chip>
@if (isUploadVersion()) {
<span class="adf-file-uploading-row__version" tabindex="0" >
<mat-chip color="primary"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.VERSION' | translate: { version: versionNumber }"
[title]="'version' + versionNumber"
>{{ versionNumber }}</mat-chip>
</span>
}
@if (isUploading()) {
<div
tabindex="0"
role="button"
#toggleIcon="toggleIcon"
adf-toggle-icon
(keyup.enter)="onCancel(file)"
(click)="onCancel(file)"
data-automation-id="cancel-upload-progress"
*ngIf="isUploading()"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.STOP_FILE_UPLOAD' | translate: { file: file.name }"
class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.STOP_FILE' | translate }}">
tabindex="0"
role="button"
(mouseenter)="onToggleMouseEnter(toggleIconState)"
(mouseleave)="onToggleMouseLeave(toggleIconState)"
(focus)="onToggleFocus(toggleIconState)"
(blur)="onToggleBlur(toggleIconState)"
(keyup.enter)="onCancel(file)"
(click)="onCancel(file)"
data-automation-id="cancel-upload-progress"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.STOP_FILE_UPLOAD' | translate: { file: file.name }"
class="adf-file-uploading-row__group adf-file-uploading-row__group--toggle"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.STOP_FILE' | translate }}">
<span class="adf-file-uploading-row__status" *ngIf="!toggleIcon.isToggled">
@if (toggleIconState.isToggled) {
<mat-icon class="adf-file-uploading-row__action adf-file-uploading-row__action--cancel" adf-icon="clear" />
} @else {
<span class="adf-file-uploading-row__status">
{{ file.progress.loaded | adfFileSize }} / {{ file.progress.total | adfFileSize }}
</span>
<mat-icon *ngIf="toggleIcon.isToggled" class="adf-file-uploading-row__action adf-file-uploading-row__action--cancel" adf-icon="clear" />
}
</div>
}
@if (isUploadComplete()) {
<button mat-icon-button
adf-toggle-icon
#toggleIcon="toggleIcon"
*ngIf="isUploadComplete()"
class="adf-file-uploading-row__group"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.UPLOAD_SUCCESSFUL' | translate: { file: file.name }"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.UPLOAD_SUCCESSFUL' | translate }}">
<mat-icon class="adf-file-uploading-row__status adf-file-uploading-row__status--done" adf-icon="check_circle" />
(mouseenter)="onToggleMouseEnter(toggleIconState)"
(mouseleave)="onToggleMouseLeave(toggleIconState)"
(focus)="onToggleFocus(toggleIconState)"
(blur)="onToggleBlur(toggleIconState)"
class="adf-file-uploading-row__group"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.UPLOAD_SUCCESSFUL' | translate: { file: file.name }"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.UPLOAD_SUCCESSFUL' | translate }}">
<mat-icon class="adf-file-uploading-row__status adf-file-uploading-row__status--done" adf-icon="check_circle" />
</button>
}
@if (isUploadVersionComplete()) {
<div
*ngIf="isUploadVersionComplete()"
class="adf-file-uploading-row__file-version"
[attr.aria-label]="'ADF_FILE_UPLOAD.STATUS.FILE_DONE_STATUS' | translate"
>
<mat-icon class="adf-file-uploading-row__status--done" adf-icon="check_circle" matListItemIcon />
class="adf-file-uploading-row__file-version"
[attr.aria-label]="'ADF_FILE_UPLOAD.STATUS.FILE_DONE_STATUS' | translate"
>
<mat-icon class="adf-file-uploading-row__status--done" adf-icon="check_circle" matListItemIcon />
</div>
}
@if (canCancelUpload()) {
<button
adf-toggle-icon
#toggleIconCancel="toggleIcon"
mat-icon-button
*ngIf="canCancelUpload()"
(click)="onCancel(file)"
data-automation-id="cancel-upload-queue"
class="adf-file-uploading-row__group"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.STOP_FILE' | translate }}"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.STOP_FILE' | translate: { file: file.name }">
<mat-icon *ngIf="!toggleIconCancel.isToggled" class="adf-file-uploading-row__status adf-file-uploading-row__status--pending" adf-icon="schedule" />
(mouseenter)="onToggleMouseEnter(toggleIconCancelState)"
(mouseleave)="onToggleMouseLeave(toggleIconCancelState)"
(focus)="onToggleFocus(toggleIconCancelState)"
(blur)="onToggleBlur(toggleIconCancelState)"
mat-icon-button
(click)="onCancel(file)"
data-automation-id="cancel-upload-queue"
class="adf-file-uploading-row__group"
title="{{ 'ADF_FILE_UPLOAD.BUTTON.STOP_FILE' | translate }}"
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.STOP_FILE' | translate: { file: file.name }">
<mat-icon *ngIf="toggleIconCancel.isToggled" class="adf-file-uploading-row__action adf-file-uploading-row__action--remove" adf-icon="remove_circle" />
@if (toggleIconCancelState.isToggled) {
<mat-icon class="adf-file-uploading-row__action adf-file-uploading-row__action--remove" adf-icon="remove_circle" />
} @else {
<mat-icon class="adf-file-uploading-row__status adf-file-uploading-row__status--pending" adf-icon="schedule" />
}
</button>
}
@if (isUploadError()) {
@let uploadErrorLabel = getFileUploadErrorKey(file.errorCode) | translate;
<div
role="status"
*ngIf="isUploadError()"
class="adf-file-uploading-row__block adf-file-uploading-row__status--error">
<mat-icon
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.UPLOAD_FILE_ERROR' | translate: { error: file.errorCode | adfFileUploadError }"
[title]="file.errorCode | adfFileUploadError"
adf-icon="report_problem"
matListItemIcon
role="status"
class="adf-file-uploading-row__block adf-file-uploading-row__status--error">
<mat-icon
[attr.aria-label]="'ADF_FILE_UPLOAD.ARIA-LABEL.UPLOAD_FILE_ERROR' | translate: { error: uploadErrorLabel }"
[title]="uploadErrorLabel"
adf-icon="report_problem"
matListItemIcon
/>
</div>
}
@if (showCancelledStatus()) {
<div
[attr.aria-label]="'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate"
role="status"
*ngIf="showCancelledStatus()"
class="adf-file-uploading-row__block adf-file-uploading-row__status--cancelled">
{{ 'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate }}
[attr.aria-label]="'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate"
role="status"
class="adf-file-uploading-row__block adf-file-uploading-row__status--cancelled">
{{ 'ADF_FILE_UPLOAD.STATUS.FILE_CANCELED_STATUS' | translate }}
</div>
}
</div>
@@ -106,4 +106,118 @@ describe('FileUploadingListRowComponent', () => {
const cancelButton = getCancelButton();
expect(cancelButton.title).toBe('ADF_FILE_UPLOAD.BUTTON.STOP_FILE');
});
describe('getFileUploadErrorKey', () => {
it('should return correct key format for error code 500', () => {
const result = component.getFileUploadErrorKey(500);
expect(result).toBe('FILE_UPLOAD.ERRORS.500');
});
it('should return correct key format for error code 404', () => {
const result = component.getFileUploadErrorKey(404);
expect(result).toBe('FILE_UPLOAD.ERRORS.404');
});
it('should return correct key format for error code 403', () => {
const result = component.getFileUploadErrorKey(403);
expect(result).toBe('FILE_UPLOAD.ERRORS.403');
});
it('should return GENERIC key when errorCode is null', () => {
const result = component.getFileUploadErrorKey(null);
expect(result).toBe('FILE_UPLOAD.ERRORS.GENERIC');
});
it('should return GENERIC key when errorCode is undefined', () => {
const result = component.getFileUploadErrorKey(undefined);
expect(result).toBe('FILE_UPLOAD.ERRORS.GENERIC');
});
it('should return GENERIC key when errorCode is 0', () => {
const result = component.getFileUploadErrorKey(0);
expect(result).toBe('FILE_UPLOAD.ERRORS.GENERIC');
});
});
describe('Toggle Icon State', () => {
describe('onToggleMouseEnter', () => {
it('should set isToggled to true when not focused', () => {
component.toggleIconState = { isFocused: false, isToggled: false };
component.onToggleMouseEnter(component.toggleIconState);
expect(component.toggleIconState.isToggled).toBe(true);
});
it('should not change isToggled when already focused', () => {
component.toggleIconState = { isFocused: true, isToggled: false };
component.onToggleMouseEnter(component.toggleIconState);
expect(component.toggleIconState.isToggled).toBe(false);
});
});
describe('onToggleMouseLeave', () => {
it('should set isToggled to false when not focused', () => {
component.toggleIconState = { isFocused: false, isToggled: true };
component.onToggleMouseLeave(component.toggleIconState);
expect(component.toggleIconState.isToggled).toBe(false);
});
it('should not change state when focused but not toggled', () => {
component.toggleIconState = { isFocused: true, isToggled: false };
component.onToggleMouseLeave(component.toggleIconState);
expect(component.toggleIconState.isFocused).toBe(true);
expect(component.toggleIconState.isToggled).toBe(false);
});
it('should reset both isFocused and isToggled when both are true', () => {
component.toggleIconState = { isFocused: true, isToggled: true };
component.onToggleMouseLeave(component.toggleIconState);
expect(component.toggleIconState.isFocused).toBe(false);
expect(component.toggleIconState.isToggled).toBe(false);
});
});
describe('onToggleFocus', () => {
it('should set both isFocused and isToggled to true', () => {
component.toggleIconState = { isFocused: false, isToggled: false };
component.onToggleFocus(component.toggleIconState);
expect(component.toggleIconState.isFocused).toBe(true);
expect(component.toggleIconState.isToggled).toBe(true);
});
});
describe('onToggleBlur', () => {
it('should set both isFocused and isToggled to false', () => {
component.toggleIconState = { isFocused: true, isToggled: true };
component.onToggleBlur(component.toggleIconState);
expect(component.toggleIconState.isFocused).toBe(false);
expect(component.toggleIconState.isToggled).toBe(false);
});
});
describe('Multiple toggle states', () => {
it('should maintain independent state for toggleIconState and toggleIconCancelState', () => {
component.onToggleFocus(component.toggleIconState);
component.onToggleMouseEnter(component.toggleIconCancelState);
expect(component.toggleIconState.isFocused).toBe(true);
expect(component.toggleIconState.isToggled).toBe(true);
expect(component.toggleIconCancelState.isFocused).toBe(false);
expect(component.toggleIconCancelState.isToggled).toBe(true);
});
});
});
});
@@ -17,28 +17,21 @@
import { FileModel, FileUploadStatus } from '../../common/models/file.model';
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatListModule } from '@angular/material/list';
import { FileSizePipe, IconModule } from '@alfresco/adf-core';
import { MatChipsModule } from '@angular/material/chips';
import { TranslatePipe } from '@ngx-translate/core';
import { ToggleIconDirective } from '../directives/toggle-icon.directive';
import { MatButtonModule } from '@angular/material/button';
import { FileUploadErrorPipe } from '../pipes/file-upload-error.pipe';
interface ToggleIconState {
isFocused: boolean;
isToggled: boolean;
}
@Component({
selector: 'adf-file-uploading-list-row',
imports: [
CommonModule,
IconModule,
MatListModule,
MatChipsModule,
TranslatePipe,
ToggleIconDirective,
FileSizePipe,
MatButtonModule,
FileUploadErrorPipe
],
imports: [IconModule, MatListModule, MatChipsModule, TranslatePipe, FileSizePipe, MatButtonModule],
templateUrl: './file-uploading-list-row.component.html',
styleUrls: ['./file-uploading-list-row.component.scss'],
encapsulation: ViewEncapsulation.None
@@ -50,10 +43,40 @@ export class FileUploadingListRowComponent {
@Output()
cancel = new EventEmitter<FileModel>();
toggleIconState: ToggleIconState = { isFocused: false, isToggled: false };
toggleIconCancelState: ToggleIconState = { isFocused: false, isToggled: false };
onCancel(file: FileModel): void {
this.cancel.emit(file);
}
onToggleMouseEnter(state: ToggleIconState): void {
if (!state.isFocused) {
state.isToggled = true;
}
}
onToggleMouseLeave(state: ToggleIconState): void {
if (!state.isFocused) {
state.isToggled = false;
}
if (state.isFocused && state.isToggled) {
state.isFocused = false;
state.isToggled = false;
}
}
onToggleFocus(state: ToggleIconState): void {
state.isFocused = true;
state.isToggled = true;
}
onToggleBlur(state: ToggleIconState): void {
state.isFocused = false;
state.isToggled = false;
}
showCancelledStatus(): boolean {
return (
this.file.status === FileUploadStatus.Cancelled ||
@@ -93,4 +116,8 @@ export class FileUploadingListRowComponent {
isUploadVersionComplete(): boolean {
return this.file?.status === FileUploadStatus.Complete && this.isUploadVersion();
}
getFileUploadErrorKey(errorCode?: number | null): string {
return `FILE_UPLOAD.ERRORS.${errorCode || 'GENERIC'}`;
}
}
@@ -1,6 +1,7 @@
<div class="upload-list">
@for (file of files; track file.id) {
<ng-template
ngFor
[ngForOf]="files"
[ngForTemplate]="template" />
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ $implicit: file }" />
}
</div>
@@ -20,11 +20,11 @@ import { UploadService } from '../../common/services/upload.service';
import { FileModel, FileUploadStatus } from '../../common/models/file.model';
import { Component, ContentChild, Input, Output, TemplateRef, EventEmitter, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgTemplateOutlet } from '@angular/common';
@Component({
selector: 'adf-file-uploading-list',
imports: [CommonModule],
imports: [NgTemplateOutlet],
templateUrl: './file-uploading-list.component.html',
styleUrls: ['./file-uploading-list.component.scss']
})
@@ -1,70 +1,74 @@
<form class="adf-upload-button-file-container">
<!--Files Upload-->
<div *ngIf="!uploadFolders">
<!--Single Files Upload-->
<ng-container *ngIf="!multipleFiles">
<input
class="adf-upload-button-file-container-upload-single-file"
id="upload-single-file"
data-automation-id="upload-single-file"
[type]="file ? 'button' : 'file'"
name="uploadFiles"
accept="{{acceptedFilesType}}"
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onFilesAdded($event)"
(click)="onClickUploadButton()"/>
<label tabindex="0" (keydown.enter)="onClickUploadButton()" for="upload-single-file" class="adf-upload-button-label">
<mat-icon class="adf-upload-button-icon" adf-icon="file_upload" />
<span id="upload-single-file-label" *ngIf="!staticTitle">
{{ 'FILE_UPLOAD.BUTTON.UPLOAD_FILE' | translate }}</span>
<span id="upload-single-file-label-static" *ngIf="staticTitle">
{{ staticTitle }}</span>
</label>
</ng-container>
<!--Multiple Files Upload-->
<ng-container *ngIf="multipleFiles">
<input
id="upload-multiple-files"
data-automation-id="upload-multiple-files"
type="file"
name="uploadFiles"
multiple="multiple"
accept="{{acceptedFilesType}}"
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onFilesAdded($event)"/>
<label for="upload-multiple-files" class="adf-upload-button-label">
<mat-icon class="adf-upload-button-icon" adf-icon="file_upload" />
<span id="upload-multiple-file-label" *ngIf="!staticTitle">
{{ 'FILE_UPLOAD.BUTTON.UPLOAD_FILE' | translate }}</span>
<span id="upload-multiple-file-label-static" *ngIf="staticTitle">
{{ staticTitle }}</span>
</label>
</ng-container>
</div>
<!--Folders Upload-->
<ng-container *ngIf="uploadFolders">
<!--Files Upload-->
@if (!uploadFolders) {
<div>
<!--Single Files Upload-->
@if (!multipleFiles) {
<input
id="uploadFolder"
data-automation-id="uploadFolder"
type="file"
name="uploadFiles"
multiple="multiple"
accept="{{acceptedFilesType}}"
webkitdirectory directory
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onDirectoryAdded($event)"/>
<label for="uploadFolder" data-automation-id="uploadFolder" class="adf-upload-button-label">
<mat-icon class="adf-upload-button-icon" adf-icon="file_upload" />
<span id="uploadFolder-label" *ngIf="!staticTitle">
{{ 'FILE_UPLOAD.BUTTON.UPLOAD_FOLDER' | translate }}</span>
<span id="uploadFolder-label-static" *ngIf="staticTitle">
{{ staticTitle }}</span>
class="adf-upload-button-file-container-upload-single-file"
id="upload-single-file"
data-automation-id="upload-single-file"
[type]="file ? 'button' : 'file'"
name="uploadFiles"
accept="{{acceptedFilesType}}"
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onFilesAdded($event)"
(click)="onClickUploadButton()"/>
<label tabindex="0" (keydown.enter)="onClickUploadButton()" for="upload-single-file" class="adf-upload-button-label">
<mat-icon class="adf-upload-button-icon" adf-icon="file_upload" />
@if (staticTitle) {
<span id="upload-single-file-label-static"> {{ staticTitle }}</span>
} @else {
<span id="upload-single-file-label">{{ 'FILE_UPLOAD.BUTTON.UPLOAD_FILE' | translate }}</span>
}
</label>
</ng-container>
}
<!--Multiple Files Upload-->
@if (multipleFiles) {
<input
id="upload-multiple-files"
data-automation-id="upload-multiple-files"
type="file"
name="uploadFiles"
multiple="multiple"
accept="{{acceptedFilesType}}"
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onFilesAdded($event)"/>
<label for="upload-multiple-files" class="adf-upload-button-label">
<mat-icon class="adf-upload-button-icon" adf-icon="file_upload" />
@if (staticTitle) {
<span id="upload-multiple-file-label-static"> {{ staticTitle }}</span>
} @else {
<span id="upload-multiple-file-label">{{ 'FILE_UPLOAD.BUTTON.UPLOAD_FILE' | translate }}</span>
}
</label>
}
</div>
}
<!--Folders Upload-->
@if (uploadFolders) {
<input
id="uploadFolder"
data-automation-id="uploadFolder"
type="file"
name="uploadFiles"
multiple="multiple"
accept="{{acceptedFilesType}}"
webkitdirectory directory
[attr.disabled]="isButtonDisabled()"
[title]="tooltip"
(change)="onDirectoryAdded($event)"/>
<label for="uploadFolder" data-automation-id="uploadFolder" class="adf-upload-button-label">
<mat-icon class="adf-upload-button-icon" adf-icon="file_upload" />
@if (staticTitle) {
<span id="uploadFolder-label-static"> {{ staticTitle }}</span>
} @else {
<span id="uploadFolder-label">{{ 'FILE_UPLOAD.BUTTON.UPLOAD_FOLDER' | translate }}</span>
}
</label>
}
</form>
@@ -24,13 +24,13 @@ import { Node } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { PermissionModel } from '../../document-list/models/permissions.model';
import { UploadBase } from './base-upload/upload-base';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { TranslatePipe } from '@ngx-translate/core';
@Component({
selector: 'adf-upload-button',
imports: [CommonModule, MatButtonModule, TranslatePipe, IconModule],
imports: [MatButtonModule, TranslatePipe, IconModule],
templateUrl: './upload-button.component.html',
styleUrls: ['./upload-button.component.scss'],
encapsulation: ViewEncapsulation.None
@@ -1,86 +0,0 @@
/*!
* @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, ViewChild } from '@angular/core';
import { ToggleIconDirective } from './toggle-icon.directive';
import { ComponentFixture, TestBed } from '@angular/core/testing';
@Component({
selector: 'adf-test-component',
imports: [ToggleIconDirective],
template: ` <button id="testButton" adf-toggle-icon>test</button> `
})
class TestComponent {
@ViewChild(ToggleIconDirective)
directive: ToggleIconDirective;
}
describe('ToggleIconDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestComponent]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should set toggle to true on mouseenter', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new MouseEvent('mouseenter'));
expect(component.directive.isToggled).toBe(true);
});
it('should set toggle to false on mouseleave if element is not focused', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new MouseEvent('mouseleave'));
expect(component.directive.isToggled).toBe(false);
});
it('should set toggle and focus to false on mouseleave when element is focused', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new Event('focus'));
expect(component.directive.isToggled).toBe(true);
button.dispatchEvent(new MouseEvent('mouseleave'));
expect(component.directive.isToggled).toBe(false);
expect(component.directive.isFocused).toBe(false);
});
it('should set toggle and focus to true when element is focused', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new Event('focus'));
expect(component.directive.isToggled).toBe(true);
expect(component.directive.isFocused).toBe(true);
});
it('should set toggle and focus to true when element blur event', () => {
const button: HTMLElement = fixture.nativeElement.querySelector('#testButton');
button.dispatchEvent(new Event('focus'));
button.dispatchEvent(new Event('blur'));
expect(component.directive.isToggled).toBe(false);
expect(component.directive.isFocused).toBe(false);
});
});
@@ -1,62 +0,0 @@
/*!
* @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 { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[adf-toggle-icon]',
exportAs: 'toggleIcon'
})
export class ToggleIconDirective {
private isFocus: boolean = false;
private toggle: boolean = false;
@HostListener('mouseenter') onMouseEnter() {
if (!this.isFocus) {
this.toggle = true;
}
}
@HostListener('mouseleave') onMouseLeave() {
if (!this.isFocus) {
this.toggle = false;
}
if (this.isFocus && this.toggle) {
this.isFocus = false;
this.toggle = false;
}
}
@HostListener('focus') onFocus() {
this.isFocus = true;
this.toggle = true;
}
@HostListener('blur') onBlur() {
this.isFocus = false;
this.toggle = false;
}
get isToggled(): boolean {
return this.toggle;
}
get isFocused(): boolean {
return this.isFocus;
}
}
@@ -1,52 +0,0 @@
/*!
* @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 { NoopTranslateModule } from '@alfresco/adf-core';
import { FileUploadErrorPipe } from './file-upload-error.pipe';
import { TestBed } from '@angular/core/testing';
describe('FileUploadErrorPipe', () => {
let pipe: FileUploadErrorPipe;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopTranslateModule],
providers: [FileUploadErrorPipe]
});
pipe = TestBed.inject(FileUploadErrorPipe);
});
it('should return generic message when error code is null', () => {
expect(pipe.transform(null)).toBe('FILE_UPLOAD.ERRORS.GENERIC');
});
it('should return 500 message', () => {
expect(pipe.transform(500)).toBe('FILE_UPLOAD.ERRORS.500');
});
it('should return 504 message', () => {
expect(pipe.transform(504)).toBe('FILE_UPLOAD.ERRORS.504');
});
it('should return 403 message', () => {
expect(pipe.transform(403)).toBe('FILE_UPLOAD.ERRORS.403');
});
it('should return 404 message', () => {
expect(pipe.transform(404)).toBe('FILE_UPLOAD.ERRORS.404');
});
});
@@ -1,31 +0,0 @@
/*!
* @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 { Pipe, PipeTransform, inject } from '@angular/core';
import { TranslationService } from '@alfresco/adf-core';
@Pipe({
name: 'adfFileUploadError',
pure: true
})
export class FileUploadErrorPipe implements PipeTransform {
private readonly translation = inject(TranslationService);
transform(errorCode: number): string {
return this.translation.instant(`FILE_UPLOAD.ERRORS.${errorCode || 'GENERIC'}`);
}
}
@@ -24,9 +24,6 @@ export * from './components/file-uploading-list-row.component';
export * from './components/upload-files.event';
export * from './directives/file-draggable.directive';
export * from './directives/toggle-icon.directive';
export * from './pipes/file-upload-error.pipe';
export * from './components/base-upload/upload-base';
@@ -22,14 +22,10 @@ import { FileUploadingListComponent } from './components/file-uploading-list.com
import { UploadButtonComponent } from './components/upload-button.component';
import { UploadVersionButtonComponent } from './components/upload-version-button.component';
import { UploadDragAreaComponent } from './components/upload-drag-area.component';
import { FileUploadErrorPipe } from './pipes/file-upload-error.pipe';
import { FileDraggableDirective } from './directives/file-draggable.directive';
import { ToggleIconDirective } from './directives/toggle-icon.directive';
export const CONTENT_UPLOAD_DIRECTIVES = [
FileUploadErrorPipe,
FileDraggableDirective,
ToggleIconDirective,
UploadDragAreaComponent,
UploadButtonComponent,
UploadVersionButtonComponent,
@@ -48,6 +48,7 @@ import { AlfrescoViewerComponent } from './alfresco-viewer.component';
import { RenditionService } from '../../common/services/rendition.service';
import { NodeActionsService } from '../../document-list/services/node-actions.service';
import { provideApiTesting } from '../../testing/providers';
import { MatIconTestingModule } from '@angular/material/icon/testing';
@Component({
selector: 'adf-viewer-container-toolbar',
@@ -179,7 +180,7 @@ describe('AlfrescoViewerComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoopAuthModule, NoopTranslateModule, MatDialogModule, AlfrescoViewerComponent],
imports: [NoopAuthModule, MatIconTestingModule, NoopTranslateModule, MatDialogModule, AlfrescoViewerComponent],
providers: [
provideApiTesting(),
{
@@ -1,30 +0,0 @@
/*!
* @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 class DecimalNumberModel {
minIntegerDigits: number;
minFractionDigits: number;
maxFractionDigits: number;
constructor(obj?: any) {
if (obj) {
this.minIntegerDigits = obj.minIntegerDigits;
this.minFractionDigits = obj.minFractionDigits;
this.maxFractionDigits = obj.maxFractionDigits;
}
}
}
-1
View File
@@ -18,6 +18,5 @@
export * from './comment.model';
export * from './pagination.model';
export * from './request-pagination.model';
export * from './decimal-number.model';
export * from './general-user.model';
export * from './path.model';
@@ -19,7 +19,12 @@ import { DecimalPipe } from '@angular/common';
import { Pipe, PipeTransform, inject } from '@angular/core';
import { AppConfigService } from '../app-config/app-config.service';
import { UserPreferencesService } from '../common/services/user-preferences.service';
import { DecimalNumberModel } from '../models/decimal-number.model';
export interface DecimalNumberModel {
minIntegerDigits?: number;
minFractionDigits?: number;
maxFractionDigits?: number;
}
@Pipe({
name: 'adfDecimalNumber',
@@ -33,6 +33,7 @@ import { ViewerWithCustomToolbarActionsComponent } from './mock/adf-viewer-conta
import { ViewerWithCustomToolbarComponent } from './mock/adf-viewer-container-toolbar.component.mock';
import { ViewerComponent } from './viewer.component';
import { ThumbnailService } from '../../common/services/thumbnail.service';
import { MatIconTestingModule } from '@angular/material/icon/testing';
@Component({
selector: 'adf-dialog-dummy',
@@ -71,6 +72,7 @@ describe('ViewerComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
MatIconTestingModule,
ViewerWithCustomToolbarComponent,
ViewerWithCustomSidebarComponent,
ViewerWithCustomOpenWithComponent,
@@ -106,8 +108,8 @@ describe('ViewerComponent', () => {
describe('Mime Type Test', () => {
it('should mimeType change when blobFile changes', () => {
const mockSimpleChanges: SimpleChanges = {
blobFile: new SimpleChange(null, { type: 'image/png' }, true)
const mockSimpleChanges: SimpleChanges = {
blobFile: new SimpleChange(null, { type: 'image/png' }, true)
};
component.ngOnChanges(mockSimpleChanges);
@@ -117,9 +119,9 @@ describe('ViewerComponent', () => {
it('should set mimeTypeIconUrl when mimeType changes and no nodeMimeType is provided', () => {
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('image/png');
const mockSimpleChanges: SimpleChanges = {
mimeType: new SimpleChange(null, 'image/png', true),
nodeMimeType: undefined
const mockSimpleChanges: SimpleChanges = {
mimeType: new SimpleChange(null, 'image/png', true),
nodeMimeType: undefined
};
component.ngOnChanges(mockSimpleChanges);
@@ -130,9 +132,9 @@ describe('ViewerComponent', () => {
it('should set mimeTypeIconUrl when nodeMimeType changes', () => {
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('application/pdf');
const mockSimpleChanges: SimpleChanges = {
mimeType: new SimpleChange(null, 'image/png', true),
nodeMimeType: new SimpleChange(null, 'application/pdf', true)
const mockSimpleChanges: SimpleChanges = {
mimeType: new SimpleChange(null, 'image/png', true),
nodeMimeType: new SimpleChange(null, 'application/pdf', true)
};
component.ngOnChanges(mockSimpleChanges);
@@ -531,8 +533,8 @@ describe('ViewerComponent', () => {
});
it('should file name be present if is overlay mode ', async () => {
const mockSimpleChanges: SimpleChanges = {
blobFile: new SimpleChange(null, { type: 'image/png' }, true)
const mockSimpleChanges: SimpleChanges = {
blobFile: new SimpleChange(null, { type: 'image/png' }, true)
};
component.ngOnChanges(mockSimpleChanges);
fixture.detectChanges();