diff --git a/e2e/playwright/navigation/src/tests/single-click.spec.ts b/e2e/playwright/navigation/src/tests/single-click.spec.ts
index 6d48acc27c..9fc476a56c 100644
--- a/e2e/playwright/navigation/src/tests/single-click.spec.ts
+++ b/e2e/playwright/navigation/src/tests/single-click.spec.ts
@@ -56,7 +56,7 @@ test.describe('Single click on item name', () => {
});
test.afterAll(async ({ nodesApiAction }) => {
- await nodesApiAction.deleteNodes([deletedFolder1Id, deletedFile1Id], true);
+ await nodesApiAction.deleteNodes([deletedFolder1Id, deletedFile1Id, folder1Id, folderSearchId], true);
});
test('[C284899] Hyperlink does not appear for items in the Trash', async ({ trashPage }) => {
@@ -65,17 +65,11 @@ test.describe('Single click on item name', () => {
expect(await trashPage.dataTable.getCellLinkByName(deletedFolder1).isVisible(), 'Link on name is present').toBe(false);
});
- test.describe('on Personal Files', () => {
- test.afterAll(async ({ nodesApiAction }) => {
- await nodesApiAction.deleteNodes([folder1Id, folderSearchId], true);
- });
-
- test('[C280034] Navigate inside the folder when clicking the hyperlink on Personal Files', async ({ personalFiles }) => {
- await personalFiles.navigate();
- await personalFiles.dataTable.getCellLinkByName(folder1).click();
- await personalFiles.dataTable.spinnerWaitForReload();
- expect(await personalFiles.breadcrumb.currentItem.innerText()).toBe(folder1);
- });
+ test('[C280034] Navigate inside the folder when clicking the hyperlink on Personal Files', async ({ personalFiles }) => {
+ await personalFiles.navigate();
+ await personalFiles.dataTable.getCellLinkByName(folder1).click();
+ await personalFiles.dataTable.spinnerWaitForReload();
+ expect(await personalFiles.breadcrumb.currentItem.innerText()).toBe(folder1);
});
test('[C284902] Navigate inside the library when clicking the hyperlink on File Libraries', async ({ myLibrariesPage }) => {
diff --git a/e2e/playwright/viewer/exclude.tests.json b/e2e/playwright/viewer/exclude.tests.json
index 0967ef424b..14b8e54beb 100644
--- a/e2e/playwright/viewer/exclude.tests.json
+++ b/e2e/playwright/viewer/exclude.tests.json
@@ -1 +1,3 @@
-{}
+{
+ "C286379": "https://alfresco.atlassian.net/browse/ACS-5601"
+}
diff --git a/e2e/playwright/viewer/src/tests/viewer-action.spec.ts b/e2e/playwright/viewer/src/tests/viewer-action.spec.ts
index 5a778a24ab..7f45bb51b5 100644
--- a/e2e/playwright/viewer/src/tests/viewer-action.spec.ts
+++ b/e2e/playwright/viewer/src/tests/viewer-action.spec.ts
@@ -102,7 +102,6 @@ test.describe('viewer action file', () => {
const download = await downloadPromise;
expect(download.suggestedFilename(), 'File should found in download location').toBe(fileForEditOffline);
expect(await personalFiles.viewer.isViewerOpened(), 'Viewer is closed after pressing Full screen').toBe(true);
- await personalFiles.reload({ waitUntil: 'domcontentloaded' });
await personalFiles.acaHeader.clickViewerMoreActions();
expect(await personalFiles.matMenu.isMenuItemVisible('Cancel Editing'), 'Cancel Editing menu should be visible').toBe(true);
});
@@ -112,7 +111,6 @@ test.describe('viewer action file', () => {
await personalFiles.viewer.waitForViewerToOpen();
await personalFiles.acaHeader.clickViewerMoreActions();
await personalFiles.matMenu.clickMenuItem('Cancel Editing');
- await personalFiles.reload({ waitUntil: 'domcontentloaded' });
await personalFiles.acaHeader.clickViewerMoreActions();
expect(await personalFiles.matMenu.isMenuItemVisible('Edit Offline'), 'Edit offline menu should be visible').toBe(true);
});
diff --git a/extension.schema.json b/extension.schema.json
index 36f68a1481..f946bfb8fd 100644
--- a/extension.schema.json
+++ b/extension.schema.json
@@ -640,6 +640,34 @@
"$ref": "node_modules/@alfresco/adf-core/app.config.schema.json#/definitions/search-configuration"
}
]
+ },
+ "badges": {
+ "description": "List of badges to display in the name column",
+ "type": "array",
+ "items": { "$ref": "#/definitions/badge" },
+ "minItems": 1
+ },
+ "badge": {
+ "type": "object",
+ "required": ["id", "icon", "tooltip"],
+ "properties": {
+ "id": {
+ "description": "Unique identifier. Must be in the format '[namespace]:[name]'.",
+ "type": "string"
+ },
+ "icon": {
+ "description": "Badge icon to display.",
+ "type": "string"
+ },
+ "tooltip": {
+ "description": "Badge tooltip to display on hover.",
+ "type": "string"
+ },
+ "component": {
+ "description": "Custom component id to display",
+ "type": "string"
+ }
+ }
}
},
diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html
index cdb2cc3fe3..7981cb8599 100644
--- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html
+++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html
@@ -1,25 +1,30 @@
-
-
- {{ displayText$ | async }}
-
+
+
+
+ {{ displayText$ | async }}
+
-
-
-
+
+
+
+
+
diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss
index 2adc67c0fb..6bc28dfef6 100644
--- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss
+++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss
@@ -1,6 +1,16 @@
.aca-custom-name-column {
- display: block;
+ display: flex;
align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ .aca-name-column-badges {
+ display: flex;
+
+ .adf-datatable-cell-badge {
+ color: var(--theme-contrast-gray);
+ }
+ }
.aca-name-column-container {
aca-locked-by {
@@ -17,3 +27,11 @@
}
}
}
+
+.adf-datatable-content-cell.adf-name-column.aca-custom-name-column {
+ position: unset;
+}
+
+.adf-datatable-list .adf-datatable-link:hover .aca-name-column-badges {
+ color: var(--adf-theme-foreground-text-color);
+}
diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts
index 5daf9ddacf..fde887f474 100644
--- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts
+++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts
@@ -28,19 +28,44 @@ import { StoreModule } from '@ngrx/store';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';
+import { AppExtensionService } from '@alfresco/aca-shared';
+import { of } from 'rxjs';
+import { ContentActionType } from '@alfresco/adf-extensions';
+import { By } from '@angular/platform-browser';
describe('CustomNameColumnComponent', () => {
let fixture: ComponentFixture
;
let component: CustomNameColumnComponent;
+ let appExtensionService: AppExtensionService;
beforeEach(() => {
TestBed.configureTestingModule({
- imports: [HttpClientModule, TranslateModule.forRoot(), CustomNameColumnComponent, StoreModule.forRoot({ app: () => {} }, { initialState: {} })],
+ imports: [
+ HttpClientModule,
+ TranslateModule.forRoot(),
+ CustomNameColumnComponent,
+ StoreModule.forRoot(
+ { app: (state) => state },
+ {
+ initialState: {
+ app: {
+ selection: {
+ nodes: [],
+ libraries: [],
+ isEmpty: true,
+ count: 0
+ }
+ }
+ }
+ }
+ )
+ ],
providers: [Actions]
});
fixture = TestBed.createComponent(CustomNameColumnComponent);
component = fixture.componentInstance;
+ appExtensionService = TestBed.inject(AppExtensionService);
});
it('should not render lock element if file is not locked', () => {
@@ -114,4 +139,55 @@ describe('CustomNameColumnComponent', () => {
component.onLinkClick(event);
expect(event.stopPropagation).toHaveBeenCalled();
});
+
+ describe('Name column badges', () => {
+ beforeEach(() => {
+ component.context = {
+ row: {
+ node: {
+ entry: {
+ isFile: true,
+ id: 'nodeId'
+ }
+ },
+ getValue: (key: string) => key
+ }
+ };
+ });
+
+ it('should get badges when component initializes', () => {
+ spyOn(appExtensionService, 'getBadges').and.returnValue(
+ of([{ id: 'test', type: ContentActionType.custom, icon: 'warning', tooltip: 'test tooltip' }])
+ );
+ component.ngOnInit();
+ fixture.detectChanges();
+ const badges = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-badge')).map((badge) => badge.nativeElement);
+ expect(appExtensionService.getBadges).toHaveBeenCalled();
+ expect(badges.length).toBe(1);
+ expect(badges[0].innerText).toBe('warning');
+ expect(badges[0].attributes['title'].value).toBe('test tooltip');
+ });
+
+ it('should call provided handler on click', () => {
+ spyOn(appExtensionService, 'runActionById');
+ spyOn(appExtensionService, 'getBadges').and.returnValue(
+ of([{ id: 'test', type: ContentActionType.custom, icon: 'warning', tooltip: 'test tooltip', actions: { click: 'test' } }])
+ );
+ component.ngOnInit();
+ fixture.detectChanges();
+ const badges = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-badge')).map((badge) => badge.nativeElement);
+ badges[0].click();
+ expect(appExtensionService.runActionById).toHaveBeenCalledWith('test', component.context.row.node);
+ });
+
+ it('should render dynamic component when badge has one provided', () => {
+ spyOn(appExtensionService, 'getBadges').and.returnValue(
+ of([{ id: 'test', type: ContentActionType.custom, icon: 'warning', tooltip: 'test tooltip', component: 'test-id' }])
+ );
+ component.ngOnInit();
+ fixture.detectChanges();
+ const dynamicComponent = fixture.debugElement.query(By.css('adf-dynamic-component')).nativeElement;
+ expect(dynamicComponent).toBeDefined();
+ });
+ });
});
diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts
index e3a1e87a7d..dfeefcc7bb 100644
--- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts
+++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts
@@ -28,13 +28,15 @@ import { Actions, ofType } from '@ngrx/effects';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { NodeActionTypes } from '@alfresco/aca-shared/store';
-import { LockedByComponent, isLocked } from '@alfresco/aca-shared';
+import { LockedByComponent, isLocked, AppExtensionService, Badge } from '@alfresco/aca-shared';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
+import { IconModule } from '@alfresco/adf-core';
+import { ExtensionsModule } from '@alfresco/adf-extensions';
@Component({
standalone: true,
- imports: [CommonModule, TranslateModule, LockedByComponent, ContentPipeModule],
+ imports: [CommonModule, TranslateModule, LockedByComponent, ContentPipeModule, IconModule, ExtensionsModule],
selector: 'aca-custom-name-column',
templateUrl: './name-column.component.html',
styleUrls: ['./name-column.component.scss'],
@@ -48,8 +50,15 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On
isFile: boolean;
isFileWriteLocked: boolean;
+ badges: Badge[];
- constructor(element: ElementRef, private cd: ChangeDetectorRef, private actions$: Actions, private nodesService: NodesApiService) {
+ constructor(
+ element: ElementRef,
+ private cd: ChangeDetectorRef,
+ private actions$: Actions,
+ private nodesService: NodesApiService,
+ private appExtensionService: AppExtensionService
+ ) {
super(element, nodesService);
}
@@ -86,6 +95,13 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On
this.isFileWriteLocked = isLocked(this.node);
this.cd.detectChanges();
});
+
+ this.appExtensionService
+ .getBadges(this.node)
+ .pipe(takeUntil(this.onDestroy$$))
+ .subscribe((badges) => {
+ this.badges = badges;
+ });
}
onLinkClick(event: Event) {
@@ -99,4 +115,8 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On
this.onDestroy$$.next(true);
this.onDestroy$$.complete();
}
+
+ onBadgeClick(badge: Badge) {
+ this.appExtensionService.runActionById(badge.actions?.click, this.node);
+ }
}
diff --git a/projects/aca-content/src/lib/ui/theme.scss b/projects/aca-content/src/lib/ui/theme.scss
index 8afd57585c..08d30fa30c 100644
--- a/projects/aca-content/src/lib/ui/theme.scss
+++ b/projects/aca-content/src/lib/ui/theme.scss
@@ -4,14 +4,12 @@
@import 'variables/variables';
@include custom-theme($custom-theme);
-$contrast-gray: #646569;
-
.mat-toolbar {
color: var(--theme-text-color, rgba(0, 0, 0, 0.54));
}
.adf-name-location-cell-location.adf-datatable-cell-value {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
}
.mat-tab-list {
@@ -29,14 +27,14 @@ $contrast-gray: #646569;
.mat-checkbox-label,
mat-toolbar.mat-toolbar.mat-toolbar-multiple-row,
mat-toolbar.mat-toolbar.mat-toolbar-single-row {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
opacity: 1;
}
.adf-upload-dialog {
&__header,
&__content {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
}
}
@@ -44,7 +42,7 @@ mat-toolbar.mat-toolbar.mat-toolbar-single-row {
.adf-version-list-item {
&-comment,
&-date {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
opacity: 1;
}
}
@@ -52,19 +50,19 @@ mat-toolbar.mat-toolbar.mat-toolbar-single-row {
.mat-chip.mat-standard-chip {
background-color: #efefef;
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
}
.adf-property-field {
.adf-textitem-edit-icon.mat-icon {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
}
}
.adf-property-field.adf-card-textitem-field:hover .adf-property-clear-value {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
}
.adf-empty-content__icon {
- color: $contrast-gray;
+ color: var(--theme-contrast-gray);
}
diff --git a/projects/aca-content/src/lib/ui/variables/variables.scss b/projects/aca-content/src/lib/ui/variables/variables.scss
index c68a38a8fc..8a11c08041 100644
--- a/projects/aca-content/src/lib/ui/variables/variables.scss
+++ b/projects/aca-content/src/lib/ui/variables/variables.scss
@@ -35,6 +35,7 @@ $action-button-text-color: rgba(33, 35, 40, 0.7);
$page-layout-header-background-color: #fff;
$search-chip-icon-color: #757575;
$disabled-chip-background-color: #f5f5f5;
+$contrast-gray: #646569;
// CSS Variables
$defaults: (
@@ -74,7 +75,8 @@ $defaults: (
--theme-action-button-text-color: $action-button-text-color,
--theme-page-layout-header-background-color: $page-layout-header-background-color,
--theme-search-chip-icon-color: $search-chip-icon-color,
- --theme-disabled-chip-background-color: $disabled-chip-background-color
+ --theme-disabled-chip-background-color: $disabled-chip-background-color,
+ --theme-contrast-gray: $contrast-gray
);
// propagates SCSS variables into the CSS variables scope
diff --git a/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts b/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts
index 51139cc0a9..2d31902701 100644
--- a/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts
+++ b/projects/aca-playwright-shared/src/page-objects/components/dataTable/data-table.component.ts
@@ -69,7 +69,7 @@ export class DataTableComponent extends BaseComponent {
*
* @returns reference to cell element which contains link.
*/
- getCellLinkByName = (name: string): Locator => this.getChild('.adf-datatable-cell-value[role="link"]', { hasText: name });
+ getCellLinkByName = (name: string): Locator => this.getChild('.adf-cell-value span', { hasText: name });
/**
* Method used in cases where we want to localize the element by [aria-label]
diff --git a/projects/aca-shared/src/lib/models/types.ts b/projects/aca-shared/src/lib/models/types.ts
index 76e6ab5d1d..ae8ac84bf3 100644
--- a/projects/aca-shared/src/lib/models/types.ts
+++ b/projects/aca-shared/src/lib/models/types.ts
@@ -22,6 +22,7 @@
* from Hyland Software. If not, see .
*/
+import { ContentActionRef } from '@alfresco/adf-extensions';
import { Route } from '@angular/router';
export interface SettingsGroupRef {
@@ -45,3 +46,7 @@ export interface SettingsParameterRef {
export interface ExtensionRoute extends Route {
parentRoute?: string;
}
+
+export interface Badge extends ContentActionRef {
+ tooltip: string;
+}
diff --git a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts
index e934155d32..966b8865ba 100644
--- a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts
+++ b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts
@@ -43,6 +43,7 @@ import { provideMockStore } from '@ngrx/store/testing';
import { hasQuickShareEnabled } from '@alfresco/aca-shared/rules';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
+import { NodeEntry } from '@alfresco/js-api';
describe('AppExtensionService', () => {
let service: AppExtensionService;
@@ -1677,4 +1678,62 @@ describe('AppExtensionService', () => {
done();
});
});
+
+ it('should get badges from config', (done) => {
+ extensions.setEvaluators({
+ 'action.enabled': () => true
+ });
+
+ applyConfig({
+ $id: 'test',
+ $name: 'test',
+ $version: '1.0.0',
+ $license: 'MIT',
+ $vendor: 'Good company',
+ $runtime: '1.5.0',
+ features: {
+ badges: [
+ {
+ id: 'action1-id',
+ icon: 'warning',
+ tooltip: 'test tooltip',
+ type: 'custom',
+ rules: {
+ visible: 'action.enabled'
+ }
+ },
+ {
+ id: 'action2-id',
+ icon: 'settings',
+ tooltip: 'test tooltip2',
+ type: 'custom',
+ rules: {
+ visible: 'action.enabled'
+ }
+ }
+ ]
+ }
+ });
+
+ const node: NodeEntry = {
+ entry: {
+ id: 'testId',
+ name: 'testName',
+ nodeType: 'test',
+ isFile: true,
+ isFolder: false,
+ modifiedAt: undefined,
+ createdAt: undefined,
+ modifiedByUser: undefined,
+ createdByUser: undefined
+ }
+ };
+
+ service.getBadges(node).subscribe((badges) => {
+ expect(badges.length).toBe(2);
+ expect(badges[0].id).toEqual('action1-id');
+ expect(badges[1].id).toEqual('action2-id');
+ done();
+ });
+ });
});
diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts
index bb32e031ab..393c0339e7 100644
--- a/projects/aca-shared/src/lib/services/app.extension.service.ts
+++ b/projects/aca-shared/src/lib/services/app.extension.service.ts
@@ -53,10 +53,9 @@ import { AppConfigService, AuthenticationService, LogService } from '@alfresco/a
import { BehaviorSubject, Observable } from 'rxjs';
import { RepositoryInfo, NodeEntry } from '@alfresco/js-api';
import { ViewerRules } from '../models/viewer.rules';
-import { SettingsGroupRef } from '../models/types';
+import { Badge, SettingsGroupRef } from '../models/types';
import { NodePermissionService } from '../services/node-permission.service';
import { filter, map } from 'rxjs/operators';
-import { ModalConfiguration } from '../models/modal-configuration';
@Injectable({
providedIn: 'root'
@@ -80,6 +79,7 @@ export class AppExtensionService implements RuleContext {
private _createActions = new BehaviorSubject>([]);
private _mainActions = new BehaviorSubject(null);
private _sidebarActions = new BehaviorSubject>([]);
+ private _badges = new BehaviorSubject>([]);
private _filesDocumentListPreset = new BehaviorSubject>([]);
documentListPresets: {
@@ -158,6 +158,7 @@ export class AppExtensionService implements RuleContext {
this._openWithActions.next(this.loader.getContentActions(config, 'features.viewer.openWith'));
this._createActions.next(this.loader.getElements(config, 'features.create'));
this._mainActions.next(this.loader.getFeatures(config).mainAction);
+ this._badges.next(this.loader.getElements(config, 'features.badges'));
this._filesDocumentListPreset.next(this.getDocumentListPreset(config, 'files'));
this.navbar = this.loadNavBar(config);
@@ -370,6 +371,10 @@ export class AppExtensionService implements RuleContext {
);
}
+ getBadges(node: NodeEntry): Observable> {
+ return this._badges.pipe(map((badges) => badges.filter((badge) => this.evaluateRule(badge.rules.visible, node))));
+ }
+
private buildMenu(actionRef: ContentActionRef): ContentActionRef {
if (actionRef.type === ContentActionType.menu && actionRef.children && actionRef.children.length > 0) {
const children = actionRef.children.filter((action) => this.filterVisible(action)).map((action) => this.buildMenu(action));
@@ -492,7 +497,7 @@ export class AppExtensionService implements RuleContext {
return false;
}
- runActionById(id: string, additionalPayload?: ModalConfiguration) {
+ runActionById(id: string, additionalPayload?: any) {
const action = this.extensions.getActionById(id);
if (action) {
const { type, payload } = action;