add tabulatable loader and related configuration

This commit is contained in:
FrancescoMolinaro
2024-01-04 17:15:25 +01:00
parent 9386536ca9
commit de8c0c9528
16 changed files with 605 additions and 19 deletions

View File

@@ -12,7 +12,7 @@
</div>
</ng-template>
</li>
<li [ngbNavItem]="'logs'">
<li [ngbNavItem]="'logs'" (click)="activateTableMode()">
<a ngbNavLink>{{'admin-notify-dashboard.logs' | translate}}</a>
<ng-template ngbNavContent>
<div id="logs">

View File

@@ -14,6 +14,8 @@ import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.compo
import { AdminNotifySearchConfigurationService } from './config/admin-notify-search-configuration.service';
import { AdminNotifySearchFilterService } from './config/admin-notify-filter-service';
import { AdminNotifySearchFilterConfig } from './config/admin-notify-search-filter-config';
import { ViewMode } from "../../core/shared/view-mode.model";
import { Router } from "@angular/router";
export const FILTER_SEARCH: InjectionToken<SearchFilterService> = new InjectionToken<SearchFilterService>('searchFilterService');
@@ -45,7 +47,8 @@ export class AdminNotifyDashboardComponent implements OnInit{
id: 'single-result-options',
pageSize: 1
});
constructor(private searchService: SearchService) {}
constructor(private searchService: SearchService,
private router: Router) {}
ngOnInit() {
const mertricsRowsConfigurations = this.metricsConfig
@@ -99,4 +102,28 @@ export class AdminNotifyDashboardComponent implements OnInit{
};
});
}
/**
* Activate Table view mode for search result rendering
*/
activateTableMode() {
this.searchService.setViewMode(ViewMode.Table, this.getSearchLinkParts());
}
/**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/
public getSearchLink(): string {
return this.searchService.getSearchLink();
}
/**
* @returns {string[]} The base path to the search page, or the current page when inPlaceSearch is true, split in separate pieces
*/
public getSearchLinkParts(): string[] {
if (this.searchService) {
return [];
}
return this.getSearchLink().split('/');
}
}

View File

