Merge remote-tracking branch 'upstream/main' into add-json5-eslint-support

# Conflicts:
#	src/assets/i18n/fi.json5
#	src/assets/i18n/fr.json5
This commit is contained in:
Alexandre Vryghem
2023-04-04 00:07:07 +02:00
70 changed files with 852 additions and 333 deletions

View File

@@ -22,6 +22,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { APP_CONFIG } from '../../../config/app-config.interface';
import { environment } from '../../../environments/environment';
import { SortDirection } from '../../core/cache/models/sort-options.model';
describe('BrowseByDatePageComponent', () => {
let comp: BrowseByDatePageComponent;
@@ -49,12 +50,22 @@ describe('BrowseByDatePageComponent', () => {
]
}
});
const lastItem = Object.assign(new Item(), {
id: 'last-item-id',
metadata: {
'dc.date.issued': [
{
value: '1960-01-01'
}
]
}
});
const mockBrowseService = {
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
getFirstItemFor: () => createSuccessfulRemoteDataObject$(firstItem)
};
const mockBrowseService = {
getBrowseEntriesFor: (options: BrowseEntrySearchOptions) => toRemoteData([]),
getBrowseItemsFor: (value: string, options: BrowseEntrySearchOptions) => toRemoteData([firstItem]),
getFirstItemFor: (definition: string, scope?: string, sortDirection?: SortDirection) => null
};
const mockDsoService = {
findById: () => createSuccessfulRemoteDataObject$(mockCommunity)
@@ -91,9 +102,14 @@ describe('BrowseByDatePageComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByDatePageComponent);
const browseService = fixture.debugElement.injector.get(BrowseService);
spyOn(browseService, 'getFirstItemFor')
// ok to expect the default browse as first param since we just need the mock items obtained via sort direction.
.withArgs('author', undefined, SortDirection.ASC).and.returnValue(createSuccessfulRemoteDataObject$(firstItem))
.withArgs('author', undefined, SortDirection.DESC).and.returnValue(createSuccessfulRemoteDataObject$(lastItem));
comp = fixture.componentInstance;
fixture.detectChanges();
route = (comp as any).route;
fixture.detectChanges();
});
it('should initialize the list of items', () => {
@@ -107,6 +123,7 @@ describe('BrowseByDatePageComponent', () => {
});
it('should create a list of startsWith options with the current year first', () => {
expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
//expect(comp.startsWithOptions[0]).toEqual(new Date().getUTCFullYear());
expect(comp.startsWithOptions[0]).toEqual(1960);
});
});

View File