@@ -1,7 +1,34 @@
<div class="d-flex bg-light w-100 align-items-center">
<div class="p-2 text-truncate">{{indexableObject.queueTimeout ?? 'n/a'}}</div>
<div class="p-2 text-truncate">{{indexableObject.source ?? 'n/a'}}</div>
<div class="p-2 text-truncate">{{indexableObject.target ?? 'n/a'}}</div>
<div class="p-2 text-truncate">{{indexableObject.coarNotifyType ?? 'n/a'}}</div>
<div class="p-2 text-truncate">{{indexableObject.queueStatusLabel ?? 'n/a'}}</div>
<div class="table-responsive">
<table id="formats" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Source</th>
<th scope="col">Target</th>
<th scope="col">Type</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let object of indexableObjects">
<td>
<div>{{object.queueTimeout}}</div>
</td>
<td>
<div>{{object.source}}</div>
</td>
<td>
<div>{{object.target}}</div>
</td>
<td>
<div>{{object.coarNotifyType}}</div>
</td>
<td>
<div>{{object.queueStatusLabel}}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -1,24 +1,26 @@
import { Component, OnInit } from '@angular/core';
import {
listableObjectComponent
} from '../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { AdminNotifySearchResult } from '../models/admin-notify-message-search-result.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
import { Context } from '../../../core/shared/context.model';
import {
SearchResultListElementComponent
} from '../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
import {
tabulatableObjectsComponent
} from "../../../shared/object-collection/shared/tabulatable-objects/tabulatable-objects.decorator";
import {
TabulatableResultListElementsComponent
} from "../../../shared/object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component";
import { PaginatedList } from "../../../core/data/paginated-list.model";
@listableObjectComponent(AdminNotifySearchResult, ViewMode.ListElement, Context.CoarNotify)
@tabulatableObjectsComponent(AdminNotifySearchResult, ViewMode.Table, Context.CoarNotify)
@Component({
selector: 'ds-admin-notify-search-result',
templateUrl: './admin-notify-search-result.component.html',
styleUrls: ['./admin-notify-search-result.component.scss']
})
export class AdminNotifySearchResultComponent extends SearchResultListElementComponent<AdminNotifySearchResult, AdminNotifyMessage> implements OnInit{
indexableObject: AdminNotifyMessage;
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifyMessage>, AdminNotifyMessage> implements OnInit{
public indexableObjects: AdminNotifyMessage[];
ngOnInit() {
this.indexableObject = this.object.indexableObject;
this.indexableObjects = this.objects.page.map(object => object.indexableObject);
console.log(this.objects.page)
}
}

View File

@@ -56,6 +56,12 @@ export class AdminNotifyMessage extends DSpaceObject {
@autoserialize
queueStatus: number;
/**
* The status of the queue
*/
@autoserialize
indexableObject: AdminNotifyMessage;
@deserialize
_links: {
self: {

View File

@@ -7,4 +7,5 @@ export enum ViewMode {
GridElement = 'grid',
DetailedListElement = 'detailed',
StandalonePage = 'standalone',
Table = 'table',
}

View File

@@ -58,3 +58,20 @@
</ds-object-detail>
<ds-object-table
[config]="config"
[sortConfig]="sortConfig"
[objects]="objects"
[hideGear]="hideGear"
[linkType]="linkType"
[context]="context"
[hidePaginationDetail]="hidePaginationDetail"
[showPaginator]="showPaginator"
[showThumbnails]="showThumbnails"
(paginationChange)="onPaginationChange($event)"
(pageChange)="onPageChange($event)"
(pageSizeChange)="onPageSizeChange($event)"
(sortDirectionChange)="onSortDirectionChange($event)"
(sortFieldChange)="onSortFieldChange($event)"
*ngIf="(currentMode$ | async) === viewModeEnum.Table">
</ds-object-table>

View File

@@ -154,7 +154,6 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
}
private instantiateComponent(object: ListableObject, changes?: SimpleChanges): void {
const component = this.getComponent(object.getRenderTypes(), this.viewMode, this.context);
const viewContainerRef = this.listableObjectDirective.viewContainerRef;

View File

@@ -0,0 +1,194 @@
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { Context } from '../../../../core/shared/context.model';
import { hasNoValue, hasValue, isNotEmpty } from '../../../empty.util';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObject } from '../listable-object.model';
import { environment } from '../../../../../environments/environment';
import { ThemeConfig } from '../../../../../config/theme.config';
import { InjectionToken } from '@angular/core';
export const DEFAULT_VIEW_MODE = ViewMode.ListElement;
export const DEFAULT_CONTEXT = Context.Any;
export const DEFAULT_THEME = '*';
/**
* A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
*
* "level" represents the index of the first default value that was used to find the match with:
* ViewMode being index 0, Context index 1 and theme index 2. Examples:
* - If a default value was used for context, but not view-mode and theme, the "level" will be 1
* - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
* - If no default value was used for any of the fields, the "level" will be 3
*
* "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
* - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
* - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
* - If a default value was used for all fields, the "relevancy" will be 0
* - If no default value was used for any of the fields, the "relevancy" will be 3
*
* To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
* If any of the two is higher than the other, that match is most relevant. Examples:
* - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
* - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
* - { level: 1, relevancy: 1 } is more relevant than null
*/
class MatchRelevancy {
constructor(public match: any,
public level: number,
public relevancy: number) {
}
isMoreRelevantThan(otherMatch: MatchRelevancy): boolean {
if (hasNoValue(otherMatch)) {
return true;
}
if (otherMatch.level > this.level) {
return false;
}
if (otherMatch.level === this.level && otherMatch.relevancy > this.relevancy) {
return false;
}
return true;
}
isLessRelevantThan(otherMatch: MatchRelevancy): boolean {
return !this.isMoreRelevantThan(otherMatch);
}
}
/**
* Factory to allow us to inject getThemeConfigFor so we can mock it in tests
*/
export const GET_THEME_CONFIG_FOR_FACTORY = new InjectionToken<(str) => ThemeConfig>('getThemeConfigFor', {
providedIn: 'root',
factory: () => getThemeConfigFor
});
const map = new Map();
/**
* Decorator used for rendering a listable object
* @param objectType The object type or entity type the component represents
* @param viewMode The view mode the component represents
* @param context The optional context the component represents
* @param theme The optional theme for the component
*/
export function tabulatableObjectsComponent(objectsType: string | GenericConstructor<ListableObject>, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
return function decorator(component: any) {
if (hasNoValue(objectsType)) {
return;
}
if (hasNoValue(map.get(objectsType))) {
map.set(objectsType, new Map());
}
if (hasNoValue(map.get(objectsType).get(viewMode))) {
map.get(objectsType).set(viewMode, new Map());
}
if (hasNoValue(map.get(objectsType).get(viewMode).get(context))) {
map.get(objectsType).get(viewMode).set(context, new Map());
}
map.get(objectsType).get(viewMode).get(context).set(theme, component);
};
}
/**
* Getter to retrieve the matching listable object component
*
* Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch()
* The most relevant match between types is kept and eventually returned
*
* @param types The types of which one should match the listable component
* @param viewMode The view mode that should match the components
* @param context The context that should match the components
* @param theme The theme that should match the components
*/
export function getTabulatableObjectsComponent(types: (string | GenericConstructor<ListableObject>)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) {
let currentBestMatch: MatchRelevancy = null;
for (const type of types) {
const typeMap = map.get(type);
if (hasValue(typeMap)) {
const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]);
if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) {
currentBestMatch = match;
}
}
}
return hasValue(currentBestMatch) ? currentBestMatch.match : null;
}
/**
* Find an object within a nested map, matching the provided keys as best as possible, falling back on defaults wherever
* needed.
*
* Starting off with a Map, it loops over the provided keys, going deeper into the map until it finds a value
* If at some point, no value is found, it'll attempt to use the default value for that index instead
* If the default value exists, the index is stored in the "level"
* If no default value exists, 1 is added to "relevancy"
* See {@link MatchRelevancy} what these represent
*
* @param typeMap a multi-dimensional map
* @param keys the keys of the multi-dimensional map to loop over. Each key represents a level within the map
* @param defaults the default values to use for each level, in case no value is found for the key at that index
* @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
*/
function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
let currentMap = typeMap;
let level = -1;
let relevancy = 0;
for (let i = 0; i < keys.length; i++) {
// If we're currently checking the theme, resolve it first to take extended themes into account
let currentMatch = defaults[i] === DEFAULT_THEME ? resolveTheme(currentMap, keys[i]) : currentMap.get(keys[i]);
if (hasNoValue(currentMatch)) {
currentMatch = currentMap.get(defaults[i]);
if (level === -1) {
level = i;
}
} else {
relevancy++;
}
if (hasValue(currentMatch)) {
if (currentMatch instanceof Map) {
currentMap = currentMatch as Map<any, any>;
} else {
return new MatchRelevancy(currentMatch, level > -1 ? level : i + 1, relevancy);
}
} else {
return null;
}
}
return null;
}
/**
* Searches for a ThemeConfig by its name;
*/
export const getThemeConfigFor = (themeName: string): ThemeConfig => {
return environment.themes.find(theme => theme.name === themeName);
};
/**
* Find a match in the given map for the given theme name, taking theme extension into account
*
* @param contextMap A map of theme names to components
* @param themeName The name of the theme to check
* @param checkedThemeNames The list of theme names that are already checked
*/
export const resolveTheme = (contextMap: Map<any, any>, themeName: string, checkedThemeNames: string[] = []): any => {
const match = contextMap.get(themeName);
if (hasValue(match)) {
return match;
} else {
const cfg = getThemeConfigFor(themeName);
if (hasValue(cfg) && isNotEmpty(cfg.extends)) {
const nextTheme = cfg.extends;
const nextCheckedThemeNames = [...checkedThemeNames, themeName];
if (checkedThemeNames.includes(nextTheme)) {
throw new Error('Theme extension cycle detected: ' + [...nextCheckedThemeNames, nextTheme].join(' -> '));
} else {
return resolveTheme(contextMap, nextTheme, nextCheckedThemeNames);
}
}
}
};

View File

@@ -0,0 +1,11 @@
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[dsTabulatableObjects]',
})
/**
* Directive used as a hook to know where to inject the dynamic listable object component
*/
export class TabulatableObjectsDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

View File

@@ -0,0 +1,22 @@
import { Component, Inject, OnInit } from '@angular/core';
import {
AbstractTabulatableElementComponent
} from "../../../object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component";
import { DSpaceObject } from "../../../../core/shared/dspace-object.model";
import { TruncatableService } from "../../../truncatable/truncatable.service";
import { DSONameService } from "../../../../core/breadcrumbs/dso-name.service";
import { APP_CONFIG, AppConfig } from "../../../../../config/app-config.interface";
import { PaginatedList } from "../../../../core/data/paginated-list.model";
@Component({
selector: 'ds-search-result-list-element',
template: ``
})
export class TabulatableResultListElementsComponent<T extends PaginatedList<K>, K extends DSpaceObject> extends AbstractTabulatableElementComponent<T> {
public constructor(protected truncatableService: TruncatableService,
public dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig?: AppConfig) {
super(dsoNameService);
}
}

View File

@@ -0,0 +1,32 @@
<ds-pagination
[paginationOptions]="config"
[pageInfoState]="objects?.payload"
[collectionSize]="objects?.payload?.totalElements"
[sortOptions]="sortConfig"
[hideGear]="hideGear"
[objects]="objects"
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
[hidePaginationDetail]="hidePaginationDetail"
[showPaginator]="showPaginator"
(pageChange)="onPageChange($event)"
(pageSizeChange)="onPageSizeChange($event)"
(sortDirectionChange)="onSortDirectionChange($event)"
(sortFieldChange)="onSortFieldChange($event)"
(paginationChange)="onPaginationChange($event)"
(prev)="goPrev()"
(next)="goNext()"
>
<div class="row" *ngIf="objects?.hasSucceeded">
<div @fadeIn>
<ds-tabulatable-objects-loader [objects]="objects.payload"
[context]="context"
[showThumbnails]="showThumbnails"
[linkType]="linkType">
</ds-tabulatable-objects-loader>
</div>
</div>
<ds-error *ngIf="objects.hasFailed" message="{{'error.objects' | translate}}"></ds-error>
<ds-themed-loading *ngIf="objects.isLoading" message="{{'loading.objects' | translate}}"></ds-themed-loading>
</ds-pagination>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ObjectTableComponent } from './object-table.component';
describe('ObjectTableComponent', () => {
let component: ObjectTableComponent;
let fixture: ComponentFixture<ObjectTableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ObjectTableComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ObjectTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,209 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { ViewMode } from "../../core/shared/view-mode.model";
import { PaginationComponentOptions } from "../pagination/pagination-component-options.model";
import { SortDirection, SortOptions } from "../../core/cache/models/sort-options.model";
import { CollectionElementLinkType } from "../object-collection/collection-element-link.type";
import { Context } from "../../core/shared/context.model";
import { BehaviorSubject} from "rxjs";
import { RemoteData } from "../../core/data/remote-data";
import { PaginatedList } from "../../core/data/paginated-list.model";
import { ListableObject } from "../object-collection/shared/listable-object.model";
import { fadeIn } from "../animations/fade";
@Component({
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated,
selector: 'ds-object-table',
templateUrl: './object-table.component.html',
styleUrls: ['./object-table.component.scss'],
animations: [fadeIn]
})
export class ObjectTableComponent {
/**
* The view mode of this component
*/
viewMode = ViewMode.Table;
/**
* The current pagination configuration
*/
@Input() config: PaginationComponentOptions;
/**
* The current sort configuration
*/
@Input() sortConfig: SortOptions;
/**
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
*/
@Input() showPaginator = true;
/**
* Whether to show the thumbnail preview
*/
@Input() showThumbnails;
/**
* The whether or not the gear is hidden
*/
@Input() hideGear = false;
/**
* Whether or not the pager is visible when there is only a single page of results
*/
@Input() hidePagerWhenSinglePage = true;
/**
* The link type of the listable elements
*/
@Input() linkType: CollectionElementLinkType;
/**
* The context of the listable elements
*/
@Input() context: Context;
/**
* Option for hiding the pagination detail
*/
@Input() hidePaginationDetail = false;
/**
* Behavior subject to output the current listable objects
*/
private _objects$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>;
/**
* Setter to make sure the observable is turned into an observable
* @param objects The new objects to output
*/
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
this._objects$.next(objects);
}
/**
* Getter to return the current objects
*/
get objects() {
return this._objects$.getValue();
}
/**
* An event fired when the page is changed.
* Event's payload equals to the newly selected page.
*/
@Output() change: EventEmitter<{
pagination: PaginationComponentOptions,
sort: SortOptions
}> = new EventEmitter<{
pagination: PaginationComponentOptions,
sort: SortOptions
}>();
/**
* An event fired when the page is changed.
* Event's payload equals to the newly selected page.
*/
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
/**
* An event fired when the page wsize is changed.
* Event's payload equals to the newly selected page size.
*/
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
/**
* An event fired when the sort direction is changed.
* Event's payload equals to the newly selected sort direction.
*/
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
/**
* An event fired when on of the pagination parameters changes
*/
@Output() paginationChange: EventEmitter<any> = new EventEmitter<any>();
/**
* An event fired when the sort field is changed.
* Event's payload equals to the newly selected sort field.
*/
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
/**
* If showPaginator is set to true, emit when the previous button is clicked
*/
@Output() prev = new EventEmitter<boolean>();
/**
* If showPaginator is set to true, emit when the next button is clicked
*/
@Output() next = new EventEmitter<boolean>();
data: any = {};
constructor() {
this._objects$ = new BehaviorSubject(undefined);
}
/**
* Initialize the instance variables
*/
ngOnInit(): void {
console.log('table rendered')
}
/**
* Emits the current page when it changes
* @param event The new page
*/
onPageChange(event) {
this.pageChange.emit(event);
}
/**
* Emits the current page size when it changes
* @param event The new page size
*/
onPageSizeChange(event) {
this.pageSizeChange.emit(event);
}
/**
* Emits the current sort direction when it changes
* @param event The new sort direction
*/
onSortDirectionChange(event) {
this.sortDirectionChange.emit(event);
}
/**
* Emits the current sort field when it changes
* @param event The new sort field
*/
onSortFieldChange(event) {
this.sortFieldChange.emit(event);
}
/**
* Emits the current pagination when it changes
* @param event The new pagination
*/
onPaginationChange(event) {
this.paginationChange.emit(event);
}
/**
* Go to the previous page
*/
goPrev() {
this.prev.emit(true);
}
/**
* Go to the next page
*/
goNext() {
this.next.emit(true);
}
}

View File

@@ -286,6 +286,17 @@ import { SplitPipe } from './utils/split.pipe';
import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component';
import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component';
import { NotificationBoxComponent } from './notification-box/notification-box.component';
import { ObjectTableComponent } from './object-table/object-table.component';
import { TabulatableObjectsLoaderComponent } from './object-collection/shared/tabulatable-objects/tabulatable-objects-loader.component';
import {
TabulatableObjectsDirective
} from "./object-collection/shared/tabulatable-objects/tabulatable-objects.directive";
import {
AbstractTabulatableElementComponent
} from "./object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component";
import {
TabulatableResultListElementsComponent
} from "./object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component";
const MODULES = [
CommonModule,
@@ -349,7 +360,9 @@ const COMPONENTS = [
ThemedObjectListComponent,
ObjectDetailComponent,
ObjectGridComponent,
ObjectTableComponent,
AbstractListableElementComponent,
AbstractTabulatableElementComponent,
ObjectCollectionComponent,
PaginationComponent,
RSSComponent,
@@ -415,6 +428,7 @@ const ENTRY_COMPONENTS = [
CollectionListElementComponent,
CommunityListElementComponent,
SearchResultListElementComponent,
TabulatableResultListElementsComponent,
CommunitySearchResultListElementComponent,
CollectionSearchResultListElementComponent,
CollectionGridElementComponent,
@@ -471,7 +485,8 @@ const ENTRY_COMPONENTS = [
EpersonGroupListComponent,
EpersonSearchBoxComponent,
GroupSearchBoxComponent,
NotificationBoxComponent
NotificationBoxComponent,
TabulatableObjectsLoaderComponent,
];
const PROVIDERS = [
@@ -490,6 +505,7 @@ const DIRECTIVES = [
RoleDirective,
MetadataRepresentationDirective,
ListableObjectDirective,
TabulatableObjectsDirective,
ClaimedTaskActionsDirective,
FileValueAccessorDirective,
FileValidator,