@@ -1,11 +1,10 @@
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
import {
BrowseByMetadataPageComponent,
browseParamsToOptions, getBrowseSearchOptions
browseParamsToOptions,
getBrowseSearchOptions
} from '../browse-by-metadata-page/browse-by-metadata-page.component';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { BrowseService } from '../../core/browse/browse.service';
@@ -16,7 +15,9 @@ import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { isValidDate } from '../../shared/date.util';
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-browse-by-date-page',
@@ -72,30 +73,24 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
/**
* Update the StartsWith options
* In this implementation, it creates a list of years starting from now, going all the way back to the earliest
* date found on an item within this scope. The further back in time, the bigger the change in years become to avoid
* extremely long lists with a one-year difference.
* In this implementation, it creates a list of years starting from the most recent item or the current year, going
* all the way back to the earliest date found on an item within this scope. The further back in time, the bigger
* the change in years become to avoid extremely long lists with a one-year difference.
* To determine the change in years, the config found under GlobalConfig.BrowseBy is used for this.
* @param definition The metadata definition to fetch the first item for
* @param metadataKeys The metadata fields to fetch the earliest date from (expects a date field)
* @param scope The scope under which to fetch the earliest item for
*/
updateStartsWithOptions(definition: string, metadataKeys: string[], scope?: string) {
const firstItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.ASC);
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
this.subs.push(
this.browseService.getFirstItemFor(definition, scope).subscribe((firstItemRD: RemoteData<Item>) => {
let lowerLimit = this.appConfig.browseBy.defaultLowerLimit;
if (hasValue(firstItemRD.payload)) {
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
if (isNotEmpty(date) && isValidDate(date)) {
const dateObj = new Date(date);
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
lowerLimit = isNaN(dateObj.getUTCFullYear()) ? lowerLimit : dateObj.getUTCFullYear();
}
}
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
const options = [];
const currentYear = new Date().getUTCFullYear();
const oneYearBreak = Math.floor((currentYear - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((currentYear - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
if (lowerLimit <= fiveYearBreak) {
lowerLimit -= 10;
} else if (lowerLimit <= oneYearBreak) {
@@ -103,7 +98,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
} else {
lowerLimit -= 1;
}
let i = currentYear;
let i = upperLimit;
while (i > lowerLimit) {
options.push(i);
if (i <= fiveYearBreak) {
@@ -121,4 +116,24 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
})
);
}
/**
* Returns the year from the item metadata field or the limit.
* @param itemRD the item remote data
* @param metadataKeys The metadata fields to fetch the earliest date from (expects a date field)
* @param limit the limit to use if the year can't be found in metadata
* @private
*/
private getLimit(itemRD: RemoteData<Item>, metadataKeys: string[], limit: number): number {
if (hasValue(itemRD.payload)) {
const date = itemRD.payload.firstMetadataValue(metadataKeys);
if (isNotEmpty(date) && isValidDate(date)) {
const dateObj = new Date(date);
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
return isNaN(dateObj.getUTCFullYear()) ? limit : dateObj.getUTCFullYear();
} else {
return new Date().getUTCFullYear();
}
}
}
}

View File

@@ -154,6 +154,8 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
if (typeof params.value === 'string'){
this.value = params.value.trim();
} else {
this.value = '';
}
if (typeof params.startsWith === 'string'){

View File

@@ -28,7 +28,8 @@
[title]="'toggle ' + node.name"
[attr.aria-label]="'toggle ' + node.name"
(click)="toggleExpanded(node)"
[ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'">
[ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'"
[attr.data-test]="(hasChild(null, node)| async) ? 'expand-button' : ''">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span>
</button>

View File

@@ -8,7 +8,7 @@ import { PostRequest } from '../data/request.models';
import {
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER
} from '../xsrf/xsrf.interceptor';
} from '../xsrf/xsrf.constants';
describe(`ServerAuthRequestService`, () => {
let href: string;

View File

@@ -13,7 +13,7 @@ import {
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER,
DSPACE_XSRF_COOKIE
} from '../xsrf/xsrf.interceptor';
} from '../xsrf/xsrf.constants';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';

View File

@@ -22,6 +22,7 @@ import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
import { HrefOnlyDataService } from '../data/href-only-data.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { BrowseDefinitionDataService } from './browse-definition-data.service';
import { SortDirection } from '../cache/models/sort-options.model';
export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig<BrowseEntry | Item>[] = [
@@ -160,8 +161,9 @@ export class BrowseService {
* Get the first item for a metadata definition in an optional scope
* @param definition
* @param scope
* @param sortDirection optional sort parameter
*/
getFirstItemFor(definition: string, scope?: string): Observable<RemoteData<Item>> {
getFirstItemFor(definition: string, scope?: string, sortDirection?: SortDirection): Observable<RemoteData<Item>> {
const href$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definition),
hasValueOperator(),
@@ -177,6 +179,9 @@ export class BrowseService {
}
args.push('page=0');
args.push('size=1');
if (sortDirection) {
args.push('sort=default,' + sortDirection);
}
if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString();
}

View File

@@ -62,25 +62,33 @@ describe('LocaleService test suite', () => {
});
describe('getCurrentLanguageCode', () => {
it('should return language saved on cookie', () => {
beforeEach(() => {
spyOn(translateService, 'getLangs').and.returnValue(langList);
});
it('should return the language saved on cookie if it\'s a valid & active language', () => {
spyOnGet.and.returnValue('de');
expect(service.getCurrentLanguageCode()).toBe('de');
});
describe('', () => {
beforeEach(() => {
spyOn(translateService, 'getLangs').and.returnValue(langList);
});
it('should return the default language if the cookie language is disabled', () => {
spyOnGet.and.returnValue('disabled');
expect(service.getCurrentLanguageCode()).toBe('en');
});
it('should return language from browser setting', () => {
spyOn(translateService, 'getBrowserLang').and.returnValue('it');
expect(service.getCurrentLanguageCode()).toBe('it');
});
it('should return the default language if the cookie language does not exist', () => {
spyOnGet.and.returnValue('does-not-exist');
expect(service.getCurrentLanguageCode()).toBe('en');
});
it('should return default language from config', () => {
spyOn(translateService, 'getBrowserLang').and.returnValue('fr');
expect(service.getCurrentLanguageCode()).toBe('en');
});
it('should return language from browser setting', () => {
spyOn(translateService, 'getBrowserLang').and.returnValue('it');
expect(service.getCurrentLanguageCode()).toBe('it');
});
it('should return default language from config', () => {
spyOn(translateService, 'getBrowserLang').and.returnValue('fr');
expect(service.getCurrentLanguageCode()).toBe('en');
});
});

View File

@@ -11,6 +11,7 @@ import { map, mergeMap, take } from 'rxjs/operators';
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { RouteService } from '../services/route.service';
import { DOCUMENT } from '@angular/common';
import { LangConfig } from '../../../config/lang-config.interface';
export const LANG_COOKIE = 'dsLanguage';
@@ -52,8 +53,7 @@ export class LocaleService {
getCurrentLanguageCode(): string {
// Attempt to get the language from a cookie
let lang = this.getLanguageCodeFromCookie();
if (isEmpty(lang)) {
// Cookie not found
if (isEmpty(lang) || environment.languages.find((langConfig: LangConfig) => langConfig.code === lang && langConfig.active) === undefined) {
// Attempt to get the browser language from the user
if (this.translate.getLangs().includes(this.translate.getBrowserLang())) {
lang = this.translate.getBrowserLang();

View File

@@ -0,0 +1,33 @@
/**
* XSRF / CSRF related constants
*/
/**
* Name of CSRF/XSRF header we (client) may SEND in requests to backend.
* (This is a standard header name for XSRF/CSRF defined by Angular)
*/
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
/**
* Name of CSRF/XSRF header we (client) may RECEIVE in responses from backend
* This header is defined by DSpace backend, see https://github.com/DSpace/RestContract/blob/main/csrf-tokens.md
*/
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
/**
* Name of client-side Cookie where we store the CSRF/XSRF token between requests.
* This cookie is only available to client, and should be updated whenever a new XSRF_RESPONSE_HEADER
* is found in a response from the backend.
*/
export const XSRF_COOKIE = 'XSRF-TOKEN';
/**
* Name of server-side cookie the backend expects the XSRF token to be in.
* When the backend receives a modifying request, it will validate the CSRF/XSRF token by looking
* for a match between the XSRF_REQUEST_HEADER and this Cookie. For more details see
* https://github.com/DSpace/RestContract/blob/main/csrf-tokens.md
*
* NOTE: This Cookie is NOT readable to the client/UI. It is only readable to the backend and will
* be sent along automatically by the user's browser.
*/
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';

View File

@@ -12,15 +12,7 @@ import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { CookieService } from '../services/cookie.service';
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';
// Name of XSRF header we may receive in responses from backend
export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN';
// Name of cookie where we store the XSRF token
export const XSRF_COOKIE = 'XSRF-TOKEN';
// Name of cookie the backend expects the XSRF token to be in
export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from './xsrf.constants';
/**
* Custom Http Interceptor intercepting Http Requests & Responses to

View File

@@ -1,6 +1,20 @@
.ngx-gallery {
display: inline-block;
margin-bottom: 20px;
width: 340px !important;
height: 279px !important;
:host ::ng-deep {
.ngx-gallery {
width: unset !important;
height: unset !important;
}
ngx-gallery-image {
max-width: 340px !important;
.ngx-gallery-image {
background-position: left;
}
}
ngx-gallery-image:after {
padding-top: 75%;
display: block;
content: '';
}
}

View File

@@ -1,4 +1,10 @@
video {
width: 340px;
height: 279px;
width: 100%;
height: auto;
max-width: 340px;
}
.buttons {
display: flex;
gap: .25rem;
}

View File

@@ -20,9 +20,9 @@
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
</ng-container>
<ng-container *ngIf="mediaViewer.image">
<div *ngIf="mediaViewer.image" class="mb-2">
<ds-themed-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-themed-media-viewer>
</ng-container>
</div>
<ds-themed-item-page-file-section [item]="object"></ds-themed-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-themed-metadata-representation-list class="ds-item-page-mixed-author-field"

View File

@@ -21,9 +21,9 @@
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
</ng-container>
<ng-container *ngIf="mediaViewer.image">
<div *ngIf="mediaViewer.image" class="mb-2">
<ds-themed-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-themed-media-viewer>
</ng-container>
</div>
<ds-themed-item-page-file-section [item]="object"></ds-themed-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-themed-metadata-representation-list class="ds-item-page-mixed-author-field"

View File

@@ -16,7 +16,7 @@
<ng-container #componentViewContainer></ng-container>
</div>
<small *ngIf="hasHint && ((!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
<small *ngIf="hasHint && (formBuilderService.hasArrayGroupValue(model) || (!model.repeatable && (isRelationship === false || value?.value === null)) || (model.repeatable === true && context?.index === context?.context?.groups?.length - 1)) && (!showErrorMessages || errorMessages.length === 0)"
class="text-muted ds-hint" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
<!-- In case of repeatable fields show empty space for all elements except the first -->
<div *ngIf="context?.index !== null

View File

@@ -147,12 +147,14 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
new DynamicListCheckboxGroupModel({
id: 'checkboxList',
vocabularyOptions: vocabularyOptions,
repeatable: true
repeatable: true,
required: false,
}),
new DynamicListRadioGroupModel({
id: 'radioList',
vocabularyOptions: vocabularyOptions,
repeatable: false
repeatable: false,
required: false,
}),
new DynamicRelationGroupModel({
submissionId: '1234',

View File

@@ -259,7 +259,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private submissionObjectService: SubmissionObjectDataService,
private ref: ChangeDetectorRef,
private formService: FormService,
private formBuilderService: FormBuilderService,
public formBuilderService: FormBuilderService,
private submissionService: SubmissionService,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
) {

View File

@@ -15,8 +15,10 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod
vocabularyOptions: VocabularyOptions;
groupLength?: number;
repeatable: boolean;
value?: any;
value?: VocabularyEntry[];
typeBindRelations?: DynamicFormControlRelation[];
required: boolean;
hint?: string;
}
export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@@ -26,6 +28,8 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@serializable() groupLength: number;
@serializable() _value: VocabularyEntry[];
@serializable() typeBindRelations: DynamicFormControlRelation[];
@serializable() required: boolean;
@serializable() hint: string;
isListGroup = true;
valueUpdates: Subject<any>;
@@ -36,6 +40,8 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
this.groupLength = config.groupLength || 5;
this._value = [];
this.repeatable = config.repeatable;
this.required = config.required;
this.hint = config.hint;
this.valueUpdates = new Subject<any>();
this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value);
@@ -56,9 +62,8 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
if (Array.isArray(value)) {
this._value = value;
} else {
// _value is non extendible so assign it a new array
const newValue = (this.value as VocabularyEntry[]).concat([value]);
this._value = newValue;
// _value is non-extendable so assign it a new array
this._value = (this.value as VocabularyEntry[]).concat([value]);
}
}
}

View File

@@ -6,12 +6,15 @@ import {
} from '@ng-dynamic-forms/core';
import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model';
import { hasValue } from '../../../../../empty.util';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig<any> {
vocabularyOptions: VocabularyOptions;
groupLength?: number;
repeatable: boolean;
value?: any;
value?: VocabularyEntry[];
required: boolean;
hint?: string;
}
export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
@@ -19,6 +22,8 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
@serializable() vocabularyOptions: VocabularyOptions;
@serializable() repeatable: boolean;
@serializable() groupLength: number;
@serializable() required: boolean;
@serializable() hint: string;
isListGroup = true;
constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) {
@@ -27,6 +32,8 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
this.vocabularyOptions = config.vocabularyOptions;
this.groupLength = config.groupLength || 5;
this.repeatable = config.repeatable;
this.required = config.required;
this.hint = config.hint;
this.value = config.value;
}

View File

@@ -17,7 +17,6 @@
[id]="item.id"
[formControlName]="item.id"
[name]="model.name"
[required]="model.required"
[value]="item.value"
(blur)="onBlur($event)"
(change)="onChange($event)"

View File

@@ -1,6 +1,5 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormGroup, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms';
import {
DynamicCheckboxModel,
DynamicFormControlComponent,
@@ -110,6 +109,9 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
protected setOptionsFromVocabulary() {
if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) {
const listGroup = this.group.controls[this.model.id] as FormGroup;
if (this.model.repeatable && this.model.required) {
listGroup.addValidators(this.hasAtLeastOneVocabularyEntry());
}
const pageInfo: PageInfo = new PageInfo({
elementsPerPage: 9999, currentPage: 1
} as PageInfo);
@@ -121,7 +123,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
let tempList: ListItem[] = [];
this.optionsList = entries.page;
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
entries.page.forEach((option, key) => {
entries.page.forEach((option: VocabularyEntry, key: number) => {
const value = option.authority || option.value;
const checked: boolean = isNotEmpty(findKey(
this.model.value,
@@ -156,4 +158,13 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
}
}
/**
* Checks if at least one {@link VocabularyEntry} has been selected.
*/
hasAtLeastOneVocabularyEntry(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return control && control.value && Object.values(control.value).find((checked: boolean) => checked === true) ? null : this.model.errorMessages;
};
}
}

View File

@@ -235,10 +235,16 @@ describe('FormBuilderService test suite', () => {
new DynamicListCheckboxGroupModel({
id: 'testCheckboxList',
vocabularyOptions: vocabularyOptions,
repeatable: true
repeatable: true,
required: false,
}),
new DynamicListRadioGroupModel({ id: 'testRadioList', vocabularyOptions: vocabularyOptions, repeatable: false }),
new DynamicListRadioGroupModel({
id: 'testRadioList',
vocabularyOptions: vocabularyOptions,
repeatable: false,
required: false,
}),
new DynamicRelationGroupModel({
submissionId,

View File

@@ -9,7 +9,7 @@ import { UploaderOptions } from './uploader-options.model';
import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
import { UploaderProperties } from './uploader-properties.model';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.constants';
import { CookieService } from '../../../core/services/cookie.service';
import { DragService } from '../../../core/drag.service';

View File

@@ -5,7 +5,7 @@
{{ 'statistics.table.title.' + report.reportType | translate }}
</h3>
<table class="table table-striped">
<table class="table table-striped" [attr.data-test]="report.reportType">
<tbody>
@@ -20,7 +20,7 @@
<tr *ngFor="let point of report.points"
class="{{point.id}}-data">
<th scope="row">
<th scope="row" data-test="statistics-label">
{{ getLabel(point) | async }}
</th>
<td *ngFor="let header of headers"

View File

@@ -3,8 +3,10 @@ import { Point, UsageReport } from '../../core/statistics/models/usage-report.mo
import { Observable, of } from 'rxjs';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { map } from 'rxjs/operators';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../core/shared/operators';
import { getRemoteDataPayload, getFinishedRemoteData } from '../../core/shared/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { TranslateService } from '@ngx-translate/core';
import { isEmpty } from '../../shared/empty.util';
/**
* Component representing a statistics table for a given usage report.
@@ -35,6 +37,7 @@ export class StatisticsTableComponent implements OnInit {
constructor(
protected dsoService: DSpaceObjectDataService,
protected nameService: DSONameService,
private translateService: TranslateService,
) {
}
@@ -54,9 +57,9 @@ export class StatisticsTableComponent implements OnInit {
switch (this.report.reportType) {
case 'TotalVisits':
return this.dsoService.findById(point.id).pipe(
getFirstSucceededRemoteData(),
getFinishedRemoteData(),
getRemoteDataPayload(),
map((item) => this.nameService.getName(item)),
map((item) => !isEmpty(item) ? this.nameService.getName(item) : this.translateService.instant('statistics.table.no-name')),
);
case 'TopCities':
case 'topCountries':

View File

@@ -5,5 +5,6 @@
[submissionDefinition]="submissionDefinition"
[submissionErrors]="submissionErrors"
[item]="item"
[collectionModifiable]="collectionModifiable"
[submissionId]="submissionId"></ds-submission-form>
</div>

View File

@@ -36,6 +36,13 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
*/
public collectionId: string;
/**
* Checks if the collection can be modifiable by the user
* @type {booelan}
*/
public collectionModifiable: boolean | null = null;
/**
* The list of submission's sections
* @type {WorkspaceitemSectionsObject}
@@ -109,6 +116,9 @@ export class SubmissionEditComponent implements OnDestroy, OnInit {
* Retrieve workspaceitem/workflowitem from server and initialize all instance variables
*/
ngOnInit() {
this.collectionModifiable = this.route.snapshot.data?.collectionModifiable ?? null;
this.subs.push(
this.route.paramMap.pipe(
switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))),

View File

@@ -25,7 +25,7 @@
class="btn btn-outline-primary"
(blur)="onClose()"
(click)="onClose()"
[disabled]="(processingChange$ | async)"
[disabled]="(processingChange$ | async) || collectionModifiable == false"
ngbDropdownToggle>
<span *ngIf="(processingChange$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
<span *ngIf="!(processingChange$ | async)">{{ selectedCollectionName$ | async }}</span>

View File

@@ -52,6 +52,12 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
*/
@Input() currentDefinition: string;
/**
* Checks if the collection can be modifiable by the user
* @type {booelan}
*/
@Input() collectionModifiable: boolean | null = null;
/**
* The submission id
* @type {string}

View File

@@ -11,6 +11,7 @@
<ds-submission-form-collection [currentCollectionId]="collectionId"
[currentDefinition]="definitionId"
[submissionId]="submissionId"
[collectionModifiable]="collectionModifiable"
(collectionChange)="onCollectionChange($event)">
</ds-submission-form-collection>
</div>

View File

@@ -34,8 +34,16 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy {
* @type {string}
*/
@Input() collectionId: string;
@Input() item: Item;
/**
* Checks if the collection can be modifiable by the user
* @type {booelan}
*/
@Input() collectionModifiable: boolean | null = null;
/**
* The list of submission's sections
* @type {WorkspaceitemSectionsObject}

View File

@@ -34,7 +34,11 @@ import {
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'workflow-item.edit.title', breadcrumbKey: 'workflow-item.edit' }
data: {
title: 'workflow-item.edit.title',
breadcrumbKey: 'workflow-item.edit',
collectionModifiable: false
}
},
{
canActivate: [AuthenticatedGuard],

View File

@@ -2680,7 +2680,7 @@
"itemtemplate.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
"itemtemplate.edit.metadata.notifications.discarded.title": "Changed discarded",
"itemtemplate.edit.metadata.notifications.discarded.title": "Changes discarded",
"itemtemplate.edit.metadata.notifications.error.title": "An error occurred",
@@ -2690,7 +2690,7 @@
"itemtemplate.edit.metadata.notifications.outdated.content": "The item template you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts",
"itemtemplate.edit.metadata.notifications.outdated.title": "Changed outdated",
"itemtemplate.edit.metadata.notifications.outdated.title": "Changes outdated",
"itemtemplate.edit.metadata.notifications.saved.content": "Your changes to this item template's metadata were saved.",
@@ -4033,6 +4033,8 @@
"statistics.table.header.views": "Views",
"statistics.table.no-name": "(object name could not be loaded)",
"submission.edit.breadcrumbs": "Edit Submission",

View File

@@ -156,7 +156,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nimi",
// TODO New key - Add a translation
// "admin.registries.bitstream-formats.table.id" : "ID",
"admin.registries.bitstream-formats.table.id": "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
@@ -207,7 +208,7 @@
"admin.registries.metadata.schemas.table.delete": "Poista valittu",
// "admin.registries.metadata.schemas.table.id": "ID",
"admin.registries.metadata.schemas.table.id": "ID-tunnus",
"admin.registries.metadata.schemas.table.id": "ID",
// "admin.registries.metadata.schemas.table.name": "Name",
"admin.registries.metadata.schemas.table.name": "Nimi",
@@ -237,7 +238,8 @@
// "admin.registries.schema.fields.table.field": "Field",
"admin.registries.schema.fields.table.field": "Kenttä",
// TODO New key - Add a translation
// "admin.registries.schema.fields.table.id": "ID",
"admin.registries.schema.fields.table.id": "ID",
// "admin.registries.schema.fields.table.scopenote": "Scope Note",
@@ -408,7 +410,7 @@
"admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Jäsenenä näissä ryhmissä:",
// "admin.access-control.epeople.form.table.id": "ID",
"admin.access-control.epeople.form.table.id": "ID-tunnus",
"admin.access-control.epeople.form.table.id": "ID",
// "admin.access-control.epeople.form.table.name": "Name",
"admin.access-control.epeople.form.table.name": "Nimi",
@@ -2495,7 +2497,7 @@
"item.edit.tabs.status.buttons.reinstate.label": "Palauta tietue arkistoon",
// "item.edit.tabs.status.buttons.withdraw.button": "Withdraw this item",
"item.edit.tabs.status.buttons.withdraw.button": "poista tämä kohde",
"item.edit.tabs.status.buttons.withdraw.button": "Poista tämä kohde",
// "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository",
"item.edit.tabs.status.buttons.withdraw.label": "Poista tietue käytöstä",
@@ -2510,7 +2512,7 @@
"item.edit.tabs.status.labels.handle": "Handle-tunnus",
// "item.edit.tabs.status.labels.id": "Item Internal ID",
"item.edit.tabs.status.labels.id": "Tietueen sisäinen ID-tunnus",
"item.edit.tabs.status.labels.id": "Tietueen sisäinen ID",
// "item.edit.tabs.status.labels.itemPage": "Item Page",
"item.edit.tabs.status.labels.itemPage": "Tietueen tiedot",
@@ -3348,7 +3350,7 @@
"orgunit.page.edit": "Muokkaa tietuetta",
// "orgunit.page.id": "ID",
"orgunit.page.id": "ID-tunnus",
"orgunit.page.id": "ID",
// "orgunit.page.titleprefix": "Organizational Unit: ",
"orgunit.page.titleprefix": "Organisaatioyksikkö: ",
@@ -3400,7 +3402,7 @@
"person.page.orcid": "ORCID-tunniste",
// "person.page.staffid": "Staff ID",
"person.page.staffid": "Henkilökunnan ID-tunnus",
"person.page.staffid": "Henkilökunnan ID",
// "person.page.titleprefix": "Person: ",
"person.page.titleprefix": "Käyttäjä: ",
@@ -3652,7 +3654,7 @@
"project.page.funder": "Rahoittajat",
// "project.page.id": "ID",
"project.page.id": "ID-tunnus",
"project.page.id": "ID",
// "project.page.keyword": "Keywords",
"project.page.keyword": "Asiasanat",
@@ -3936,7 +3938,7 @@
"resource-policies.form.eperson-group-list.table.headers.action": "Toimenpide",
// "resource-policies.form.eperson-group-list.table.headers.id": "ID",
"resource-policies.form.eperson-group-list.table.headers.id": "ID-tunnus",
"resource-policies.form.eperson-group-list.table.headers.id": "ID",
// "resource-policies.form.eperson-group-list.table.headers.name": "Name",
"resource-policies.form.eperson-group-list.table.headers.name": "Nimi",
@@ -3984,7 +3986,7 @@
"resource-policies.table.headers.group": "Ryhmä",
// "resource-policies.table.headers.id": "ID",
"resource-policies.table.headers.id": "ID-tunnus",
"resource-policies.table.headers.id": "ID",
// "resource-policies.table.headers.name": "Name",
"resource-policies.table.headers.name": "Nimi",
@@ -4911,8 +4913,8 @@
// "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):",
"submission.sections.upload.header.policy.default.withlist": "Yksittäisten tiedostojen pääsyrajoitusten lisäksi {{collectionName}}-kokoelmaan ladatut tiedostot ovat seuraavien ryhmien saatavilla:",
// "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or <strong>upload additional files by dragging & dropping them anywhere on the page.</strong>",
"submission.sections.upload.info": "Tietueen kaikki tiedostot on lueteltu tässä. Voit päivittää tiedoston metadataa ja pääsyehtoja tai <strong>ladata lisää tiedostoja raahaamalla ne mihin hyvänsä sivun kohtaan</strong>",
// "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
"submission.sections.upload.info": "Tietueen kaikki tiedostot on lueteltu tässä. Voit päivittää tiedoston metadataa ja pääsyehtoja tai <strong>ladata lisää tiedostoja raahaamalla ne mihin hyvänsä sivun kohtaan.</strong>",
// "submission.sections.upload.no-entry": "No",
"submission.sections.upload.no-entry": "Ei",
@@ -5063,8 +5065,7 @@
"uploader.or": " tai",
// "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)",
// TODO Source message changed - Revise the translation
"uploader.processing": "Käsitellään",
"uploader.processing": "Käsitellään ladattuja tiedostoja... (voit sulkea tämän sivun)",
// "uploader.queue-length": "Queue length",
"uploader.queue-length": "Jonon pituus",

View File

@@ -952,6 +952,9 @@
// "bitstream-request-a-copy.submit.error": "Something went wrong with submitting the item request.",
"bitstream-request-a-copy.submit.error": "Un problème est survenu lors de la soumission de la demande d'Item",
//"browse.back.all-results": "All browse results",
"browse.back.all-results": "Tous les résultats",
// "browse.comcol.by.author": "By Author",
"browse.comcol.by.author": "Auteur",
@@ -1069,9 +1072,18 @@
// "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}",
"browse.title": "Parcourir la collection {{ collection }} par {{ field }} {{ value }}",
//"browse.title.page": "Browsing {{ collection }} by {{ field }} {{ value }}",
"browse.title.page": "Parcourir la collection {{ collection }} par {{ field }} {{ value }}",
//"search.browse.item-back": "Back to Results",
"search.browse.item-back": "Retour aux résultats",
// "chips.remove": "Remove chip",
"chips.remove": "Supprimer fragment",
//"claimed-approved-search-result-list-element.title": "Approved",
"claimed-approved-search-result-list-element.title": "Approuvé",
// "collection.create.head": "Create a Collection",
"collection.create.head": "Créer une collection",
@@ -1719,6 +1731,9 @@
// "cookies.consent.decline": "Decline",
"cookies.consent.decline": "Refuser",
//"cookies.consent.ok": "That's ok",
"cookies.consent.ok": "Accepter",
// "cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>. <br/> To learn more, please read our {privacyPolicy}.",
"cookies.consent.content-notice.description": "Vos données personnelles sont récupérées et utilisées dans les contextes suivants : <strong>authentification, préférences, consentement et statistiques</strong>. <br/> Pour plus d'informations, veuillez vous référer à la {privacyPolicy}.",
@@ -1884,6 +1899,9 @@
// "dso-selector.set-scope.community.button": "Search all of DSpace",
"dso-selector.set-scope.community.button": "Chercher dans toutes les collections",
//"dso-selector.set-scope.community.or-divider": "or",
"dso-selector.set-scope.community.or-divider": "ou",
// "dso-selector.set-scope.community.input-header": "Search for a community or collection",
"dso-selector.set-scope.community.input-header": "Chercher une communauté ou une collection",
@@ -2496,6 +2514,71 @@
// "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper",
"item.edit.tabs.item-mapper.title": "Édition d'Item - Association de collection",
// "item.edit.identifiers.doi.status.UNKNOWN": "Unknown",
"item.edit.identifiers.doi.status.UNKNOWN": "Inconnu",
// "item.edit.identifiers.doi.status.TO_BE_REGISTERED": "Queued for registration",
"item.edit.identifiers.doi.status.TO_BE_REGISTERED": "En attente d'inscription",
// "item.edit.identifiers.doi.status.TO_BE_RESERVED": "Queued for reservation",
"item.edit.identifiers.doi.status.TO_BE_RESERVED": "En attente de réservation",
// "item.edit.identifiers.doi.status.IS_REGISTERED": "Registered",
"item.edit.identifiers.doi.status.IS_REGISTERED": "Inscrit",
// "item.edit.identifiers.doi.status.IS_RESERVED": "Reserved",
"item.edit.identifiers.doi.status.IS_RESERVED": "Réseré",
// "item.edit.identifiers.doi.status.UPDATE_RESERVED": "Reserved (update queued)",
"item.edit.identifiers.doi.status.UPDATE_RESERVED": "Réservé (en attente)",
// "item.edit.identifiers.doi.status.UPDATE_REGISTERED": "Registered (update queued)",
"item.edit.identifiers.doi.status.UPDATE_REGISTERED": "Inscrit (en attente)",
// "item.edit.identifiers.doi.status.UPDATE_BEFORE_REGISTRATION": "Queued for update and registration",
"item.edit.identifiers.doi.status.UPDATE_BEFORE_REGISTRATION": "En attente pour la mise à jour et l'inscription",
// "item.edit.identifiers.doi.status.TO_BE_DELETED": "Queued for deletion",
"item.edit.identifiers.doi.status.TO_BE_DELETED": "En attente pour la suppression",
// "item.edit.identifiers.doi.status.DELETED": "Deleted",
"item.edit.identifiers.doi.status.DELETED": "Supprimé",
// "item.edit.identifiers.doi.status.PENDING": "Pending (not registered)",
"item.edit.identifiers.doi.status.PENDING": "En attente (non inscrit)",
// "item.edit.identifiers.doi.status.MINTED": "Minted (not registered)",
"item.edit.identifiers.doi.status.MINTED": "Émis (non inscrit)",
// "item.edit.tabs.status.buttons.register-doi.label": "Register a new or pending DOI",
"item.edit.tabs.status.buttons.register-doi.label": "Inscrire un nouveau DOI ou un DOI en attente",
// "item.edit.tabs.status.buttons.register-doi.button": "Register DOI...",
"item.edit.tabs.status.buttons.register-doi.button": "Inscrire le DOI...",
// "item.edit.register-doi.header": "Register a new or pending DOI",
"item.edit.register-doi.header": "Inscrire un nouveau DOI ou un DOI en attente",
// "item.edit.register-doi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out",
"item.edit.register-doi.description": "Réviser les identifiants en attente and les métadonnées ci-dessous item metadata below puis cliquer sur Confirmer afin de lancer l'inscription du DOI ou sur Annuler pour interrompre l'inscription",
// "item.edit.register-doi.confirm": "Confirm",
"item.edit.register-doi.confirm": "Confirmer",
// "item.edit.register-doi.cancel": "Cancel",
"item.edit.register-doi.cancel": "Annuler",
// "item.edit.register-doi.success": "DOI queued for registration successfully.",
"item.edit.register-doi.success": "DOI mis en attente pour l'inscription.",
// "item.edit.register-doi.error": "Error registering DOI",
"item.edit.register-doi.error": "Erreur lors de l'inscription du DOI",
// "item.edit.register-doi.to-update": "The following DOI has already been minted and will be queued for registration online",
"item.edit.register-doi.to-update": "Le DOI suivant a déjà été généré et sera mis en attente pour être inscrit",
// "item.edit.item-mapper.buttons.add": "Map item to selected collections",
"item.edit.item-mapper.buttons.add": "Associer l'Item aux collections sélectionnées",
@@ -3739,6 +3822,9 @@
// "mydspace.show.workspace": "Your Submissions",
"mydspace.show.workspace": "Vos dépôts",
//"mydspace.show.supervisedWorkspace": "Supervised items",
"mydspace.show.supervisedWorkspace": "Items supervisés",
// "mydspace.status.archived": "Archived",
"mydspace.status.archived": "Archivés",
@@ -3805,6 +3891,9 @@
// "nav.stop-impersonating": "Stop impersonating EPerson",
"nav.stop-impersonating": "Retour à votre propre EPerson",
//"nav.subscriptions" : "Subscriptions",
"nav.subscriptions": "Abonnements",
// "nav.toggle": "Toggle navigation",
"nav.toggle": "Basculer la navigation",
@@ -5061,6 +5150,9 @@
// "submission.import-external.source.crossref": "CrossRef",
"submission.import-external.source.crossref": "CrossRef (DOI)",
//"submission.import-external.source.datacite": "DataCite",
"submission.import-external.source.datacite": "DataCite (DOI)",
// "submission.import-external.source.scielo": "SciELO",
"submission.import-external.source.scielo": "SciELO",
@@ -5076,6 +5168,9 @@
// "submission.import-external.source.orcidWorks": "ORCID",
"submission.import-external.source.orcidWorks": "ORCID",
//"submission.import-external.source.epo": "European Patent Office (EPO)",
"submission.import-external.source.epo": "Office Européen des brevets (OEB)",
// "submission.import-external.source.loading": "Loading ...",
"submission.import-external.source.loading": "En cours de chargement ...",
@@ -5991,8 +6086,15 @@
// "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata",
"virtual-metadata.delete-relationship.modal-head": "Sélectionner les Items pour lesquels vous souhaitez sauvegarder les métadonnées virtuelles en tant que métadonnées réelles",
//"supervisedWorkspace.search.results.head": "Supervised Items",
"supervisedWorkspace.search.results.head": "Items supervisés",
//"workspace.search.results.head": "Your submissions",
"workspace.search.results.head": "Vos dépôts",
// "workspace.search.results.head": "Your submissions",
"workspace.search.results.head": "Vos soumissions",
"workspace.search.results.head": "Vos dépôts",
// "workflowAdmin.search.results.head": "Administer Workflow",
"workflowAdmin.search.results.head": "Workflow Administrateur",
@@ -6000,6 +6102,9 @@
// "workflow.search.results.head": "Workflow tasks",
"workflow.search.results.head": "Tableau de suivi",
//"supervision.search.results.head": "Workflow and Workspace tasks",
"supervision.search.results.head": "Tableau de suivi et dépôts en cours",
// "workflow-item.edit.breadcrumbs": "Edit workflowitem",
"workflow-item.edit.breadcrumbs": "Éditer l'Item du Workflow",
@@ -6072,7 +6177,83 @@
// "idle-modal.log-out": "Log out",
"idle-modal.log-out": "Déconnexion",
// "idle-modal.extend-session": "Extend session"
// "idle-modal.extend-session": "Extend session",
"idle-modal.extend-session": "Prolonger la session",
// "system-wide-alert-banner.retrieval.error": "Something went wrong retrieving the system-wide alert banner",
"system-wide-alert-banner.retrieval.error": "Une erreur s'est produite lors de la récupération de la bannière du message d'avertissement",
//"system-wide-alert-banner.countdown.prefix": "In",
"system-wide-alert-banner.countdown.prefix": "Dans",
// "system-wide-alert-banner.countdown.days": "{{days}} day(s),",
"system-wide-alert-banner.countdown.days": "{{days}} jour(s),",
// "system-wide-alert-banner.countdown.hours": "{{hours}} hour(s) and",
"system-wide-alert-banner.countdown.hours": "{{hours}} heure(s) et",
// "system-wide-alert-banner.countdown.minutes": "{{minutes}} minute(s):",
"system-wide-alert-banner.countdown.minutes": "{{minutes}} minute(s):",
// "menu.section.system-wide-alert": "System-wide Alert",
"menu.section.system-wide-alert": "Messages d'avertissement",
// "system-wide-alert.form.header": "System-wide Alert",
"system-wide-alert.form.header": "Messages d'avertissement",
// "system-wide-alert-form.retrieval.error": "Something went wrong retrieving the system-wide alert",
"system-wide-alert-form.retrieval.error": "Une erreur s'est produite lors de la récupération de la bannière du message d'avertissement",
//"system-wide-alert.form.cancel": "Cancel",
"system-wide-alert.form.cancel": "Annuler",
//"system-wide-alert.form.save": "Save",
"system-wide-alert.form.save": "Sauvegarder",
//"system-wide-alert.form.label.active": "ACTIVE",
"system-wide-alert.form.label.active": "ACTIF",
//"system-wide-alert.form.label.inactive": "INACTIVE",
"system-wide-alert.form.label.inactive": "INACTIF",
//"system-wide-alert.form.error.message": "The system wide alert must have a message",
"system-wide-alert.form.error.message": "Le message d'avertissement ne peut pas être vide",
//"system-wide-alert.form.label.message": "Alert message",
"system-wide-alert.form.label.message": "Message d'avertissement",
//"system-wide-alert.form.label.countdownTo.enable": "Enable a countdown timer",
"system-wide-alert.form.label.countdownTo.enable": "Activer un compte à rebours",
//"system-wide-alert.form.label.countdownTo.hint": "Hint: Set a countdown timer. When enabled, a date can be set in the future and the system-wide alert banner will perform a countdown to the set date. When this timer ends, it will disappear from the alert. The server will NOT be automatically stopped.",
"system-wide-alert.form.label.countdownTo.hint": "Lorsque cette option est activée, il est possible de fixer une date dans le futur et la bannière du message d'avertissement effectuera un compte à rebours jusqu'à la date fixée. À la fin du compte à rebours, le message d'avertissement disparaîtra mais le serveur ne sera pas arrêté automatiquement.",
//"system-wide-alert.form.label.preview": "System-wide alert preview",
"system-wide-alert.form.label.preview": "Aperçu du message d'avertissement",
//"system-wide-alert.form.update.success": "The system-wide alert was successfully updated",
"system-wide-alert.form.update.success": "Le message d'avertissement a été mis à jour",
//"system-wide-alert.form.update.error": "Something went wrong when updating the system-wide alert",
"system-wide-alert.form.update.error": "Un erreur s'est produite lors de la mise à jour du message d'avertissement",
//"system-wide-alert.form.create.success": "The system-wide alert was successfully created",
"system-wide-alert.form.create.success": "Le message d'avertissement a été crée",
//"system-wide-alert.form.create.error": "Something went wrong when creating the system-wide alert",
"system-wide-alert.form.create.error": "Un erreur s'est produite lors de la création du message d'avertissement",
//"admin.system-wide-alert.breadcrumbs": "System-wide Alerts",
"admin.system-wide-alert.breadcrumbs": "Messages d'avertissement",
//"admin.system-wide-alert.title": "System-wide Alerts",
"admin.system-wide-alert.title": "Messages d'avertissement",
}

View File

@@ -209,6 +209,10 @@ export const environment: BuildConfig = {
code: 'el',
label: 'Ελληνικά',
active: true,
}, {
code: 'disabled',
label: 'Disabled',
active: false,
}],
// Browse-By Pages