From cb7b8b3d221aa57972cb694955bc93df1ba8c1f0 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 15 Nov 2023 10:51:01 +0100 Subject: [PATCH 01/50] Submission form now displays custom messages for regex validated fields if they exist --- .../shared/form/builder/parsers/concat-field-parser.ts | 4 +++- .../shared/form/builder/parsers/dropdown-field-parser.ts | 4 +++- src/app/shared/form/builder/parsers/field-parser.ts | 9 +++++++-- src/app/shared/form/builder/parsers/name-field-parser.ts | 6 ++++-- src/app/shared/form/builder/parsers/parser-factory.ts | 2 ++ .../shared/form/builder/parsers/series-field-parser.ts | 6 ++++-- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index e86de70c816..c18fa8234e4 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -19,6 +19,7 @@ import { SUBMISSION_ID } from './field-parser'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { TranslateService } from '@ngx-translate/core'; export class ConcatFieldParser extends FieldParser { @@ -27,10 +28,11 @@ export class ConcatFieldParser extends FieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService, protected separator: string, protected firstPlaceholder: string = null, protected secondPlaceholder: string = null) { - super(submissionId, configData, initFormValues, parserOptions); + super(submissionId, configData, initFormValues, parserOptions, translate); this.separator = separator; this.firstPlaceholder = firstPlaceholder; diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 3e5ec0b9daa..a4bfb810f34 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -15,6 +15,7 @@ import { import { isNotEmpty } from '../../../empty.util'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; +import { TranslateService } from '@ngx-translate/core'; export class DropdownFieldParser extends FieldParser { @@ -23,8 +24,9 @@ export class DropdownFieldParser extends FieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService ) { - super(submissionId, configData, initFormValues, parserOptions); + super(submissionId, configData, initFormValues, parserOptions, translate); } public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 7ea55d44549..3c4d425df36 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -25,6 +25,7 @@ import { VocabularyOptions } from '../../../../core/submission/vocabularies/mode import { ParserType } from './parser-type'; import { isNgbDateStruct } from '../../../date.util'; import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; +import { TranslateService } from '@ngx-translate/core'; export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); @@ -50,7 +51,8 @@ export abstract class FieldParser { @Inject(SUBMISSION_ID) protected submissionId: string, @Inject(CONFIG_DATA) protected configData: FormFieldModel, @Inject(INIT_FORM_VALUES) protected initFormValues: any, - @Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions + @Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions, + protected translate: TranslateService ) { } @@ -395,11 +397,14 @@ export abstract class FieldParser { } else { regex = new RegExp(this.configData.input.regex); } + const baseTranslationKey = 'error.validation.pattern'; + const fieldranslationKey = `${baseTranslationKey}.${controlModel.id}`; + const fieldTranslationExists = this.translate.instant(fieldranslationKey) !== fieldranslationKey; controlModel.validators = Object.assign({}, controlModel.validators, { pattern: regex }); controlModel.errorMessages = Object.assign( {}, controlModel.errorMessages, - { pattern: 'error.validation.pattern' }); + { pattern: fieldTranslationExists ? fieldranslationKey : baseTranslationKey }); } protected markAsRequired(controlModel) { diff --git a/src/app/shared/form/builder/parsers/name-field-parser.ts b/src/app/shared/form/builder/parsers/name-field-parser.ts index e5ecb034ea4..469b32be929 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.ts @@ -1,4 +1,5 @@ import { Inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; import { CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser'; @@ -10,8 +11,9 @@ export class NameFieldParser extends ConcatFieldParser { @Inject(SUBMISSION_ID) submissionId: string, @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, - @Inject(PARSER_OPTIONS) parserOptions: ParserOptions + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService ) { - super(submissionId, configData, initFormValues, parserOptions, ',', 'form.last-name', 'form.first-name'); + super(submissionId, configData, initFormValues, parserOptions, translate, ',', 'form.last-name', 'form.first-name'); } } diff --git a/src/app/shared/form/builder/parsers/parser-factory.ts b/src/app/shared/form/builder/parsers/parser-factory.ts index 26a9cb0f289..97fc36cbb48 100644 --- a/src/app/shared/form/builder/parsers/parser-factory.ts +++ b/src/app/shared/form/builder/parsers/parser-factory.ts @@ -19,12 +19,14 @@ import { SeriesFieldParser } from './series-field-parser'; import { TagFieldParser } from './tag-field-parser'; import { TextareaFieldParser } from './textarea-field-parser'; import { DisabledFieldParser } from './disabled-field-parser'; +import { TranslateService } from '@ngx-translate/core'; const fieldParserDeps = [ SUBMISSION_ID, CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, + TranslateService ]; /** diff --git a/src/app/shared/form/builder/parsers/series-field-parser.ts b/src/app/shared/form/builder/parsers/series-field-parser.ts index 36ee9c36c1a..589a6dc04ca 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.ts @@ -1,4 +1,5 @@ import { Inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; import { CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser'; @@ -10,8 +11,9 @@ export class SeriesFieldParser extends ConcatFieldParser { @Inject(SUBMISSION_ID) submissionId: string, @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, - @Inject(PARSER_OPTIONS) parserOptions: ParserOptions + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService ) { - super(submissionId, configData, initFormValues, parserOptions, ';'); + super(submissionId, configData, initFormValues, parserOptions, translate, ';'); } } From 0ae2b52c72026ff4df7837c6685489583719e866 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 15 Nov 2023 13:27:58 +0100 Subject: [PATCH 02/50] Fix tests to include translationservice dependency --- .../form/builder/form-builder.service.spec.ts | 6 +++- .../builder/parsers/date-field-parser.spec.ts | 10 ++++-- .../parsers/disabled-field-parser.spec.ts | 8 +++-- .../parsers/dropdown-field-parser.spec.ts | 8 +++-- .../builder/parsers/list-field-parser.spec.ts | 10 +++--- .../parsers/lookup-field-parser.spec.ts | 8 +++-- .../parsers/lookup-name-field-parser.spec.ts | 8 +++-- .../builder/parsers/name-field-parser.spec.ts | 10 +++--- .../parsers/onebox-field-parser.spec.ts | 12 ++++--- .../relation-group-field-parser.spec.ts | 10 +++--- .../form/builder/parsers/row-parser.spec.ts | 35 ++++++++++++------- .../parsers/series-field-parser.spec.ts | 10 +++--- .../builder/parsers/tag-field-parser.spec.ts | 8 +++-- .../parsers/textarea-field-parser.spec.ts | 8 +++-- 14 files changed, 96 insertions(+), 55 deletions(-) diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 5e045c88ed5..f15fbc42381 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -51,6 +51,8 @@ import { FormRowModel } from '../../../core/config/models/config-submission-form import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; import {createSuccessfulRemoteDataObject$} from '../../remote-data.utils'; import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; +import { getMockTranslateService } from '../../mocks/translate.service.mock'; +import { TranslateService } from '@ngx-translate/core'; describe('FormBuilderService test suite', () => { @@ -81,6 +83,7 @@ describe('FormBuilderService test suite', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(typeFieldTestValue); + let translateService = getMockTranslateService(); TestBed.configureTestingModule({ imports: [ReactiveFormsModule], providers: [ @@ -88,7 +91,8 @@ describe('FormBuilderService test suite', () => { { provide: DynamicFormValidationService, useValue: {} }, { provide: NG_VALIDATORS, useValue: testValidator, multi: true }, { provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true }, - { provide: ConfigurationDataService, useValue: configSpy } + { provide: ConfigurationDataService, useValue: configSpy }, + { provide: TranslateService, useValue: translateService }, ] }); diff --git a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts index 9ab43709ad0..891bb1d9d46 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts @@ -3,10 +3,14 @@ import { DateFieldParser } from './date-field-parser'; import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + + describe('DateFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -37,13 +41,13 @@ describe('DateFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof DateFieldParser).toBe(true); }); it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => { - const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -56,7 +60,7 @@ describe('DateFieldParser test suite', () => { }; const expectedValue = '1983-11-18'; - const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts index d69f0e48e90..2168d7e2bf3 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -2,10 +2,12 @@ import { FormFieldModel } from '../models/form-field.model'; import { ParserOptions } from './parser-options'; import { DisabledFieldParser } from './disabled-field-parser'; import { DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('DisabledFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -35,13 +37,13 @@ describe('DisabledFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof DisabledFieldParser).toBe(true); }); it('should return a DynamicDisabledModel object when repeatable option is false', () => { - const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -56,7 +58,7 @@ describe('DisabledFieldParser test suite', () => { }; const expectedValue = 'test description'; - const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); expect(fieldModel.value.value).toEqual(expectedValue); diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts index 3dca7558b34..08a93f76d36 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts @@ -2,9 +2,11 @@ import { FormFieldModel } from '../models/form-field.model'; import { DropdownFieldParser } from './dropdown-field-parser'; import { DynamicScrollableDropdownModel } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('DropdownFieldParser test suite', () => { let field: FormFieldModel; + let translateService = getMockTranslateService(); const submissionId = '1234'; const initFormValues = {}; @@ -37,13 +39,13 @@ describe('DropdownFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof DropdownFieldParser).toBe(true); }); it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => { - const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -52,7 +54,7 @@ describe('DropdownFieldParser test suite', () => { it('should throw when authority is not passed', () => { field.selectableMetadata[0].controlledVocabulary = null; - const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(() => parser.parse()) .toThrow(); diff --git a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts index 30d1913a519..ba9f9291cf0 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts @@ -4,10 +4,12 @@ import { ListFieldParser } from './list-field-parser'; import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('ListFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -39,13 +41,13 @@ describe('ListFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof ListFieldParser).toBe(true); }); it('should return a DynamicListCheckboxGroupModel object when repeatable option is true', () => { - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -54,7 +56,7 @@ describe('ListFieldParser test suite', () => { it('should return a DynamicListRadioGroupModel object when repeatable option is false', () => { field.repeatable = false; - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -67,7 +69,7 @@ describe('ListFieldParser test suite', () => { }; const expectedValue = [new FormFieldMetadataValueObject('test type')]; - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts index 24efcf34622..a932dc637c2 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts @@ -3,10 +3,12 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu import { LookupFieldParser } from './lookup-field-parser'; import { DynamicLookupModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('LookupFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -38,13 +40,13 @@ describe('LookupFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof LookupFieldParser).toBe(true); }); it('should return a DynamicLookupModel object when repeatable option is false', () => { - const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -57,7 +59,7 @@ describe('LookupFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test journal'); - const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts index d0281681ef6..6220a6e74ce 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts @@ -3,10 +3,12 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu import { LookupNameFieldParser } from './lookup-name-field-parser'; import { DynamicLookupNameModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('LookupNameFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -38,13 +40,13 @@ describe('LookupNameFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof LookupNameFieldParser).toBe(true); }); it('should return a DynamicLookupNameModel object when repeatable option is false', () => { - const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -57,7 +59,7 @@ describe('LookupNameFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test author'); - const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 6b520142cc4..e124181b247 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -3,12 +3,14 @@ import { NameFieldParser } from './name-field-parser'; import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('NameFieldParser test suite', () => { let field1: FormFieldModel; let field2: FormFieldModel; let field3: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -71,13 +73,13 @@ describe('NameFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); expect(parser instanceof NameFieldParser).toBe(true); }); it('should return a DynamicConcatModel object when repeatable option is false', () => { - const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -85,7 +87,7 @@ describe('NameFieldParser test suite', () => { }); it('should return a DynamicConcatModel object with the correct separator', () => { - const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -98,7 +100,7 @@ describe('NameFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test, name', undefined, undefined, 'test'); - const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index a4c71d1f426..6b7ac65a587 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -5,11 +5,13 @@ import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic- import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { ParserOptions } from './parser-options'; import { FieldParser } from './field-parser'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('OneboxFieldParser test suite', () => { let field1: FormFieldModel; let field2: FormFieldModel; let field3: FormFieldModel; + let translateService = getMockTranslateService(); const submissionId = '1234'; const initFormValues = {}; @@ -73,13 +75,13 @@ describe('OneboxFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); expect(parser instanceof OneboxFieldParser).toBe(true); }); it('should return a DynamicQualdropModel object when selectableMetadata is multiple', () => { - const parser = new OneboxFieldParser(submissionId, field2, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field2, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -87,7 +89,7 @@ describe('OneboxFieldParser test suite', () => { }); it('should return a DsDynamicInputModel object when selectableMetadata is not multiple', () => { - const parser = new OneboxFieldParser(submissionId, field3, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field3, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -95,7 +97,7 @@ describe('OneboxFieldParser test suite', () => { }); it('should return a DynamicOneboxModel object when selectableMetadata has authority', () => { - const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -124,7 +126,7 @@ describe('OneboxFieldParser test suite', () => { languageCodes: [] } as FormFieldModel; - parser = new OneboxFieldParser(submissionId, regexField, initFormValues, parserOptions); + parser = new OneboxFieldParser(submissionId, regexField, initFormValues, parserOptions, translateService); fieldModel = parser.parse(); }); diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts index 7d48ad2d002..8ae0ccfedf3 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts @@ -3,10 +3,12 @@ import { RelationGroupFieldParser } from './relation-group-field-parser'; import { DynamicRelationGroupModel } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('RelationGroupFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -73,13 +75,13 @@ describe('RelationGroupFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof RelationGroupFieldParser).toBe(true); }); it('should return a DynamicRelationGroupModel object', () => { - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -88,7 +90,7 @@ describe('RelationGroupFieldParser test suite', () => { it('should throw when rows configuration is empty', () => { field.rows = null; - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(() => parser.parse()) .toThrow(); @@ -99,7 +101,7 @@ describe('RelationGroupFieldParser test suite', () => { author: [new FormFieldMetadataValueObject('test author')], affiliation: [new FormFieldMetadataValueObject('test affiliation')] }; - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); const expectedValue = [{ diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index 1f9bde8a7fb..fca16b28e35 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -3,6 +3,10 @@ import { RowParser } from './row-parser'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { DynamicRowArrayModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { FormRowModel } from '../../../../core/config/models/config-submission-form.model'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; +import { TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { Injector } from '@angular/core'; describe('RowParser test suite', () => { @@ -16,6 +20,7 @@ describe('RowParser test suite', () => { let row8: FormRowModel; let row9: FormRowModel; let row10: FormRowModel; + let injector: Injector; const submissionId = '1234'; const scopeUUID = 'testScopeUUID'; @@ -25,6 +30,12 @@ describe('RowParser test suite', () => { const typeField = 'dc_type'; beforeEach(() => { + let translateService = getMockTranslateService(); + injector = Injector.create({ + providers: [ + { provide: TranslateService, useValue: translateService }, + ], + }); row1 = { fields: [ { @@ -330,14 +341,14 @@ describe('RowParser test suite', () => { }); it('should init parser properly', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); expect(parser instanceof RowParser).toBe(true); }); describe('parse', () => { it('should return a DynamicRowGroupModel object', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -345,7 +356,7 @@ describe('RowParser test suite', () => { }); it('should return a row with three fields', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -353,7 +364,7 @@ describe('RowParser test suite', () => { }); it('should return a DynamicRowArrayModel object', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -361,7 +372,7 @@ describe('RowParser test suite', () => { }); it('should return a row that contains only scoped fields', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -369,7 +380,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a dropdown combo field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -377,7 +388,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a lookup-name field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -385,7 +396,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a list field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -393,7 +404,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a date field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -401,7 +412,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a tag field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -409,7 +420,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a textarea field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -417,7 +428,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a group field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly, typeField); diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index 0761cfe60e2..0ce50081e40 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -3,10 +3,12 @@ import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-conc import { SeriesFieldParser } from './series-field-parser'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('SeriesFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -34,13 +36,13 @@ describe('SeriesFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof SeriesFieldParser).toBe(true); }); it('should return a DynamicConcatModel object when repeatable option is false', () => { - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -48,7 +50,7 @@ describe('SeriesFieldParser test suite', () => { }); it('should return a DynamicConcatModel object with the correct separator', () => { - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -61,7 +63,7 @@ describe('SeriesFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test; series', undefined, undefined, 'test'); - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts index 115829f8d38..ae10dbd3861 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts @@ -3,10 +3,12 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu import { TagFieldParser } from './tag-field-parser'; import { DynamicTagModel } from '../ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('TagFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -38,13 +40,13 @@ describe('TagFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof TagFieldParser).toBe(true); }); it('should return a DynamicTagModel object when repeatable option is false', () => { - const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -59,7 +61,7 @@ describe('TagFieldParser test suite', () => { ], }; - const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index 855e464f21d..259f8a60e14 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -3,10 +3,12 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu import { TextareaFieldParser } from './textarea-field-parser'; import { DsDynamicTextAreaModel } from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { ParserOptions } from './parser-options'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('TextareaFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -36,13 +38,13 @@ describe('TextareaFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof TextareaFieldParser).toBe(true); }); it('should return a DsDynamicTextAreaModel object when repeatable option is false', () => { - const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -57,7 +59,7 @@ describe('TextareaFieldParser test suite', () => { }; const expectedValue = 'test description'; - const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); From b5dbaada495cdafd712d3568197d9e8ca53fd230 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 15 Nov 2023 13:42:53 +0100 Subject: [PATCH 03/50] Remove unnecessary import --- src/app/shared/form/builder/parsers/row-parser.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index fca16b28e35..f414715f5be 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -4,7 +4,6 @@ import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-ro import { DynamicRowArrayModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { FormRowModel } from '../../../../core/config/models/config-submission-form.model'; import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; -import { TestBed } from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; import { Injector } from '@angular/core'; From 8de483a3088669070bd31435e8bc394342a63b97 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 6 Dec 2023 09:57:43 +0100 Subject: [PATCH 04/50] enable type-bind for checkbox inputs during submission --- .../models/list/dynamic-list-radio-group.model.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts index 0a32498173e..2bcf8b5f139 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts @@ -1,5 +1,6 @@ import { DynamicFormControlLayout, + DynamicFormControlRelation, DynamicRadioGroupModel, DynamicRadioGroupModelConfig, serializable @@ -15,12 +16,14 @@ export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig { @serializable() vocabularyOptions: VocabularyOptions; @serializable() repeatable: boolean; + @serializable() typeBindRelations: DynamicFormControlRelation[]; @serializable() groupLength: number; @serializable() required: boolean; @serializable() hint: string; @@ -35,6 +38,7 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { this.required = config.required; this.hint = config.hint; this.value = config.value; + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; } get hasAuthority(): boolean { From dbf2964160ea698ae768215b9b77b7a42df06cce Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 13 Mar 2024 15:11:55 +0100 Subject: [PATCH 05/50] Fix linting issues --- .../form/builder/form-builder.service.spec.ts | 13 +++---------- .../form/builder/parsers/concat-field-parser.ts | 5 ++--- .../form/builder/parsers/date-field-parser.spec.ts | 3 ++- .../builder/parsers/disabled-field-parser.spec.ts | 4 ++-- .../builder/parsers/dropdown-field-parser.spec.ts | 3 ++- .../form/builder/parsers/dropdown-field-parser.ts | 4 ++-- src/app/shared/form/builder/parsers/field-parser.ts | 6 ++---- .../form/builder/parsers/list-field-parser.spec.ts | 3 ++- .../builder/parsers/lookup-field-parser.spec.ts | 3 ++- .../parsers/lookup-name-field-parser.spec.ts | 3 ++- .../form/builder/parsers/name-field-parser.spec.ts | 3 ++- .../form/builder/parsers/name-field-parser.ts | 4 ++-- .../builder/parsers/onebox-field-parser.spec.ts | 3 ++- .../shared/form/builder/parsers/parser-factory.ts | 4 ++-- .../parsers/relation-group-field-parser.spec.ts | 3 ++- .../shared/form/builder/parsers/row-parser.spec.ts | 7 ++++--- .../builder/parsers/series-field-parser.spec.ts | 3 ++- .../form/builder/parsers/series-field-parser.ts | 4 ++-- .../form/builder/parsers/tag-field-parser.spec.ts | 3 ++- .../builder/parsers/textarea-field-parser.spec.ts | 3 ++- 20 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index b0050877c6d..1e481391915 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -31,12 +31,14 @@ import { DynamicTextAreaModel, DynamicTimePickerModel, } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { getMockTranslateService } from '../../mocks/translate.service.mock'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { DynamicDsDatePickerModel } from './ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; @@ -55,15 +57,6 @@ import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.mod import { FormBuilderService } from './form-builder.service'; import { FormFieldModel } from './models/form-field.model'; import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model'; -import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; -import { DynamicLookupNameModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model'; -import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; -import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; -import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; -import {createSuccessfulRemoteDataObject$} from '../../remote-data.utils'; -import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; -import { getMockTranslateService } from '../../mocks/translate.service.mock'; -import { TranslateService } from '@ngx-translate/core'; describe('FormBuilderService test suite', () => { @@ -104,7 +97,7 @@ describe('FormBuilderService test suite', () => { { provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true }, { provide: ConfigurationDataService, useValue: configSpy }, { provide: TranslateService, useValue: translateService }, - ] + ], }); const vocabularyOptions: VocabularyOptions = { diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 0ccb529167b..67c0255d12f 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -1,4 +1,5 @@ import { Inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { hasNoValue, @@ -25,8 +26,6 @@ import { PARSER_OPTIONS, SUBMISSION_ID, } from './field-parser'; -import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; -import { TranslateService } from '@ngx-translate/core'; import { ParserOptions } from './parser-options'; export class ConcatFieldParser extends FieldParser { @@ -36,7 +35,7 @@ export class ConcatFieldParser extends FieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, - translate: TranslateService, + translate: TranslateService, protected separator: string, protected firstPlaceholder: string = null, protected secondPlaceholder: string = null) { diff --git a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts index 74129915112..ec2172523fe 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts @@ -1,9 +1,10 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { DateFieldParser } from './date-field-parser'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts index 74aa1209e87..759f357f282 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -1,8 +1,8 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model'; import { FormFieldModel } from '../models/form-field.model'; import { DisabledFieldParser } from './disabled-field-parser'; -import { DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; import { ParserOptions } from './parser-options'; describe('DisabledFieldParser test suite', () => { diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts index e38c05389d7..c0c3daa3044 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts @@ -1,8 +1,9 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicScrollableDropdownModel } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { FormFieldModel } from '../models/form-field.model'; import { DropdownFieldParser } from './dropdown-field-parser'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('DropdownFieldParser test suite', () => { let field: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 38cadc44500..fee36bec92c 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -1,5 +1,6 @@ import { Inject } from '@angular/core'; import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; import { isNotEmpty } from '../../../empty.util'; import { @@ -16,7 +17,6 @@ import { SUBMISSION_ID, } from './field-parser'; import { ParserOptions } from './parser-options'; -import { TranslateService } from '@ngx-translate/core'; export class DropdownFieldParser extends FieldParser { @@ -25,7 +25,7 @@ export class DropdownFieldParser extends FieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, - translate: TranslateService + translate: TranslateService, ) { super(submissionId, configData, initFormValues, parserOptions, translate); } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 99d646c8a0c..f7608131468 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -8,6 +8,7 @@ import { MATCH_VISIBLE, OR_OPERATOR, } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; import uniqueId from 'lodash/uniqueId'; import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; @@ -35,9 +36,6 @@ import { VisibilityType } from './../../../../submission/sections/visibility-typ import { setLayout } from './parser.utils'; import { ParserOptions } from './parser-options'; import { ParserType } from './parser-type'; -import { isNgbDateStruct } from '../../../date.util'; -import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; -import { TranslateService } from '@ngx-translate/core'; export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); @@ -64,7 +62,7 @@ export abstract class FieldParser { @Inject(CONFIG_DATA) protected configData: FormFieldModel, @Inject(INIT_FORM_VALUES) protected initFormValues: any, @Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions, - protected translate: TranslateService + protected translate: TranslateService, ) { } diff --git a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts index 194e35b39fc..646fee9dec0 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts @@ -1,10 +1,11 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ListFieldParser } from './list-field-parser'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('ListFieldParser test suite', () => { let field: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts index 3d44d29d36c..2ed45803edc 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts @@ -1,9 +1,10 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicLookupModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { LookupFieldParser } from './lookup-field-parser'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('LookupFieldParser test suite', () => { let field: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts index c15e62e698b..3384071f6ee 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts @@ -1,9 +1,10 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicLookupNameModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { LookupNameFieldParser } from './lookup-name-field-parser'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('LookupNameFieldParser test suite', () => { let field: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 772b717fc01..80b3f9ee83a 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -1,9 +1,10 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { NameFieldParser } from './name-field-parser'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('NameFieldParser test suite', () => { let field1: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/name-field-parser.ts b/src/app/shared/form/builder/parsers/name-field-parser.ts index 62ea4b5b741..4bb36ad4d03 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.ts @@ -1,6 +1,6 @@ import { Inject } from '@angular/core'; - import { TranslateService } from '@ngx-translate/core'; + import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; import { @@ -18,7 +18,7 @@ export class NameFieldParser extends ConcatFieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, - translate: TranslateService + translate: TranslateService, ) { super(submissionId, configData, initFormValues, parserOptions, translate, ',', 'form.last-name', 'form.first-name'); } diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index ece9efa78d6..236f7ed0ee9 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -1,9 +1,10 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DynamicQualdropModel } from '../ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; import { FormFieldModel } from '../models/form-field.model'; import { FieldParser } from './field-parser'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('OneboxFieldParser test suite', () => { let field1: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/parser-factory.ts b/src/app/shared/form/builder/parsers/parser-factory.ts index 1be9662c312..a56a27d77f7 100644 --- a/src/app/shared/form/builder/parsers/parser-factory.ts +++ b/src/app/shared/form/builder/parsers/parser-factory.ts @@ -1,4 +1,5 @@ import { StaticProvider } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { DateFieldParser } from './date-field-parser'; import { DisabledFieldParser } from './disabled-field-parser'; @@ -20,14 +21,13 @@ import { RelationGroupFieldParser } from './relation-group-field-parser'; import { SeriesFieldParser } from './series-field-parser'; import { TagFieldParser } from './tag-field-parser'; import { TextareaFieldParser } from './textarea-field-parser'; -import { TranslateService } from '@ngx-translate/core'; const fieldParserDeps = [ SUBMISSION_ID, CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, - TranslateService + TranslateService, ]; /** diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts index c80025a81ac..9eeeeea35c3 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts @@ -1,9 +1,10 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicRelationGroupModel } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; import { RelationGroupFieldParser } from './relation-group-field-parser'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('RelationGroupFieldParser test suite', () => { let field: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index f74787aa167..d87931a4880 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -1,11 +1,12 @@ +import { Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { FormRowModel } from '../../../../core/config/models/config-submission-form.model'; import { DynamicRowArrayModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { FormFieldModel } from '../models/form-field.model'; import { RowParser } from './row-parser'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; -import { TranslateService } from '@ngx-translate/core'; -import { Injector } from '@angular/core'; describe('RowParser test suite', () => { diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index eeeb74cef84..8141bf23e06 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -1,8 +1,9 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; import { SeriesFieldParser } from './series-field-parser'; describe('SeriesFieldParser test suite', () => { diff --git a/src/app/shared/form/builder/parsers/series-field-parser.ts b/src/app/shared/form/builder/parsers/series-field-parser.ts index 365ced69dec..196718ab87c 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.ts @@ -1,6 +1,6 @@ import { Inject } from '@angular/core'; - import { TranslateService } from '@ngx-translate/core'; + import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; import { @@ -18,7 +18,7 @@ export class SeriesFieldParser extends ConcatFieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, - translate: TranslateService + translate: TranslateService, ) { super(submissionId, configData, initFormValues, parserOptions, translate, ';'); } diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts index a3863f55570..303d63cad5d 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts @@ -1,8 +1,9 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicTagModel } from '../ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; import { TagFieldParser } from './tag-field-parser'; describe('TagFieldParser test suite', () => { diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index 08a1cf71ba9..c5e33b51ae4 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -1,8 +1,9 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DsDynamicTextAreaModel } from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; -import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; describe('TextareaFieldParser test suite', () => { let field: FormFieldModel; From 6aacc1232cc0c63b5ccbf19f27e08b0bc82db81a Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 13 Mar 2024 15:17:22 +0100 Subject: [PATCH 06/50] Fix spec imports --- src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts | 2 ++ .../shared/form/builder/parsers/textarea-field-parser.spec.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index 236f7ed0ee9..b2a50395e1a 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -5,6 +5,8 @@ import { DynamicQualdropModel } from '../ds-dynamic-form-ui/models/ds-dynamic-qu import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; import { FormFieldModel } from '../models/form-field.model'; import { FieldParser } from './field-parser'; +import { OneboxFieldParser } from './onebox-field-parser'; +import { ParserOptions } from './parser-options'; describe('OneboxFieldParser test suite', () => { let field1: FormFieldModel; diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index c5e33b51ae4..cfab7c36e31 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -4,6 +4,7 @@ import { DsDynamicTextAreaModel } from '../ds-dynamic-form-ui/models/ds-dynamic- import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { ParserOptions } from './parser-options'; +import { TextareaFieldParser } from './textarea-field-parser'; describe('TextareaFieldParser test suite', () => { let field: FormFieldModel; From 3937be13f2e2cc3a7fe8f135983784f2070335f2 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 10:00:10 +0100 Subject: [PATCH 07/50] Custom ESLint rules to enforce new ThemedComponent selector convention The following cases are covered: - ThemedComponent wrapper selectors must not start with ds-themed- - Base component selectors must start with ds-base- - Themed component selectors must start with ds-themed- - The ThemedComponent wrapper must always be used in HTML - The ThemedComponent wrapper must be used in TypeScript _where appropriate_: - Required - Explicit usages (e.g. modal instantiation, routing modules, ...) - By.css selector queries (in order to align with the HTML rule) - Unchecked - Non-routing modules (to ensure the components can be declared) - ViewChild hooks (since they need to attach to the underlying component) All rules work with --fix to automatically migrate to the new convention This covers most of the codebase, but minor manual adjustment are needed afterwards --- .eslintrc.json | 22 +- .github/workflows/build.yml | 8 +- lint/.gitignore | 4 + lint/README.md | 31 +++ lint/dist/src/rules/html/package.json | 6 + lint/dist/src/rules/ts/package.json | 6 + lint/jasmine.json | 7 + lint/src/rules/html/index.ts | 16 ++ .../src/rules/html/themed-component-usages.ts | 56 +++++ lint/src/rules/ts/index.ts | 9 + .../rules/ts/themed-component-selectors.ts | 92 +++++++++ lint/src/rules/ts/themed-component-usages.ts | 132 ++++++++++++ lint/src/util/angular.ts | 16 ++ lint/src/util/misc.ts | 42 ++++ lint/src/util/theme-support.ts | 192 ++++++++++++++++++ lint/test/fixture/README.md | 9 + .../src/app/test/test-routing.module.ts | 14 ++ .../src/app/test/test-themeable.component.ts | 15 ++ .../src/app/test/test.component.spec.ts | 8 + .../fixture/src/app/test/test.component.ts | 15 ++ lint/test/fixture/src/app/test/test.module.ts | 23 +++ .../test/themed-test-themeable.component.ts | 28 +++ lint/test/fixture/src/test.ts | 0 .../test/app/test/test-themeable.component.ts | 17 ++ .../fixture/src/themes/test/test.module.ts | 19 ++ lint/test/fixture/tsconfig.json | 7 + lint/test/helpers.js | 13 ++ .../rules/themed-component-selectors.spec.ts | 140 +++++++++++++ .../rules/themed-component-usages.spec.ts | 190 +++++++++++++++++ lint/test/testing.ts | 52 +++++ lint/test/util/theme-support.spec.ts | 24 +++ lint/tsconfig.json | 23 +++ package.json | 13 +- tsconfig.json | 3 +- yarn.lock | 134 +++++++++--- 35 files changed, 1352 insertions(+), 34 deletions(-) create mode 100644 lint/.gitignore create mode 100644 lint/README.md create mode 100644 lint/dist/src/rules/html/package.json create mode 100644 lint/dist/src/rules/ts/package.json create mode 100644 lint/jasmine.json create mode 100644 lint/src/rules/html/index.ts create mode 100644 lint/src/rules/html/themed-component-usages.ts create mode 100644 lint/src/rules/ts/index.ts create mode 100644 lint/src/rules/ts/themed-component-selectors.ts create mode 100644 lint/src/rules/ts/themed-component-usages.ts create mode 100644 lint/src/util/angular.ts create mode 100644 lint/src/util/misc.ts create mode 100644 lint/src/util/theme-support.ts create mode 100644 lint/test/fixture/README.md create mode 100644 lint/test/fixture/src/app/test/test-routing.module.ts create mode 100644 lint/test/fixture/src/app/test/test-themeable.component.ts create mode 100644 lint/test/fixture/src/app/test/test.component.spec.ts create mode 100644 lint/test/fixture/src/app/test/test.component.ts create mode 100644 lint/test/fixture/src/app/test/test.module.ts create mode 100644 lint/test/fixture/src/app/test/themed-test-themeable.component.ts create mode 100644 lint/test/fixture/src/test.ts create mode 100644 lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts create mode 100644 lint/test/fixture/src/themes/test/test.module.ts create mode 100644 lint/test/fixture/tsconfig.json create mode 100644 lint/test/helpers.js create mode 100644 lint/test/rules/themed-component-selectors.spec.ts create mode 100644 lint/test/rules/themed-component-usages.spec.ts create mode 100644 lint/test/testing.ts create mode 100644 lint/test/util/theme-support.spec.ts create mode 100644 lint/tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 50a9be3d59b..a18f5873b4d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,7 +11,10 @@ "eslint-plugin-jsonc", "eslint-plugin-rxjs", "eslint-plugin-simple-import-sort", - "eslint-plugin-import-newlines" + "eslint-plugin-import-newlines", + "eslint-plugin-jsonc", + "dspace-angular-ts", + "dspace-angular-html" ], "overrides": [ { @@ -238,7 +241,11 @@ "method" ], - "rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases + "rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-selectors": "error", + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -253,7 +260,10 @@ "createDefaultProgram": true }, "rules": { - "prefer-const": "off" + "prefer-const": "off", + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -262,7 +272,11 @@ ], "extends": [ "plugin:@angular-eslint/template/recommended" - ] + ], + "rules": { + // Custom DSpace Angular rules + "dspace-angular-html/themed-component-usages": "error" + } }, { "files": [ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f20470a3c..e7d0e46f661 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,8 +85,14 @@ jobs: - name: Install Yarn dependencies run: yarn install --frozen-lockfile + - name: Build lint plugins + run: yarn run build:lint + + - name: Run lint plugin tests + run: yarn run test:lint:nobuild + - name: Run lint - run: yarn run lint --quiet + run: yarn run lint:nobuild --quiet - name: Check for circular dependencies run: yarn run check-circ-deps diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 00000000000..6b6bf3270b8 --- /dev/null +++ b/lint/.gitignore @@ -0,0 +1,4 @@ +/dist/ +/coverage/ +/node-modules/ +/docs/ diff --git a/lint/README.md b/lint/README.md new file mode 100644 index 00000000000..5fff29b1b23 --- /dev/null +++ b/lint/README.md @@ -0,0 +1,31 @@ +# ESLint plugins + +Custom ESLint rules for DSpace Angular peculiarities. + +## Overview + +- Different file types must be handled by separate plugins. We support: + - [TypeScript](./src/ts) + - [HTML](./src/html) +- All rules are written in TypeScript and compiled into [`dist`](./dist) + - The plugins are linked into the main project dependencies from here + - These directories already contain the necessary `package.json` files to mark them as ESLint plugins +- The plugins are declared in [`.eslintrc.json`](../.eslintrc.json). Individual rules can be configured or disabled there, like usual. +- Some useful links + - [Developing ESLint plugins](https://eslint.org/docs/latest/extend/plugins) + - [Custom rules in typescript-eslint](https://typescript-eslint.io/developers/custom-rules) + - [Angular ESLint](https://github.com/angular-eslint/angular-eslint) + +## Parsing project metadata in advance ~ TypeScript AST + +While it is possible to retain persistent state between files during the linting process, it becomes quite complicated if the content of one file determines how we want to lint another file. +Because the two files may be linted out of order, we may not know whether the first file is wrong before we pass by the second. This means that we cannot report or fix the issue, because the first file is already detached from the linting context. + +For example, we cannot consistently determine which components are themeable (i.e. have a `ThemedComponent` wrapper) while linting. +To work around this issue, we construct a registry of themeable components _before_ linting anything. +- We don't have a good way to hook into the ESLint parser at this time +- Instead, we leverage the actual TypeScript AST parser + - Retrieve all `ThemedComponent` wrapper files by the pattern of their path (`themed-*.component.ts`) + - Determine the themed component they're linked to (by the actual type annotation/import path, since filenames are prone to errors) + - Store metadata describing these component pairs in a global registry that can be shared between rules +- This only needs to happen once, and only takes a fraction of a second (for ~100 themeable components) \ No newline at end of file diff --git a/lint/dist/src/rules/html/package.json b/lint/dist/src/rules/html/package.json new file mode 100644 index 00000000000..d3f310d23b9 --- /dev/null +++ b/lint/dist/src/rules/html/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-dspace-angular-html", + "version": "0.0.0", + "main": "./index.js", + "private": true +} diff --git a/lint/dist/src/rules/ts/package.json b/lint/dist/src/rules/ts/package.json new file mode 100644 index 00000000000..f19e18756ac --- /dev/null +++ b/lint/dist/src/rules/ts/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-dspace-angular-ts", + "version": "0.0.0", + "main": "./index.js", + "private": true +} diff --git a/lint/jasmine.json b/lint/jasmine.json new file mode 100644 index 00000000000..dfacd41a96c --- /dev/null +++ b/lint/jasmine.json @@ -0,0 +1,7 @@ +{ + "spec_files": ["**/*.spec.js"], + "spec_dir": "lint/dist/test", + "helpers": [ + "./test/helpers.js" + ] +} diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts new file mode 100644 index 00000000000..ef0b7a87edb --- /dev/null +++ b/lint/src/rules/html/index.ts @@ -0,0 +1,16 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import themedComponentUsages from './themed-component-usages'; + +export = { + rules: { + 'themed-component-usages': themedComponentUsages, + }, + parser: require('@angular-eslint/template-parser'), +}; diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts new file mode 100644 index 00000000000..6184805a2b1 --- /dev/null +++ b/lint/src/rules/html/themed-component-usages.ts @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + DISALLOWED_THEME_SELECTORS, + fixSelectors, +} from '../../util/theme-support'; + +export default { + meta: { + type: 'problem', + fixable: 'code', + schema: [], + messages: { + mustUseThemedWrapperSelector: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', + } + }, + create(context: any) { + return { + [`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: any) { + context.report({ + messageId: 'mustUseThemedWrapperSelector', + node, + fix(fixer: any) { + const oldSelector = node.name; + const newSelector = fixSelectors(oldSelector); + + const openTagRange = [ + node.startSourceSpan.start.offset + 1, + node.startSourceSpan.start.offset + 1 + oldSelector.length + ]; + + const ops = [ + fixer.replaceTextRange(openTagRange, newSelector), + ]; + + // make sure we don't mangle self-closing tags + if (node.startSourceSpan.end.offset !== node.endSourceSpan.end.offset) { + const closeTagRange = [ + node.endSourceSpan.start.offset + 2, + node.endSourceSpan.end.offset - 1 + ]; + ops.push(fixer.replaceTextRange(closeTagRange, newSelector)); + } + + return ops; + } + }); + }, + }; + } +}; diff --git a/lint/src/rules/ts/index.ts b/lint/src/rules/ts/index.ts new file mode 100644 index 00000000000..b33135d7b0f --- /dev/null +++ b/lint/src/rules/ts/index.ts @@ -0,0 +1,9 @@ +import themedComponentSelectors from './themed-component-selectors'; +import themedComponentUsages from './themed-component-usages'; + +export = { + rules: { + 'themed-component-selectors': themedComponentSelectors, + 'themed-component-usages': themedComponentUsages, + }, +}; diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts new file mode 100644 index 00000000000..e150bb41a84 --- /dev/null +++ b/lint/src/rules/ts/themed-component-selectors.ts @@ -0,0 +1,92 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { ESLintUtils } from '@typescript-eslint/utils'; +import { getComponentSelectorNode } from '../../util/angular'; +import { stringLiteral } from '../../util/misc'; +import { + inThemedComponentOverrideFile, + isThemeableComponent, + isThemedComponentWrapper, +} from '../../util/theme-support'; + +export default ESLintUtils.RuleCreator.withoutDocs({ + meta: { + type: 'problem', + schema: [], + fixable: 'code', + messages: { + wrongSelectorUnthemedComponent: 'Unthemed version of themeable components should have a selector starting with \'ds-base-\'', + wrongSelectorThemedComponentWrapper: 'Themed component wrapper of themeable components shouldn\'t have a selector starting with \'ds-themed-\'', + wrongSelectorThemedComponentOverride: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'', + } + }, + defaultOptions: [], + create(context: any): any { + if (context.getFilename()?.endsWith('.spec.ts')) { + return {}; + } + + function enforceWrapperSelector(selectorNode: any) { + if (selectorNode?.value.startsWith('ds-themed-')) { + context.report({ + messageId: 'wrongSelectorThemedComponentWrapper', + node: selectorNode, + fix(fixer: any) { + return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-'))); + }, + }); + } + } + + function enforceBaseSelector(selectorNode: any) { + if (!selectorNode?.value.startsWith('ds-base-')) { + context.report({ + messageId: 'wrongSelectorUnthemedComponent', + node: selectorNode, + fix(fixer: any) { + return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-'))); + }, + }); + } + } + + function enforceThemedSelector(selectorNode: any) { + if (!selectorNode?.value.startsWith('ds-themed-')) { + context.report({ + messageId: 'wrongSelectorThemedComponentOverride', + node: selectorNode, + fix(fixer: any) { + return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-'))); + }, + }); + } + } + + return { + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: any) { + // keep track of all @Component nodes by their selector + const selectorNode = getComponentSelectorNode(node); + const selector = selectorNode?.value; + const classNode = node.parent; + const className = classNode.id?.name; + + if (selector === undefined || className === undefined) { + return; + } + + if (isThemedComponentWrapper(node)) { + enforceWrapperSelector(selectorNode); + } else if (inThemedComponentOverrideFile(context)) { + enforceThemedSelector(selectorNode); + } else if (isThemeableComponent(className)) { + enforceBaseSelector(selectorNode); + } + } + }; + } +}); diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts new file mode 100644 index 00000000000..5934eb5e2e6 --- /dev/null +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -0,0 +1,132 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { ESLintUtils } from '@typescript-eslint/utils'; +import { findUsages } from '../../util/misc'; +import { + allThemeableComponents, + DISALLOWED_THEME_SELECTORS, + fixSelectors, + getThemeableComponentByBaseClass, + inThemedComponentFile, + isAllowedUnthemedUsage, +} from '../../util/theme-support'; + +export default ESLintUtils.RuleCreator.withoutDocs({ + meta: { + type: 'problem', + schema: [], + fixable: 'code', + messages: { + mustUseThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper', + mustImportThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper', + }, + }, + defaultOptions: [], + create(context: any, options: any): any { + function handleUnthemedUsagesInTypescript(node: any) { + if (isAllowedUnthemedUsage(node)) { + return; + } + + const entry = getThemeableComponentByBaseClass(node.name); + + if (entry === undefined) { + // this should never happen + throw new Error(`No such themeable component in registry: '${node.name}'`); + } + + context.report({ + messageId: 'mustUseThemedWrapper', + node: node, + fix(fixer: any) { + return fixer.replaceText(node, entry.wrapperClass); + }, + }); + } + + function handleThemedSelectorQueriesInTests(node: any) { + + } + + function handleUnthemedImportsInTypescript(specifierNode: any) { + const allUsages = findUsages(context, specifierNode.local); + const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage)); + + if (badUsages.length === 0) { + return; + } + + const importedNode = specifierNode.imported; + const declarationNode = specifierNode.parent; + + const entry = getThemeableComponentByBaseClass(importedNode.name); + if (entry === undefined) { + // this should never happen + throw new Error(`No such themeable component in registry: '${importedNode.name}'`); + } + + context.report({ + messageId: 'mustImportThemedWrapper', + node: importedNode, + fix(fixer: any) { + const ops = []; + + const oldImportSource = declarationNode.source.value; + const newImportLine = `import { ${entry.wrapperClass} } from '${oldImportSource.replace(entry.baseFileName, entry.wrapperFileName)}';`; + + if (declarationNode.specifiers.length === 1) { + if (allUsages.length === badUsages.length) { + ops.push(fixer.replaceText(declarationNode, newImportLine)); + } else { + ops.push(fixer.insertTextAfter(declarationNode, newImportLine)); + } + } else { + ops.push(fixer.replaceText(specifierNode, entry.wrapperClass)); + ops.push(fixer.insertTextAfter(declarationNode, newImportLine)); + } + + return ops; + }, + }); + } + + // ignore tests and non-routing modules + if (context.getFilename()?.endsWith('.spec.ts')) { + return { + [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { + context.report({ + node, + messageId: 'mustUseThemedWrapper', + fix(fixer: any){ + const newSelector = fixSelectors(node.raw); + return fixer.replaceText(node, newSelector); + } + }); + }, + }; + } else if ( + context.getFilename()?.match(/(?!routing).module.ts$/) + || context.getFilename()?.match(/themed-.+\.component\.ts$/) + || inThemedComponentFile(context) + ) { + // do nothing + return {}; + } else { + return allThemeableComponents().reduce( + (rules, entry) => { + return { + ...rules, + [`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript, + [`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript, + }; + }, {}, + ); + } + + }, +}); diff --git a/lint/src/util/angular.ts b/lint/src/util/angular.ts new file mode 100644 index 00000000000..cb122a16dcc --- /dev/null +++ b/lint/src/util/angular.ts @@ -0,0 +1,16 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +export function getComponentSelectorNode(componentDecoratorNode: any): any | undefined { + for (const property of componentDecoratorNode.expression.arguments[0].properties) { + if (property.key?.name === 'selector') { + return property?.value; + } + } + return undefined; +} diff --git a/lint/src/util/misc.ts b/lint/src/util/misc.ts new file mode 100644 index 00000000000..1cd610fcd7e --- /dev/null +++ b/lint/src/util/misc.ts @@ -0,0 +1,42 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +export function stringLiteral(value: string): string { + return `'${value}'`; +} + +export function match(rangeA: number[], rangeB: number[]) { + return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1]; +} + +export function findUsages(context: any, localNode: any): any[] { + const ast = context.getSourceCode().ast; + + const usages: any[] = []; + + for (const token of ast.tokens) { + if (token.type === 'Identifier' && token.value === localNode.name && !match(token.range, localNode.range)) { + usages.push(context.getSourceCode().getNodeByRangeIndex(token.range[0])); + } + } + + return usages; +} + + +export function isPartOfTypeExpression(node: any): boolean { + return node.parent.type.startsWith('TSType'); +} + +export function isClassDeclaration(node: any): boolean { + return node.parent.type === 'ClassDeclaration'; +} + +export function isPartOfViewChild(node: any): boolean { + return node.parent?.callee?.name === 'ViewChild'; +} diff --git a/lint/src/util/theme-support.ts b/lint/src/util/theme-support.ts new file mode 100644 index 00000000000..bf7c265e2ea --- /dev/null +++ b/lint/src/util/theme-support.ts @@ -0,0 +1,192 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { readFileSync } from 'fs'; +import { basename } from 'path'; +import ts from 'typescript'; +import { + isClassDeclaration, + isPartOfTypeExpression, + isPartOfViewChild, +} from './misc'; + +const glob = require('glob'); + +/** + * Couples a themeable Component to its ThemedComponent wrapper + */ +export interface ThemeableComponentRegistryEntry { + basePath: string; + baseFileName: string, + baseClass: string; + + wrapperPath: string; + wrapperFileName: string, + wrapperClass: string; +} + +/** + * Listing of all themeable Components + */ +class ThemeableComponentRegistry { + public readonly entries: Set; + public readonly byBaseClass: Map; + public readonly byBasePath: Map; + public readonly byWrapperPath: Map; + + constructor() { + this.entries = new Set(); + this.byBaseClass = new Map(); + this.byBasePath = new Map(); + this.byWrapperPath = new Map(); + } + + public initialize(prefix = '') { + if (this.entries.size > 0) { + return; + } + + function registerWrapper(path: string) { + const source = getSource(path); + + function traverse(node: any) { + if (node.kind === ts.SyntaxKind.Decorator && node.expression.expression.escapedText === 'Component' && node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + const wrapperClass = node.parent.name.escapedText; + + for (const heritageClause of node.parent.heritageClauses) { + for (const type of heritageClause.types) { + if (type.expression.escapedText === 'ThemedComponent') { + const baseClass = type.typeArguments[0].typeName?.escapedText; + + ts.forEachChild(source, (topNode: any) => { + if (topNode.kind === ts.SyntaxKind.ImportDeclaration) { + for (const element of topNode.importClause.namedBindings.elements) { + if (element.name.escapedText === baseClass) { + const basePath = resolveLocalPath(topNode.moduleSpecifier.text, path); + + themeableComponents.add({ + baseClass, + basePath: basePath.replace(new RegExp(`^${prefix}`), ''), + baseFileName: basename(basePath).replace(/\.ts$/, ''), + wrapperClass, + wrapperPath: path.replace(new RegExp(`^${prefix}`), ''), + wrapperFileName: basename(path).replace(/\.ts$/, ''), + }); + } + } + } + }); + } + } + } + + return; + } else { + ts.forEachChild(node, traverse); + } + } + + traverse(source); + } + + const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found; + + for (const wrapper of wrappers) { + registerWrapper(wrapper); + } + } + + private add(entry: ThemeableComponentRegistryEntry) { + this.entries.add(entry); + this.byBaseClass.set(entry.baseClass, entry); + this.byBasePath.set(entry.basePath, entry); + this.byWrapperPath.set(entry.wrapperPath, entry); + } +} + +export const themeableComponents = new ThemeableComponentRegistry(); + +/** + * Construct the AST of a TypeScript source file + * @param file + */ +function getSource(file: string): ts.SourceFile { + return ts.createSourceFile( + file, + readFileSync(file).toString(), + ts.ScriptTarget.ES2020, // todo: actually use tsconfig.json? + /*setParentNodes */ true, + ); +} + +/** + * Resolve a possibly relative local path into an absolute path starting from the root directory of the project + */ +function resolveLocalPath(path: string, relativeTo: string) { + if (path.startsWith('src/')) { + return path; + } else if (path.startsWith('./')) { + const parts = relativeTo.split('/'); + return [ + ...parts.slice(0, parts.length - 1), + path.replace(/^.\//, '') + ].join('/') + '.ts'; + } else { + throw new Error(`Unsupported local path: ${path}`); + } +} + +export function isThemedComponentWrapper(node: any): boolean { + return node.parent.superClass?.name === 'ThemedComponent'; +} + +export function isThemeableComponent(className: string): boolean { + themeableComponents.initialize(); + return themeableComponents.byBaseClass.has(className); +} + +export function inThemedComponentOverrideFile(context: any): boolean { + const match = context.getFilename().match(/src\/themes\/[^\/]+\/(app\/.*)/); + + if (!match) { + return false; + } + themeableComponents.initialize(); + // todo: this is fragile! + return themeableComponents.byBasePath.has(`src/${match[1]}`); +} + +export function inThemedComponentFile(context: any): boolean { + themeableComponents.initialize(); + + return [ + () => themeableComponents.byBasePath.has(context.getFilename()), + () => themeableComponents.byWrapperPath.has(context.getFilename()), + () => inThemedComponentOverrideFile(context), + ].some(predicate => predicate()); +} + +export function allThemeableComponents(): ThemeableComponentRegistryEntry[] { + themeableComponents.initialize(); + return [...themeableComponents.entries]; +} + +export function getThemeableComponentByBaseClass(baseClass: string): ThemeableComponentRegistryEntry | undefined { + themeableComponents.initialize(); + return themeableComponents.byBaseClass.get(baseClass); +} + +export function isAllowedUnthemedUsage(usageNode: any) { + return isClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode); +} + +export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-'; + +export function fixSelectors(text: string): string { + return text.replaceAll(/ds-(base|themed)-/g, 'ds-'); +} diff --git a/lint/test/fixture/README.md b/lint/test/fixture/README.md new file mode 100644 index 00000000000..b19ae11b558 --- /dev/null +++ b/lint/test/fixture/README.md @@ -0,0 +1,9 @@ +# ESLint testing fixtures + +The files in this directory are used for the ESLint testing environment +- Some rules rely on registries that must be built up _before_ the rule is run + - In order to test these registries, the fixture sources contain a few dummy components +- The TypeScript ESLint test runner requires at least one dummy file to exist to run any tests + - By default, [`test.ts`](./src/test.ts) is used. Note that this file is empty; it's only there for the TypeScript configuration, the actual content is injected from the `code` property in the tests. + - To test rules that make assertions based on the path of the file, you'll need to include the `filename` property in the test configuration. Note that it must point to an existing file too! + - The `filename` must be provided as `fixture('src/something.ts')` \ No newline at end of file diff --git a/lint/test/fixture/src/app/test/test-routing.module.ts b/lint/test/fixture/src/app/test/test-routing.module.ts new file mode 100644 index 00000000000..d3a16bb6d6a --- /dev/null +++ b/lint/test/fixture/src/app/test/test-routing.module.ts @@ -0,0 +1,14 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { ThemedTestThemeableComponent } from './themed-test-themeable.component'; + +export const ROUTES = [ + { + component: ThemedTestThemeableComponent, + } +]; diff --git a/lint/test/fixture/src/app/test/test-themeable.component.ts b/lint/test/fixture/src/app/test/test-themeable.component.ts new file mode 100644 index 00000000000..bd731d8afae --- /dev/null +++ b/lint/test/fixture/src/app/test/test-themeable.component.ts @@ -0,0 +1,15 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-base-test-themeable', + template: '', +}) +export class TestThemeableComponent { +} diff --git a/lint/test/fixture/src/app/test/test.component.spec.ts b/lint/test/fixture/src/app/test/test.component.spec.ts new file mode 100644 index 00000000000..2300ac4a56f --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.spec.ts @@ -0,0 +1,8 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + diff --git a/lint/test/fixture/src/app/test/test.component.ts b/lint/test/fixture/src/app/test/test.component.ts new file mode 100644 index 00000000000..c01f104c989 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.ts @@ -0,0 +1,15 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-test', + template: '', +}) +export class TestComponent { +} diff --git a/lint/test/fixture/src/app/test/test.module.ts b/lint/test/fixture/src/app/test/test.module.ts new file mode 100644 index 00000000000..633ef492fb9 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.module.ts @@ -0,0 +1,23 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +// @ts-ignore +import { NgModule } from '@angular/core'; +import { TestThemeableComponent } from './test-themeable.component'; +import { TestComponent } from './test.component'; +import { ThemedTestThemeableComponent } from './themed-test-themeable.component'; + +@NgModule({ + declarations: [ + TestComponent, + TestThemeableComponent, + ThemedTestThemeableComponent, + ] +}) +export class TestModule { + +} diff --git a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts new file mode 100644 index 00000000000..81eb59d4183 --- /dev/null +++ b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../../../../../src/app/shared/theme-support/themed.component'; +import { TestThemeableComponent } from './test-themeable.component'; + +@Component({ + selector: 'ds-test-themeable', + template: '', +}) +export class ThemedTestThemeableComponent extends ThemedComponent { + protected getComponentName(): string { + return ''; + } + + protected importThemedComponent(themeName: string): Promise { + return Promise.resolve(undefined); + } + + protected importUnthemedComponent(): Promise { + return Promise.resolve(undefined); + } +} diff --git a/lint/test/fixture/src/test.ts b/lint/test/fixture/src/test.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts new file mode 100644 index 00000000000..05ba4e3d1bd --- /dev/null +++ b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts @@ -0,0 +1,17 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; +import { TestThemeableComponent as BaseComponent } from '../../../../app/test/test-themeable.component'; + +@Component({ + selector: 'ds-themed-test-themeable', + template: '', +}) +export class TestThemeableComponent extends BaseComponent { + +} diff --git a/lint/test/fixture/src/themes/test/test.module.ts b/lint/test/fixture/src/themes/test/test.module.ts new file mode 100644 index 00000000000..6d7601bd52c --- /dev/null +++ b/lint/test/fixture/src/themes/test/test.module.ts @@ -0,0 +1,19 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +// @ts-ignore +import { NgModule } from '@angular/core'; +import { TestThemeableComponent } from './app/test/test-themeable.component'; + +@NgModule({ + declarations: [ + TestThemeableComponent, + ] +}) +export class TestModule { + +} diff --git a/lint/test/fixture/tsconfig.json b/lint/test/fixture/tsconfig.json new file mode 100644 index 00000000000..1fd3745ec84 --- /dev/null +++ b/lint/test/fixture/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "include": [ + "src/**/*.ts" + ], + "exclude": ["dist"] +} diff --git a/lint/test/helpers.js b/lint/test/helpers.js new file mode 100644 index 00000000000..bd648d007f5 --- /dev/null +++ b/lint/test/helpers.js @@ -0,0 +1,13 @@ +const SpecReporter = require('jasmine-spec-reporter').SpecReporter; +const StacktraceOption = require('jasmine-spec-reporter').StacktraceOption; + +jasmine.getEnv().clearReporters(); // Clear default console reporter for those instead +jasmine.getEnv().addReporter(new SpecReporter({ + spec: { + displayErrorMessages: false, + }, + summary: { + displayFailed: true, + displayStacktrace: StacktraceOption.PRETTY, + }, +})); diff --git a/lint/test/rules/themed-component-selectors.spec.ts b/lint/test/rules/themed-component-selectors.spec.ts new file mode 100644 index 00000000000..2f2e9786c21 --- /dev/null +++ b/lint/test/rules/themed-component-selectors.spec.ts @@ -0,0 +1,140 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + + +import { + fixture, + tsRuleTester, +} from '../testing'; +import rule from '../../src/rules/ts/themed-component-selectors'; + +describe('themed-component-selectors', () => { + tsRuleTester.run('themed-component-selectors', rule as any, { + valid: [ + { + name: 'Regular non-themeable component selector', + code: ` + @Component({ + selector: 'ds-something', + }) + class Something { + } + `, + }, + { + name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-', + code: ` +@Component({ + selector: 'ds-base-something', +}) +class Something { +} + +@Component({ + selector: 'ds-something', +}) +class ThemedSomething extends ThemedComponent { +} + +@Component({ + selector: 'ds-themed-something', +}) +class OverrideSomething extends Something { +} + `, + }, + { + name: 'Other themed component wrappers should not interfere', + code: ` +@Component({ + selector: 'ds-something', +}) +class Something { +} + +@Component({ + selector: 'ds-something-else', +}) +class ThemedSomethingElse extends ThemedComponent { +} + `, + }, + ], + invalid: [ + { + name: 'Wrong selector for base component', + filename: fixture('src/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-something', +}) +class TestThemeableComponent { +} + `, + errors: [ + { + messageId: 'wrongSelectorUnthemedComponent', + }, + ], + output: ` +@Component({ + selector: 'ds-base-something', +}) +class TestThemeableComponent { +} + `, + }, + { + name: 'Wrong selector for wrapper component', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: 'wrongSelectorThemedComponentWrapper', + }, + ], + output: ` +@Component({ + selector: 'ds-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Wrong selector for theme override', + filename: fixture('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-something', +}) +class TestThememeableComponent extends BaseComponent { +} + `, + errors: [ + { + messageId: 'wrongSelectorThemedComponentOverride', + }, + ], + output: ` +@Component({ + selector: 'ds-themed-something', +}) +class TestThememeableComponent extends BaseComponent { +} + `, + }, + ], + } as any); +}); diff --git a/lint/test/rules/themed-component-usages.spec.ts b/lint/test/rules/themed-component-usages.spec.ts new file mode 100644 index 00000000000..2f5dbcec203 --- /dev/null +++ b/lint/test/rules/themed-component-usages.spec.ts @@ -0,0 +1,190 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { + fixture, + htmlRuleTester, + tsRuleTester, +} from '../testing'; +import tsRule from '../../src/rules/ts/themed-component-usages'; +import htmlRule from '../../src/rules/html/themed-component-usages'; + +describe('themed-component-usages (TypeScript)', () => { + tsRuleTester.run('themed-component-usages', tsRule as any, { + valid: [ + { + code: ` +const config = { + a: ThemedTestThemeableComponent, + b: ChipsComponent, +} + `, + }, + { + code: ` +export class TestThemeableComponent { +} + `, + }, + { + code: ` +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; + +export class ThemedAdminSidebarComponent extends ThemedComponent { +} + `, + }, + { + code: ` +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; + +export class Something { + @ViewChild(TestThemeableComponent) test: TestThemeableComponent; +} + `, + }, + { + name: fixture('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-themeable'); +By.Css('#test > ds-themeable > #nest'); + `, + }, + ], + invalid: [ + { + code: ` +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; +import { TestComponent } from '../test/test.component.ts'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, +} + `, + errors: [ + { + messageId: 'mustImportThemedWrapper', + }, + { + messageId: 'mustUseThemedWrapper', + }, + ], + output: ` +import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; +import { TestComponent } from '../test/test.component.ts'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, +} + ` + }, + { + filename: fixture('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-themed-themeable'); +By.css('#test > ds-themed-themeable > #nest'); + `, + errors: [ + { + messageId: 'mustUseThemedWrapper', + }, + { + messageId: 'mustUseThemedWrapper', + }, + ], + output: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + { + filename: fixture('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-base-themeable'); +By.css('#test > ds-base-themeable > #nest'); + `, + errors: [ + { + messageId: 'mustUseThemedWrapper', + }, + { + messageId: 'mustUseThemedWrapper', + }, + ], + output: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + ], + } as any); +}); + +describe('themed-component-usages (HTML)', () => { + htmlRuleTester.run('themed-component-usages', htmlRule, { + valid: [ + { + code: ` + + + + `, + }, + ], + invalid: [ + { + code: ` + + + + `, + errors: [ + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + ], + output: ` + + + + `, + }, + { + code: ` + + + + `, + errors: [ + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + { + messageId: 'mustUseThemedWrapperSelector', + }, + ], + output: ` + + + + `, + }, + ] + }); +}); diff --git a/lint/test/testing.ts b/lint/test/testing.ts new file mode 100644 index 00000000000..631d956b0b2 --- /dev/null +++ b/lint/test/testing.ts @@ -0,0 +1,52 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { RuleTester } from 'eslint'; +import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester'; +import { themeableComponents } from '../src/util/theme-support'; + +const FIXTURE = 'lint/test/fixture/'; + +// Register themed components from test fixture +themeableComponents.initialize(FIXTURE); + +TypeScriptRuleTester.itOnly = fit; + +export function fixture(path: string): string { + return FIXTURE + path; +} + +export const tsRuleTester = new TypeScriptRuleTester({ + parser: '@typescript-eslint/parser', + defaultFilenames: { + ts: fixture('src/test.ts'), + tsx: 'n/a', + }, + parserOptions: { + project: fixture('tsconfig.json'), + } +}); + +class HtmlRuleTester extends RuleTester { + run(name: string, rule: any, tests: { valid: any[], invalid: any[] }) { + super.run(name, rule, { + valid: tests.valid.map((test) => ({ + filename: fixture('test.html'), + ...test, + })), + invalid: tests.invalid.map((test) => ({ + filename: fixture('test.html'), + ...test, + })), + }); + } +} + +export const htmlRuleTester = new HtmlRuleTester({ + parser: require.resolve('@angular-eslint/template-parser'), +}); diff --git a/lint/test/util/theme-support.spec.ts b/lint/test/util/theme-support.spec.ts new file mode 100644 index 00000000000..52e63b4fed6 --- /dev/null +++ b/lint/test/util/theme-support.spec.ts @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { themeableComponents } from '../../src/util/theme-support'; + +describe('theme-support', () => { + describe('themeable component registry', () => { + it('should contain all themeable components from the fixture', () => { + expect(themeableComponents.entries.size).toBe(1); + expect(themeableComponents.byBasePath.size).toBe(1); + expect(themeableComponents.byWrapperPath.size).toBe(1); + expect(themeableComponents.byBaseClass.size).toBe(1); + + expect(themeableComponents.byBaseClass.get('TestThemeableComponent')).toBeTruthy(); + expect(themeableComponents.byBasePath.get('src/app/test/test-themeable.component.ts')).toBeTruthy(); + expect(themeableComponents.byWrapperPath.get('src/app/test/themed-test-themeable.component.ts')).toBeTruthy(); + }); + }); +}); diff --git a/lint/tsconfig.json b/lint/tsconfig.json new file mode 100644 index 00000000000..2c74bddb240 --- /dev/null +++ b/lint/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "noImplicitReturns": true, + "skipLibCheck": true, + "strict": true, + "outDir": "./dist", + "sourceMap": true, + "types": [ + "jasmine", + "node" + ] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts", + ], + "exclude": [ + "dist", + "test/fixture" + ] +} diff --git a/package.json b/package.json index c0a3843605e..b8f6402cfbd 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,15 @@ "build:stats": "ng build --stats-json", "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", + "build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json", "test": "ng test --source-map=true --watch=false --configuration test", "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", - "lint": "ng lint", - "lint-fix": "ng lint --fix=true", + "test:lint": "yarn build:lint && jasmine --config=lint/jasmine.json", + "test:lint:nobuild": "jasmine --config=lint/jasmine.json", + "lint": "yarn build:lint && ng lint", + "lint:nobuild": "ng lint", + "lint-fix": "yarn build:lint && ng lint --fix=true", "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", @@ -94,6 +98,8 @@ "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", "ejs": "^3.1.9", + "eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html", + "eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts", "express": "^4.18.2", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", @@ -160,6 +166,8 @@ "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.59.1", "@typescript-eslint/parser": "^5.59.1", + "@typescript-eslint/rule-tester": "^7.2.0", + "@typescript-eslint/utils": "^7.2.0", "axe-core": "^4.7.2", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", @@ -178,6 +186,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.1.7", + "jasmine": "^3.8.0", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", "karma": "^6.4.2", diff --git a/tsconfig.json b/tsconfig.json index afd00f8568c..66921afd47d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -55,6 +55,7 @@ } }, "exclude": [ - "cypress.config.ts" + "cypress.config.ts", + "lint" ] } diff --git a/yarn.lock b/yarn.lock index 849cbe2eda3..a137a12cbd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1836,7 +1836,7 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== @@ -2565,7 +2565,7 @@ "@types/jasmine@~3.6.0": version "3.6.11" - resolved "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.11.tgz#4b1d77aa9dfc757407cb9e277216d8e83553f09d" integrity sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ== "@types/js-cookie@2.2.6": @@ -2578,6 +2578,11 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -2671,6 +2676,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + "@types/serve-index@^1.9.1": version "1.9.1" resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" @@ -2767,6 +2777,17 @@ "@typescript-eslint/typescript-estree" "5.59.1" debug "^4.3.4" +"@typescript-eslint/rule-tester@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/rule-tester/-/rule-tester-7.2.0.tgz#ca72af90fc4d46f1c53a4fc1c28d95fe7a96e879" + integrity sha512-V/jxkkx+buBn9uM2QvdHzi1XzxBm2M+QpEORNZCRkq3vKhnZO2Sto1X0xaZ6vVbmHvOE+Zlkv7GO98PXvgGKVg== + dependencies: + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/utils" "7.2.0" + ajv "^6.10.0" + lodash.merge "4.6.2" + semver "^7.5.4" + "@typescript-eslint/scope-manager@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.2.tgz" @@ -2799,6 +2820,14 @@ "@typescript-eslint/types" "5.59.6" "@typescript-eslint/visitor-keys" "5.59.6" +"@typescript-eslint/scope-manager@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" + integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== + dependencies: + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" + "@typescript-eslint/type-utils@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz" @@ -2839,6 +2868,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b" integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA== +"@typescript-eslint/types@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" + integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== + "@typescript-eslint/typescript-estree@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.2.tgz" @@ -2891,6 +2925,20 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" + integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== + dependencies: + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/utils@5.48.2": version "5.48.2" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.2.tgz" @@ -2933,6 +2981,19 @@ eslint-scope "^5.1.1" semver "^7.3.7" +"@typescript-eslint/utils@7.2.0", "@typescript-eslint/utils@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.2.0.tgz#fc8164be2f2a7068debb4556881acddbf0b7ce2a" + integrity sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" + semver "^7.5.4" + "@typescript-eslint/utils@^5.57.0": version "5.58.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz" @@ -2979,6 +3040,14 @@ "@typescript-eslint/types" "5.59.6" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" + integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== + dependencies: + "@typescript-eslint/types" "7.2.0" + eslint-visitor-keys "^3.4.1" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" @@ -5454,6 +5523,14 @@ eslint-plugin-deprecation@^1.4.1: tslib "^2.3.1" tsutils "^3.21.0" +"eslint-plugin-dspace-angular-html@link:./lint/dist/src/rules/html": + version "0.0.0" + uid "" + +"eslint-plugin-dspace-angular-ts@link:./lint/dist/src/rules/ts": + version "0.0.0" + uid "" + eslint-plugin-import-newlines@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.3.1.tgz#e21705667778e8134382b50079fbb2c8d3a2fcde" @@ -5583,6 +5660,11 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4 resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz" integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@^8.39.0: version "8.39.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" @@ -7296,7 +7378,7 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" -jasmine-core@^3.6.0, jasmine-core@^3.8.0: +jasmine-core@^3.6.0, jasmine-core@^3.8.0, jasmine-core@~3.99.0: version "3.99.1" resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz" integrity sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg== @@ -7308,6 +7390,14 @@ jasmine-marbles@0.9.2: dependencies: lodash "^4.17.20" +jasmine@^3.8.0: + version "3.99.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.99.0.tgz#7cc7aeda7ade2d57694fc818a374f778cbb4ea62" + integrity sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw== + dependencies: + glob "^7.1.6" + jasmine-core "~3.99.0" + jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" @@ -7885,7 +7975,7 @@ lodash.isfinite@^3.3.2: resolved "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz" integrity sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA== -lodash.merge@^4.6.2: +lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== @@ -8231,6 +8321,13 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimatch@9.0.3, minimatch@^9.0.0: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -8259,13 +8356,6 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -10537,7 +10627,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.1: +semver@^7.5.1, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -11362,6 +11452,11 @@ tree-kill@1.2.2, tree-kill@^1.2.2: resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +ts-api-utils@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" + integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== + ts-node@10.2.1, ts-node@^10.0.0: version "10.2.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5" @@ -12303,7 +12398,7 @@ yargs@17.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@17.7.2: +yargs@17.7.2, yargs@^17.0.0: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -12329,19 +12424,6 @@ yargs@^16.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yargs@^17.2.1, yargs@^17.3.1: version "17.7.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz" From 13e9808df2c3be37059b7b7fbc3bb6ed74f9b1a4 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 12:30:20 +0100 Subject: [PATCH 08/50] Don't enforce ThemedComponent selectors in test HTML --- .../src/rules/html/themed-component-usages.ts | 5 ++++ .../rules/themed-component-usages.spec.ts | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts index 6184805a2b1..160d93326b1 100644 --- a/lint/src/rules/html/themed-component-usages.ts +++ b/lint/src/rules/html/themed-component-usages.ts @@ -20,6 +20,11 @@ export default { } }, create(context: any) { + if (context.getFilename().includes('.spec.ts')) { + // skip inline templates in unit tests + return {}; + } + return { [`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: any) { context.report({ diff --git a/lint/test/rules/themed-component-usages.spec.ts b/lint/test/rules/themed-component-usages.spec.ts index 2f5dbcec203..4ab3588ef62 100644 --- a/lint/test/rules/themed-component-usages.spec.ts +++ b/lint/test/rules/themed-component-usages.spec.ts @@ -137,6 +137,36 @@ describe('themed-component-usages (HTML)', () => { `, }, + { + name: fixture('src/test.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + name: fixture('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + filename: fixture('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, ], invalid: [ { From 9a27db3835a3ffcb8f09c90bba34bc67a52811b5 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 14:30:25 +0100 Subject: [PATCH 09/50] Lint e2e tests, enforce selectors --- angular.json | 1 + lint/src/rules/ts/themed-component-usages.ts | 15 ++++++- .../fixture/src/app/test/test.component.cy.ts | 8 ++++ .../rules/themed-component-usages.spec.ts | 45 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 lint/test/fixture/src/app/test/test.component.cy.ts diff --git a/angular.json b/angular.json index 5e597d4d307..a9b31ab4c01 100644 --- a/angular.json +++ b/angular.json @@ -266,6 +266,7 @@ "options": { "lintFilePatterns": [ "src/**/*.ts", + "cypress/**/*.ts", "src/**/*.html", "src/**/*.json5" ] diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index 5934eb5e2e6..2a3f18bd44f 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -98,7 +98,20 @@ export default ESLintUtils.RuleCreator.withoutDocs({ // ignore tests and non-routing modules if (context.getFilename()?.endsWith('.spec.ts')) { return { - [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { + [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { + context.report({ + node, + messageId: 'mustUseThemedWrapper', + fix(fixer: any){ + const newSelector = fixSelectors(node.raw); + return fixer.replaceText(node, newSelector); + } + }); + }, + }; + } else if (context.getFilename()?.endsWith('.cy.ts')) { + return { + [`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { context.report({ node, messageId: 'mustUseThemedWrapper', diff --git a/lint/test/fixture/src/app/test/test.component.cy.ts b/lint/test/fixture/src/app/test/test.component.cy.ts new file mode 100644 index 00000000000..2300ac4a56f --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.cy.ts @@ -0,0 +1,8 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + diff --git a/lint/test/rules/themed-component-usages.spec.ts b/lint/test/rules/themed-component-usages.spec.ts index 4ab3588ef62..89250cced03 100644 --- a/lint/test/rules/themed-component-usages.spec.ts +++ b/lint/test/rules/themed-component-usages.spec.ts @@ -52,6 +52,13 @@ export class Something { name: fixture('src/app/test/test.component.spec.ts'), code: ` By.css('ds-themeable'); +By.Css('#test > ds-themeable > #nest'); + `, + }, + { + name: fixture('src/app/test/test.component.cy.ts'), + code: ` +By.css('ds-themeable'); By.Css('#test > ds-themeable > #nest'); `, }, @@ -123,6 +130,44 @@ By.css('ds-themeable'); By.css('#test > ds-themeable > #nest'); `, }, + { + filename: fixture('src/app/test/test.component.cy.ts'), + code: ` +cy.get('ds-themed-themeable'); +cy.get('#test > ds-themed-themeable > #nest'); + `, + errors: [ + { + messageId: 'mustUseThemedWrapper', + }, + { + messageId: 'mustUseThemedWrapper', + }, + ], + output: ` +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); + `, + }, + { + filename: fixture('src/app/test/test.component.cy.ts'), + code: ` +cy.get('ds-base-themeable'); +cy.get('#test > ds-base-themeable > #nest'); + `, + errors: [ + { + messageId: 'mustUseThemedWrapper', + }, + { + messageId: 'mustUseThemedWrapper', + }, + ], + output: ` +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); + `, + }, ], } as any); }); From ae50780e282037e7b2a13799d59a86e13ea5b7c0 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 15:02:39 +0100 Subject: [PATCH 10/50] Enable linting for the lint plugins --- .eslintrc.json | 3 ++- angular.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index a18f5873b4d..ce5d1225b07 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,7 +24,8 @@ "parserOptions": { "project": [ "./tsconfig.json", - "./cypress/tsconfig.json" + "./cypress/tsconfig.json", + "./lint/tsconfig.json" ], "createDefaultProgram": true }, diff --git a/angular.json b/angular.json index a9b31ab4c01..15a36e29048 100644 --- a/angular.json +++ b/angular.json @@ -267,6 +267,7 @@ "lintFilePatterns": [ "src/**/*.ts", "cypress/**/*.ts", + "lint/**/*.ts", "src/**/*.html", "src/**/*.json5" ] From e83a0cd741ec951ae7f4c55bfa695b000ca86b78 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 15:04:53 +0100 Subject: [PATCH 11/50] Fix lint lint issues --- .../src/rules/html/themed-component-usages.ts | 10 +++--- .../rules/ts/themed-component-selectors.ts | 7 ++-- lint/src/rules/ts/themed-component-usages.ts | 32 +++++++------------ lint/src/util/theme-support.ts | 3 +- .../src/app/test/test-routing.module.ts | 2 +- lint/test/fixture/src/app/test/test.module.ts | 5 +-- .../test/themed-test-themeable.component.ts | 1 + .../test/app/test/test-themeable.component.ts | 1 + .../fixture/src/themes/test/test.module.ts | 3 +- .../rules/themed-component-selectors.spec.ts | 2 +- .../rules/themed-component-usages.spec.ts | 8 ++--- lint/test/testing.ts | 5 +-- package.json | 4 +-- 13 files changed, 40 insertions(+), 43 deletions(-) diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts index 160d93326b1..df0d775acba 100644 --- a/lint/src/rules/html/themed-component-usages.ts +++ b/lint/src/rules/html/themed-component-usages.ts @@ -17,7 +17,7 @@ export default { schema: [], messages: { mustUseThemedWrapperSelector: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', - } + }, }, create(context: any) { if (context.getFilename().includes('.spec.ts')) { @@ -36,7 +36,7 @@ export default { const openTagRange = [ node.startSourceSpan.start.offset + 1, - node.startSourceSpan.start.offset + 1 + oldSelector.length + node.startSourceSpan.start.offset + 1 + oldSelector.length, ]; const ops = [ @@ -47,15 +47,15 @@ export default { if (node.startSourceSpan.end.offset !== node.endSourceSpan.end.offset) { const closeTagRange = [ node.endSourceSpan.start.offset + 2, - node.endSourceSpan.end.offset - 1 + node.endSourceSpan.end.offset - 1, ]; ops.push(fixer.replaceTextRange(closeTagRange, newSelector)); } return ops; - } + }, }); }, }; - } + }, }; diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts index e150bb41a84..cc195efa470 100644 --- a/lint/src/rules/ts/themed-component-selectors.ts +++ b/lint/src/rules/ts/themed-component-selectors.ts @@ -6,6 +6,7 @@ * http://www.dspace.org/license/ */ import { ESLintUtils } from '@typescript-eslint/utils'; + import { getComponentSelectorNode } from '../../util/angular'; import { stringLiteral } from '../../util/misc'; import { @@ -23,7 +24,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ wrongSelectorUnthemedComponent: 'Unthemed version of themeable components should have a selector starting with \'ds-base-\'', wrongSelectorThemedComponentWrapper: 'Themed component wrapper of themeable components shouldn\'t have a selector starting with \'ds-themed-\'', wrongSelectorThemedComponentOverride: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'', - } + }, }, defaultOptions: [], create(context: any): any { @@ -86,7 +87,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ } else if (isThemeableComponent(className)) { enforceBaseSelector(selectorNode); } - } + }, }; - } + }, }); diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index 2a3f18bd44f..1032b1ef76f 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -6,6 +6,7 @@ * http://www.dspace.org/license/ */ import { ESLintUtils } from '@typescript-eslint/utils'; + import { findUsages } from '../../util/misc'; import { allThemeableComponents, @@ -50,7 +51,14 @@ export default ESLintUtils.RuleCreator.withoutDocs({ } function handleThemedSelectorQueriesInTests(node: any) { - + context.report({ + node, + messageId: 'mustUseThemedWrapper', + fix(fixer: any){ + const newSelector = fixSelectors(node.raw); + return fixer.replaceText(node, newSelector); + }, + }); } function handleUnthemedImportsInTypescript(specifierNode: any) { @@ -98,29 +106,11 @@ export default ESLintUtils.RuleCreator.withoutDocs({ // ignore tests and non-routing modules if (context.getFilename()?.endsWith('.spec.ts')) { return { - [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { - context.report({ - node, - messageId: 'mustUseThemedWrapper', - fix(fixer: any){ - const newSelector = fixSelectors(node.raw); - return fixer.replaceText(node, newSelector); - } - }); - }, + [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, }; } else if (context.getFilename()?.endsWith('.cy.ts')) { return { - [`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`](node: any) { - context.report({ - node, - messageId: 'mustUseThemedWrapper', - fix(fixer: any){ - const newSelector = fixSelectors(node.raw); - return fixer.replaceText(node, newSelector); - } - }); - }, + [`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, }; } else if ( context.getFilename()?.match(/(?!routing).module.ts$/) diff --git a/lint/src/util/theme-support.ts b/lint/src/util/theme-support.ts index bf7c265e2ea..18eed48452d 100644 --- a/lint/src/util/theme-support.ts +++ b/lint/src/util/theme-support.ts @@ -9,6 +9,7 @@ import { readFileSync } from 'fs'; import { basename } from 'path'; import ts from 'typescript'; + import { isClassDeclaration, isPartOfTypeExpression, @@ -134,7 +135,7 @@ function resolveLocalPath(path: string, relativeTo: string) { const parts = relativeTo.split('/'); return [ ...parts.slice(0, parts.length - 1), - path.replace(/^.\//, '') + path.replace(/^.\//, ''), ].join('/') + '.ts'; } else { throw new Error(`Unsupported local path: ${path}`); diff --git a/lint/test/fixture/src/app/test/test-routing.module.ts b/lint/test/fixture/src/app/test/test-routing.module.ts index d3a16bb6d6a..1ccbccc5994 100644 --- a/lint/test/fixture/src/app/test/test-routing.module.ts +++ b/lint/test/fixture/src/app/test/test-routing.module.ts @@ -10,5 +10,5 @@ import { ThemedTestThemeableComponent } from './themed-test-themeable.component' export const ROUTES = [ { component: ThemedTestThemeableComponent, - } + }, ]; diff --git a/lint/test/fixture/src/app/test/test.module.ts b/lint/test/fixture/src/app/test/test.module.ts index 633ef492fb9..a37396ef459 100644 --- a/lint/test/fixture/src/app/test/test.module.ts +++ b/lint/test/fixture/src/app/test/test.module.ts @@ -7,8 +7,9 @@ */ // @ts-ignore import { NgModule } from '@angular/core'; -import { TestThemeableComponent } from './test-themeable.component'; + import { TestComponent } from './test.component'; +import { TestThemeableComponent } from './test-themeable.component'; import { ThemedTestThemeableComponent } from './themed-test-themeable.component'; @NgModule({ @@ -16,7 +17,7 @@ import { ThemedTestThemeableComponent } from './themed-test-themeable.component' TestComponent, TestThemeableComponent, ThemedTestThemeableComponent, - ] + ], }) export class TestModule { diff --git a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts index 81eb59d4183..a45f89b6062 100644 --- a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts +++ b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts @@ -6,6 +6,7 @@ * http://www.dspace.org/license/ */ import { Component } from '@angular/core'; + import { ThemedComponent } from '../../../../../../src/app/shared/theme-support/themed.component'; import { TestThemeableComponent } from './test-themeable.component'; diff --git a/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts index 05ba4e3d1bd..d2b02ca9f1f 100644 --- a/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts +++ b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts @@ -6,6 +6,7 @@ * http://www.dspace.org/license/ */ import { Component } from '@angular/core'; + import { TestThemeableComponent as BaseComponent } from '../../../../app/test/test-themeable.component'; @Component({ diff --git a/lint/test/fixture/src/themes/test/test.module.ts b/lint/test/fixture/src/themes/test/test.module.ts index 6d7601bd52c..7aac91b07a7 100644 --- a/lint/test/fixture/src/themes/test/test.module.ts +++ b/lint/test/fixture/src/themes/test/test.module.ts @@ -7,12 +7,13 @@ */ // @ts-ignore import { NgModule } from '@angular/core'; + import { TestThemeableComponent } from './app/test/test-themeable.component'; @NgModule({ declarations: [ TestThemeableComponent, - ] + ], }) export class TestModule { diff --git a/lint/test/rules/themed-component-selectors.spec.ts b/lint/test/rules/themed-component-selectors.spec.ts index 2f2e9786c21..864c41d5980 100644 --- a/lint/test/rules/themed-component-selectors.spec.ts +++ b/lint/test/rules/themed-component-selectors.spec.ts @@ -7,11 +7,11 @@ */ +import rule from '../../src/rules/ts/themed-component-selectors'; import { fixture, tsRuleTester, } from '../testing'; -import rule from '../../src/rules/ts/themed-component-selectors'; describe('themed-component-selectors', () => { tsRuleTester.run('themed-component-selectors', rule as any, { diff --git a/lint/test/rules/themed-component-usages.spec.ts b/lint/test/rules/themed-component-usages.spec.ts index 89250cced03..4cbb135684a 100644 --- a/lint/test/rules/themed-component-usages.spec.ts +++ b/lint/test/rules/themed-component-usages.spec.ts @@ -6,13 +6,13 @@ * http://www.dspace.org/license/ */ +import htmlRule from '../../src/rules/html/themed-component-usages'; +import tsRule from '../../src/rules/ts/themed-component-usages'; import { fixture, htmlRuleTester, tsRuleTester, } from '../testing'; -import tsRule from '../../src/rules/ts/themed-component-usages'; -import htmlRule from '../../src/rules/html/themed-component-usages'; describe('themed-component-usages (TypeScript)', () => { tsRuleTester.run('themed-component-usages', tsRule as any, { @@ -90,7 +90,7 @@ const config = { a: ThemedTestThemeableComponent, b: TestComponent, } - ` + `, }, { filename: fixture('src/app/test/test.component.spec.ts'), @@ -260,6 +260,6 @@ class Test { `, }, - ] + ], }); }); diff --git a/lint/test/testing.ts b/lint/test/testing.ts index 631d956b0b2..f9507c00c3b 100644 --- a/lint/test/testing.ts +++ b/lint/test/testing.ts @@ -6,8 +6,9 @@ * http://www.dspace.org/license/ */ -import { RuleTester } from 'eslint'; import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester'; +import { RuleTester } from 'eslint'; + import { themeableComponents } from '../src/util/theme-support'; const FIXTURE = 'lint/test/fixture/'; @@ -29,7 +30,7 @@ export const tsRuleTester = new TypeScriptRuleTester({ }, parserOptions: { project: fixture('tsconfig.json'), - } + }, }); class HtmlRuleTester extends RuleTester { diff --git a/package.json b/package.json index b8f6402cfbd..ed2abad2a8b 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,9 @@ "test": "ng test --source-map=true --watch=false --configuration test", "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", - "test:lint": "yarn build:lint && jasmine --config=lint/jasmine.json", + "test:lint": "yarn build:lint && yarn test:lint:nobuild", "test:lint:nobuild": "jasmine --config=lint/jasmine.json", - "lint": "yarn build:lint && ng lint", + "lint": "yarn build:lint && yarn lint:nobuild", "lint:nobuild": "ng lint", "lint-fix": "yarn build:lint && ng lint --fix=true", "e2e": "cross-env NODE_ENV=production ng e2e", From b0758c23e5cbd616202b0117bdd9923e9b169456 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 14 Mar 2024 18:14:30 +0100 Subject: [PATCH 12/50] Enforce plugin structure and generate documentation --- lint/README.md | 19 +- lint/generate-docs.ts | 85 ++++++ lint/src/rules/html/index.ts | 14 +- .../src/rules/html/themed-component-usages.ts | 117 +++++++- lint/src/rules/ts/index.ts | 18 +- .../rules/ts/themed-component-selectors.ts | 167 ++++++++++- lint/src/rules/ts/themed-component-usages.ts | 210 +++++++++++++- lint/src/util/structure.ts | 68 +++++ lint/src/util/templates/index.ejs | 5 + lint/src/util/templates/rule.ejs | 36 +++ lint/test/fixture/index.ts | 13 + lint/test/rules.spec.ts | 26 ++ .../rules/themed-component-selectors.spec.ts | 140 --------- .../rules/themed-component-usages.spec.ts | 265 ------------------ lint/test/structure.spec.ts | 76 +++++ lint/test/testing.ts | 9 +- lint/test/{util => }/theme-support.spec.ts | 2 +- lint/tsconfig.json | 8 +- package.json | 1 + 19 files changed, 833 insertions(+), 446 deletions(-) create mode 100644 lint/generate-docs.ts create mode 100644 lint/src/util/structure.ts create mode 100644 lint/src/util/templates/index.ejs create mode 100644 lint/src/util/templates/rule.ejs create mode 100644 lint/test/fixture/index.ts create mode 100644 lint/test/rules.spec.ts delete mode 100644 lint/test/rules/themed-component-selectors.spec.ts delete mode 100644 lint/test/rules/themed-component-usages.spec.ts create mode 100644 lint/test/structure.spec.ts rename lint/test/{util => }/theme-support.spec.ts (93%) diff --git a/lint/README.md b/lint/README.md index 5fff29b1b23..1ea1fd5b650 100644 --- a/lint/README.md +++ b/lint/README.md @@ -1,12 +1,19 @@ -# ESLint plugins +# DSpace ESLint plugins Custom ESLint rules for DSpace Angular peculiarities. -## Overview +## Documentation + +The rules are split up into plugins by language: +- [TypeScript rules](./docs/ts/index.md) +- [HTML rules](./docs/html/index.md) + +> Run `yarn docs:lint` to generate this documentation! + +## Developing + +### Overview -- Different file types must be handled by separate plugins. We support: - - [TypeScript](./src/ts) - - [HTML](./src/html) - All rules are written in TypeScript and compiled into [`dist`](./dist) - The plugins are linked into the main project dependencies from here - These directories already contain the necessary `package.json` files to mark them as ESLint plugins @@ -16,7 +23,7 @@ Custom ESLint rules for DSpace Angular peculiarities. - [Custom rules in typescript-eslint](https://typescript-eslint.io/developers/custom-rules) - [Angular ESLint](https://github.com/angular-eslint/angular-eslint) -## Parsing project metadata in advance ~ TypeScript AST +### Parsing project metadata in advance ~ TypeScript AST While it is possible to retain persistent state between files during the linting process, it becomes quite complicated if the content of one file determines how we want to lint another file. Because the two files may be linted out of order, we may not know whether the first file is wrong before we pass by the second. This means that we cannot report or fix the issue, because the first file is already detached from the linting context. diff --git a/lint/generate-docs.ts b/lint/generate-docs.ts new file mode 100644 index 00000000000..b3a798628f5 --- /dev/null +++ b/lint/generate-docs.ts @@ -0,0 +1,85 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from 'fs'; +import { rmSync } from 'node:fs'; +import { join } from 'path'; + +import { default as htmlPlugin } from './src/rules/html'; +import { default as tsPlugin } from './src/rules/ts'; + +const templates = new Map(); + +function lazyEJS(path: string, data: object) { + if (!templates.has(path)) { + templates.set(path, require('ejs').compile(readFileSync(path).toString())); + } + + return templates.get(path)(data); +} + +const docsDir = join('lint', 'docs'); +const tsDir = join(docsDir, 'ts'); +const htmlDir = join(docsDir, 'html'); + +if (existsSync(docsDir)) { + rmSync(docsDir, { recursive: true }); +} + +mkdirSync(join(tsDir, 'rules'), { recursive: true }); +mkdirSync(join(htmlDir, 'rules'), { recursive: true }); + +function template(name: string): string { + return join('lint', 'src', 'util', 'templates', name); +} + +// TypeScript docs +writeFileSync( + join(tsDir, 'index.md'), + lazyEJS(template('index.ejs'), { + plugin: tsPlugin, + rules: tsPlugin.index.map(rule => rule.info), + }), +); + +for (const rule of tsPlugin.index) { + writeFileSync( + join(tsDir, 'rules', rule.info.name + '.md'), + lazyEJS(template('rule.ejs'), { + plugin: tsPlugin, + rule: rule.info, + tests: rule.tests, + }), + ); +} + +// HTML docs +writeFileSync( + join(htmlDir, 'index.md'), + lazyEJS(template('index.ejs'), { + plugin: htmlPlugin, + rules: htmlPlugin.index.map(rule => rule.info), + }), +); + +for (const rule of htmlPlugin.index) { + writeFileSync( + join(htmlDir, 'rules', rule.info.name + '.md'), + lazyEJS(template('rule.ejs'), { + plugin: htmlPlugin, + rule: rule.info, + tests: rule.tests, + }), + ); +} + diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts index ef0b7a87edb..0ea42a3c2be 100644 --- a/lint/src/rules/html/index.ts +++ b/lint/src/rules/html/index.ts @@ -6,11 +6,17 @@ * http://www.dspace.org/license/ */ -import themedComponentUsages from './themed-component-usages'; +import { + bundle, + RuleExports, +} from '../../util/structure'; +import * as themedComponentUsages from './themed-component-usages'; + +const index = [ + themedComponentUsages, +] as unknown as RuleExports[]; export = { - rules: { - 'themed-component-usages': themedComponentUsages, - }, parser: require('@angular-eslint/template-parser'), + ...bundle('dspace-angular-html', 'HTML', index), }; diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts index df0d775acba..0c083f185df 100644 --- a/lint/src/rules/html/themed-component-usages.ts +++ b/lint/src/rules/html/themed-component-usages.ts @@ -5,20 +5,39 @@ * * http://www.dspace.org/license/ */ +import { fixture } from '../../../test/fixture'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; import { DISALLOWED_THEME_SELECTORS, fixSelectors, } from '../../util/theme-support'; -export default { +export enum Message { + WRONG_SELECTOR = 'mustUseThemedWrapperSelector', +} + +export const info = { + name: 'themed-component-usages', meta: { + docs: { + description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class + +This ensures that custom themes can correctly override _all_ instances of this component. +The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple. + `, + }, type: 'problem', fixable: 'code', schema: [], messages: { - mustUseThemedWrapperSelector: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', + [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', }, }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = { + ...info, create(context: any) { if (context.getFilename().includes('.spec.ts')) { // skip inline templates in unit tests @@ -28,7 +47,7 @@ export default { return { [`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: any) { context.report({ - messageId: 'mustUseThemedWrapperSelector', + messageId: Message.WRONG_SELECTOR, node, fix(fixer: any) { const oldSelector = node.name; @@ -59,3 +78,95 @@ export default { }; }, }; + +export const tests = { + plugin: info.name, + valid: [ + { + code: ` + + + + `, + }, + { + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + filename: fixture('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + filename: fixture('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + ], + invalid: [ + { + code: ` + + + + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` + + + + `, + }, + { + code: ` + + + + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` + + + + `, + }, + ], +}; + +export default rule; diff --git a/lint/src/rules/ts/index.ts b/lint/src/rules/ts/index.ts index b33135d7b0f..2983d94386e 100644 --- a/lint/src/rules/ts/index.ts +++ b/lint/src/rules/ts/index.ts @@ -1,9 +1,15 @@ -import themedComponentSelectors from './themed-component-selectors'; -import themedComponentUsages from './themed-component-usages'; +import { + bundle, + RuleExports, +} from '../../util/structure'; +import * as themedComponentUsages from './themed-component-usages'; +import * as themedComponentSelectors from './themed-component-selectors'; + +const index = [ + themedComponentUsages, + themedComponentSelectors, +] as unknown as RuleExports[]; export = { - rules: { - 'themed-component-selectors': themedComponentSelectors, - 'themed-component-usages': themedComponentUsages, - }, + ...bundle('dspace-angular-ts', 'TypeScript', index), }; diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts index cc195efa470..5c455bf1de3 100644 --- a/lint/src/rules/ts/themed-component-selectors.ts +++ b/lint/src/rules/ts/themed-component-selectors.ts @@ -6,27 +6,53 @@ * http://www.dspace.org/license/ */ import { ESLintUtils } from '@typescript-eslint/utils'; +import { fixture } from '../../../test/fixture'; import { getComponentSelectorNode } from '../../util/angular'; import { stringLiteral } from '../../util/misc'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; import { inThemedComponentOverrideFile, isThemeableComponent, isThemedComponentWrapper, } from '../../util/theme-support'; -export default ESLintUtils.RuleCreator.withoutDocs({ +export enum Message { + BASE = 'wrongSelectorUnthemedComponent', + WRAPPER = 'wrongSelectorThemedComponentWrapper', + THEMED = 'wrongSelectorThemedComponentOverride', +} + +export const info = { + name: 'themed-component-selectors', meta: { + docs: { + description: `Themeable component selectors should follow the DSpace convention + +Each themeable component is comprised of a base component, a wrapper component and any number of themed components +- Base components should have a selector starting with \`ds-base-\` +- Themed components should have a selector starting with \`ds-themed-\` +- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\` + - This is the regular DSpace selector prefix + - **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector** + +Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source. + `, + }, type: 'problem', schema: [], fixable: 'code', messages: { - wrongSelectorUnthemedComponent: 'Unthemed version of themeable components should have a selector starting with \'ds-base-\'', - wrongSelectorThemedComponentWrapper: 'Themed component wrapper of themeable components shouldn\'t have a selector starting with \'ds-themed-\'', - wrongSelectorThemedComponentOverride: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'', + [Message.BASE]: 'Unthemed version of themeable components should have a selector starting with \'ds-base-\'', + [Message.WRAPPER]: 'Themed component wrapper of themeable components shouldn\'t have a selector starting with \'ds-themed-\'', + [Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'', }, }, defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, create(context: any): any { if (context.getFilename()?.endsWith('.spec.ts')) { return {}; @@ -35,7 +61,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ function enforceWrapperSelector(selectorNode: any) { if (selectorNode?.value.startsWith('ds-themed-')) { context.report({ - messageId: 'wrongSelectorThemedComponentWrapper', + messageId: Message.WRAPPER, node: selectorNode, fix(fixer: any) { return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-'))); @@ -47,7 +73,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ function enforceBaseSelector(selectorNode: any) { if (!selectorNode?.value.startsWith('ds-base-')) { context.report({ - messageId: 'wrongSelectorUnthemedComponent', + messageId: Message.BASE, node: selectorNode, fix(fixer: any) { return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-'))); @@ -59,7 +85,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ function enforceThemedSelector(selectorNode: any) { if (!selectorNode?.value.startsWith('ds-themed-')) { context.report({ - messageId: 'wrongSelectorThemedComponentOverride', + messageId: Message.THEMED, node: selectorNode, fix(fixer: any) { return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-'))); @@ -91,3 +117,130 @@ export default ESLintUtils.RuleCreator.withoutDocs({ }; }, }); + +export const tests = { + plugin: info.name, + valid: [ + { + name: 'Regular non-themeable component selector', + code: ` + @Component({ + selector: 'ds-something', + }) + class Something { + } + `, + }, + { + name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-', + code: ` +@Component({ + selector: 'ds-base-something', +}) +class Something { +} + +@Component({ + selector: 'ds-something', +}) +class ThemedSomething extends ThemedComponent { +} + +@Component({ + selector: 'ds-themed-something', +}) +class OverrideSomething extends Something { +} + `, + }, + { + name: 'Other themed component wrappers should not interfere', + code: ` +@Component({ + selector: 'ds-something', +}) +class Something { +} + +@Component({ + selector: 'ds-something-else', +}) +class ThemedSomethingElse extends ThemedComponent { +} + `, + }, + ], + invalid: [ + { + name: 'Wrong selector for base component', + filename: fixture('src/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-something', +}) +class TestThemeableComponent { +} + `, + errors: [ + { + messageId: Message.BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-base-something', +}) +class TestThemeableComponent { +} + `, + }, + { + name: 'Wrong selector for wrapper component', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors: [ + { + messageId: Message.WRAPPER, + }, + ], + output: ` +@Component({ + selector: 'ds-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Wrong selector for theme override', + filename: fixture('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-something', +}) +class TestThememeableComponent extends BaseComponent { +} + `, + errors: [ + { + messageId: Message.THEMED, + }, + ], + output: ` +@Component({ + selector: 'ds-themed-something', +}) +class TestThememeableComponent extends BaseComponent { +} + `, + }, + ], +}; + +export default rule; diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index 1032b1ef76f..54b93363cb6 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -6,8 +6,9 @@ * http://www.dspace.org/license/ */ import { ESLintUtils } from '@typescript-eslint/utils'; - +import { fixture } from '../../../test/fixture'; import { findUsages } from '../../util/misc'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; import { allThemeableComponents, DISALLOWED_THEME_SELECTORS, @@ -17,17 +18,40 @@ import { isAllowedUnthemedUsage, } from '../../util/theme-support'; -export default ESLintUtils.RuleCreator.withoutDocs({ +export enum Message { + WRONG_CLASS = 'mustUseThemedWrapperClass', + WRONG_IMPORT = 'mustImportThemedWrapper', + WRONG_SELECTOR = 'mustUseThemedWrapperSelector', +} + +export const info = { + name: 'themed-component-usages', meta: { + docs: { + description: `Themeable components should be used via their \`ThemedComponent\` wrapper class + +This ensures that custom themes can correctly override _all_ instances of this component. +There are a few exceptions where the base class can still be used: +- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place) +- Angular modules (except for routing modules) +- Angular \`@ViewChild\` decorators +- Type annotations + `, + }, type: 'problem', schema: [], fixable: 'code', messages: { - mustUseThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper', - mustImportThemedWrapper: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper', }, }, defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, create(context: any, options: any): any { function handleUnthemedUsagesInTypescript(node: any) { if (isAllowedUnthemedUsage(node)) { @@ -42,7 +66,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ } context.report({ - messageId: 'mustUseThemedWrapper', + messageId: Message.WRONG_CLASS, node: node, fix(fixer: any) { return fixer.replaceText(node, entry.wrapperClass); @@ -53,7 +77,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ function handleThemedSelectorQueriesInTests(node: any) { context.report({ node, - messageId: 'mustUseThemedWrapper', + messageId: Message.WRONG_SELECTOR, fix(fixer: any){ const newSelector = fixSelectors(node.raw); return fixer.replaceText(node, newSelector); @@ -79,7 +103,7 @@ export default ESLintUtils.RuleCreator.withoutDocs({ } context.report({ - messageId: 'mustImportThemedWrapper', + messageId: Message.WRONG_IMPORT, node: importedNode, fix(fixer: any) { const ops = []; @@ -133,3 +157,175 @@ export default ESLintUtils.RuleCreator.withoutDocs({ }, }); + +export const tests = { + plugin: info.name, + valid: [ + { + name: 'allow wrapper class usages', + code: ` +import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; + +const config = { + a: ThemedTestThemeableComponent, + b: ChipsComponent, +} + `, + }, + { + name: 'allow base class in class declaration', + code: ` +export class TestThemeableComponent { +} + `, + }, + { + name: 'allow inheriting from base class', + code: ` +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; + +export class ThemedAdminSidebarComponent extends ThemedComponent { +} + `, + }, + { + name: 'allow base class in ViewChild', + code: ` +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; + +export class Something { + @ViewChild(TestThemeableComponent) test: TestThemeableComponent; +} + `, + }, + { + name: 'allow wrapper selectors in test queries', + filename: fixture('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-themeable'); +By.Css('#test > ds-themeable > #nest'); + `, + }, + { + name: 'allow wrapper selectors in cypress queries', + filename: fixture('src/app/test/test.component.cy.ts'), + code: ` +By.css('ds-themeable'); +By.Css('#test > ds-themeable > #nest'); + `, + }, + ], + invalid: [ + { + name: 'disallow direct usages of base class', + code: ` +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; +import { TestComponent } from '../test/test.component.ts'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; +import { TestComponent } from '../test/test.component.ts'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, +} + `, + }, + { + name: 'disallow override selector in test queries', + filename: fixture('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-themed-themeable'); +By.css('#test > ds-themed-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + { + name: 'disallow base selector in test queries', + filename: fixture('src/app/test/test.component.spec.ts'), + code: ` +By.css('ds-base-themeable'); +By.css('#test > ds-base-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); + `, + }, + { + name: 'disallow override selector in cypress queries', + filename: fixture('src/app/test/test.component.cy.ts'), + code: ` +cy.get('ds-themed-themeable'); +cy.get('#test > ds-themed-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); + `, + }, + { + name: 'disallow base selector in cypress queries', + filename: fixture('src/app/test/test.component.cy.ts'), + code: ` +cy.get('ds-base-themeable'); +cy.get('#test > ds-base-themeable > #nest'); + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); + `, + }, + ], +}; + +export default rule; diff --git a/lint/src/util/structure.ts b/lint/src/util/structure.ts new file mode 100644 index 00000000000..13535bfe17e --- /dev/null +++ b/lint/src/util/structure.ts @@ -0,0 +1,68 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { TSESLint } from '@typescript-eslint/utils'; +import { RuleTester } from 'eslint'; +import { EnumType } from 'typescript'; + +export type Meta = TSESLint.RuleMetaData; +export type Valid = RuleTester.ValidTestCase | TSESLint.ValidTestCase; +export type Invalid = RuleTester.InvalidTestCase | TSESLint.InvalidTestCase; + + +export interface DSpaceESLintRuleInfo { + name: string; + meta: Meta, + defaultOptions: any[], +} + +export interface DSpaceESLintTestInfo { + rule: string; + valid: Valid[]; + invalid: Invalid[]; +} + +export interface DSpaceESLintPluginInfo { + name: string; + description: string; + rules: DSpaceESLintRuleInfo; + tests: DSpaceESLintTestInfo; +} + +export interface DSpaceESLintInfo { + html: DSpaceESLintPluginInfo; + ts: DSpaceESLintPluginInfo; +} + +export interface RuleExports { + Message: EnumType, + info: DSpaceESLintRuleInfo, + rule: any, + tests: any, + default: any, +} + +export function bundle( + name: string, + language: string, + index: RuleExports[], +): { + name: string, + language: string, + rules: Record, + index: RuleExports[], +} { + return index.reduce((o: any, i: any) => { + o.rules[i.info.name] = i.rule; + return o; + }, { + name, + language, + rules: {}, + index, + }); +} diff --git a/lint/src/util/templates/index.ejs b/lint/src/util/templates/index.ejs new file mode 100644 index 00000000000..7ce8c15d6b9 --- /dev/null +++ b/lint/src/util/templates/index.ejs @@ -0,0 +1,5 @@ +[DSpace ESLint plugins](../../README.md) > <%= plugin.language %> rules + +<% rules.forEach(rule => { %> +- [`<%= plugin.name %>/<%= rule.name %>`](./rules/<%= rule.name %>.md)<% if (rule.meta?.docs?.description) {%>: <%= rule.meta.docs.description.split('\n')[0] %><% }%> +<% }) %> diff --git a/lint/src/util/templates/rule.ejs b/lint/src/util/templates/rule.ejs new file mode 100644 index 00000000000..ac5df5815d6 --- /dev/null +++ b/lint/src/util/templates/rule.ejs @@ -0,0 +1,36 @@ +[DSpace ESLint plugins](../../../README.md) > [<%= plugin.language %> rules](../index.md) > `<%= plugin.name %>/<%= rule.name %>` +_______ + +<%- rule.meta.docs?.description %> + +_______ + +[Source code](../../../src/rules/<%- plugin.name.replace('dspace-angular-', '') %>/<%- rule.name %>.ts) + +### Examples + +<% if (tests.valid) {%> +#### Valid code + <% tests.valid.forEach(test => { %> + <% if (test.filename) { %> +Filename: `<%- test.filename %>` + <% } %> +``` +<%- test.code.trim() %> +``` + <% }) %> +<% } %> + +<% if (tests.invalid) {%> +#### Invalid code + <% tests.invalid.forEach(test => { %> + + <% if (test.filename) { %> +Filename: `<%- test.filename %>` + <% } %> +``` +<%- test.code.trim() %> +``` + + <% }) %> +<% } %> diff --git a/lint/test/fixture/index.ts b/lint/test/fixture/index.ts new file mode 100644 index 00000000000..1d4f33f7e28 --- /dev/null +++ b/lint/test/fixture/index.ts @@ -0,0 +1,13 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +export const FIXTURE = 'lint/test/fixture/'; + +export function fixture(path: string): string { + return FIXTURE + path; +} diff --git a/lint/test/rules.spec.ts b/lint/test/rules.spec.ts new file mode 100644 index 00000000000..a8c1b382b22 --- /dev/null +++ b/lint/test/rules.spec.ts @@ -0,0 +1,26 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { default as htmlPlugin } from '../src/rules/html'; +import { default as tsPlugin } from '../src/rules/ts'; +import { + htmlRuleTester, + tsRuleTester, +} from './testing'; + +describe('TypeScript rules', () => { + for (const { info, rule, tests } of tsPlugin.index) { + tsRuleTester.run(info.name, rule, tests); + } +}); + +describe('HTML rules', () => { + for (const { info, rule, tests } of htmlPlugin.index) { + htmlRuleTester.run(info.name, rule, tests); + } +}); diff --git a/lint/test/rules/themed-component-selectors.spec.ts b/lint/test/rules/themed-component-selectors.spec.ts deleted file mode 100644 index 864c41d5980..00000000000 --- a/lint/test/rules/themed-component-selectors.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ - - -import rule from '../../src/rules/ts/themed-component-selectors'; -import { - fixture, - tsRuleTester, -} from '../testing'; - -describe('themed-component-selectors', () => { - tsRuleTester.run('themed-component-selectors', rule as any, { - valid: [ - { - name: 'Regular non-themeable component selector', - code: ` - @Component({ - selector: 'ds-something', - }) - class Something { - } - `, - }, - { - name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-', - code: ` -@Component({ - selector: 'ds-base-something', -}) -class Something { -} - -@Component({ - selector: 'ds-something', -}) -class ThemedSomething extends ThemedComponent { -} - -@Component({ - selector: 'ds-themed-something', -}) -class OverrideSomething extends Something { -} - `, - }, - { - name: 'Other themed component wrappers should not interfere', - code: ` -@Component({ - selector: 'ds-something', -}) -class Something { -} - -@Component({ - selector: 'ds-something-else', -}) -class ThemedSomethingElse extends ThemedComponent { -} - `, - }, - ], - invalid: [ - { - name: 'Wrong selector for base component', - filename: fixture('src/app/test/test-themeable.component.ts'), - code: ` -@Component({ - selector: 'ds-something', -}) -class TestThemeableComponent { -} - `, - errors: [ - { - messageId: 'wrongSelectorUnthemedComponent', - }, - ], - output: ` -@Component({ - selector: 'ds-base-something', -}) -class TestThemeableComponent { -} - `, - }, - { - name: 'Wrong selector for wrapper component', - filename: fixture('src/app/test/themed-test-themeable.component.ts'), - code: ` -@Component({ - selector: 'ds-themed-something', -}) -class ThemedTestThemeableComponent extends ThemedComponent { -} - `, - errors: [ - { - messageId: 'wrongSelectorThemedComponentWrapper', - }, - ], - output: ` -@Component({ - selector: 'ds-something', -}) -class ThemedTestThemeableComponent extends ThemedComponent { -} - `, - }, - { - name: 'Wrong selector for theme override', - filename: fixture('src/themes/test/app/test/test-themeable.component.ts'), - code: ` -@Component({ - selector: 'ds-something', -}) -class TestThememeableComponent extends BaseComponent { -} - `, - errors: [ - { - messageId: 'wrongSelectorThemedComponentOverride', - }, - ], - output: ` -@Component({ - selector: 'ds-themed-something', -}) -class TestThememeableComponent extends BaseComponent { -} - `, - }, - ], - } as any); -}); diff --git a/lint/test/rules/themed-component-usages.spec.ts b/lint/test/rules/themed-component-usages.spec.ts deleted file mode 100644 index 4cbb135684a..00000000000 --- a/lint/test/rules/themed-component-usages.spec.ts +++ /dev/null @@ -1,265 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ - -import htmlRule from '../../src/rules/html/themed-component-usages'; -import tsRule from '../../src/rules/ts/themed-component-usages'; -import { - fixture, - htmlRuleTester, - tsRuleTester, -} from '../testing'; - -describe('themed-component-usages (TypeScript)', () => { - tsRuleTester.run('themed-component-usages', tsRule as any, { - valid: [ - { - code: ` -const config = { - a: ThemedTestThemeableComponent, - b: ChipsComponent, -} - `, - }, - { - code: ` -export class TestThemeableComponent { -} - `, - }, - { - code: ` -import { TestThemeableComponent } from '../test/test-themeable.component.ts'; - -export class ThemedAdminSidebarComponent extends ThemedComponent { -} - `, - }, - { - code: ` -import { TestThemeableComponent } from '../test/test-themeable.component.ts'; - -export class Something { - @ViewChild(TestThemeableComponent) test: TestThemeableComponent; -} - `, - }, - { - name: fixture('src/app/test/test.component.spec.ts'), - code: ` -By.css('ds-themeable'); -By.Css('#test > ds-themeable > #nest'); - `, - }, - { - name: fixture('src/app/test/test.component.cy.ts'), - code: ` -By.css('ds-themeable'); -By.Css('#test > ds-themeable > #nest'); - `, - }, - ], - invalid: [ - { - code: ` -import { TestThemeableComponent } from '../test/test-themeable.component.ts'; -import { TestComponent } from '../test/test.component.ts'; - -const config = { - a: TestThemeableComponent, - b: TestComponent, -} - `, - errors: [ - { - messageId: 'mustImportThemedWrapper', - }, - { - messageId: 'mustUseThemedWrapper', - }, - ], - output: ` -import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; -import { TestComponent } from '../test/test.component.ts'; - -const config = { - a: ThemedTestThemeableComponent, - b: TestComponent, -} - `, - }, - { - filename: fixture('src/app/test/test.component.spec.ts'), - code: ` -By.css('ds-themed-themeable'); -By.css('#test > ds-themed-themeable > #nest'); - `, - errors: [ - { - messageId: 'mustUseThemedWrapper', - }, - { - messageId: 'mustUseThemedWrapper', - }, - ], - output: ` -By.css('ds-themeable'); -By.css('#test > ds-themeable > #nest'); - `, - }, - { - filename: fixture('src/app/test/test.component.spec.ts'), - code: ` -By.css('ds-base-themeable'); -By.css('#test > ds-base-themeable > #nest'); - `, - errors: [ - { - messageId: 'mustUseThemedWrapper', - }, - { - messageId: 'mustUseThemedWrapper', - }, - ], - output: ` -By.css('ds-themeable'); -By.css('#test > ds-themeable > #nest'); - `, - }, - { - filename: fixture('src/app/test/test.component.cy.ts'), - code: ` -cy.get('ds-themed-themeable'); -cy.get('#test > ds-themed-themeable > #nest'); - `, - errors: [ - { - messageId: 'mustUseThemedWrapper', - }, - { - messageId: 'mustUseThemedWrapper', - }, - ], - output: ` -cy.get('ds-themeable'); -cy.get('#test > ds-themeable > #nest'); - `, - }, - { - filename: fixture('src/app/test/test.component.cy.ts'), - code: ` -cy.get('ds-base-themeable'); -cy.get('#test > ds-base-themeable > #nest'); - `, - errors: [ - { - messageId: 'mustUseThemedWrapper', - }, - { - messageId: 'mustUseThemedWrapper', - }, - ], - output: ` -cy.get('ds-themeable'); -cy.get('#test > ds-themeable > #nest'); - `, - }, - ], - } as any); -}); - -describe('themed-component-usages (HTML)', () => { - htmlRuleTester.run('themed-component-usages', htmlRule, { - valid: [ - { - code: ` - - - - `, - }, - { - name: fixture('src/test.ts'), - code: ` -@Component({ - template: '' -}) -class Test { -} - `, - }, - { - name: fixture('src/test.spec.ts'), - code: ` -@Component({ - template: '' -}) -class Test { -} - `, - }, - { - filename: fixture('src/test.spec.ts'), - code: ` -@Component({ - template: '' -}) -class Test { -} - `, - }, - ], - invalid: [ - { - code: ` - - - - `, - errors: [ - { - messageId: 'mustUseThemedWrapperSelector', - }, - { - messageId: 'mustUseThemedWrapperSelector', - }, - { - messageId: 'mustUseThemedWrapperSelector', - }, - ], - output: ` - - - - `, - }, - { - code: ` - - - - `, - errors: [ - { - messageId: 'mustUseThemedWrapperSelector', - }, - { - messageId: 'mustUseThemedWrapperSelector', - }, - { - messageId: 'mustUseThemedWrapperSelector', - }, - ], - output: ` - - - - `, - }, - ], - }); -}); diff --git a/lint/test/structure.spec.ts b/lint/test/structure.spec.ts new file mode 100644 index 00000000000..24e69e42d9a --- /dev/null +++ b/lint/test/structure.spec.ts @@ -0,0 +1,76 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { default as html } from '../src/rules/html'; +import { default as ts } from '../src/rules/ts'; + +describe('plugin structure', () => { + for (const pluginExports of [ts, html]) { + const pluginName = pluginExports.name ?? 'UNNAMED PLUGIN'; + + describe(pluginName, () => { + it('should have a name', () => { + expect(pluginExports.name).toBeTruthy(); + }); + + it('should have rules', () => { + expect(pluginExports.index).toBeTruthy(); + expect(pluginExports.rules).toBeTruthy(); + expect(pluginExports.index.length).toBeGreaterThan(0); + }); + + for (const ruleExports of pluginExports.index) { + const ruleName = ruleExports.info.name ?? 'UNNAMED RULE'; + + describe(ruleName, () => { + it('should have a name', () => { + expect(ruleExports.info.name).toBeTruthy(); + }); + + it('should be included under the right name in the plugin', () => { + expect(pluginExports.rules[ruleExports.info.name]).toBe(ruleExports.rule); + }); + + it('should contain metadata', () => { + expect(ruleExports.info).toBeTruthy(); + expect(ruleExports.info.name).toBeTruthy(); + expect(ruleExports.info.meta).toBeTruthy(); + expect(ruleExports.info.defaultOptions).toBeTruthy(); + }); + + it('should contain messages', () => { + expect(ruleExports.Message).toBeTruthy(); + expect(ruleExports.info.meta.messages).toBeTruthy(); + }); + + describe('messages', () => { + for (const member of Object.keys(ruleExports.Message)) { + describe(member, () => { + const id = (ruleExports.Message as any)[member]; + + it('should have a valid ID', () => { + expect(id).toBeTruthy(); + }); + + it('should have valid metadata', () => { + expect(ruleExports.info.meta.messages[id]).toBeTruthy(); + }); + }); + } + }); + + it('should contain tests', () => { + expect(ruleExports.tests).toBeTruthy(); + expect(ruleExports.tests.valid.length).toBeGreaterThan(0); + expect(ruleExports.tests.invalid.length).toBeGreaterThan(0); + }); + }); + } + }); + } +}); diff --git a/lint/test/testing.ts b/lint/test/testing.ts index f9507c00c3b..f4f92a0e631 100644 --- a/lint/test/testing.ts +++ b/lint/test/testing.ts @@ -8,20 +8,19 @@ import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester'; import { RuleTester } from 'eslint'; +import { + FIXTURE, + fixture, +} from './fixture'; import { themeableComponents } from '../src/util/theme-support'; -const FIXTURE = 'lint/test/fixture/'; // Register themed components from test fixture themeableComponents.initialize(FIXTURE); TypeScriptRuleTester.itOnly = fit; -export function fixture(path: string): string { - return FIXTURE + path; -} - export const tsRuleTester = new TypeScriptRuleTester({ parser: '@typescript-eslint/parser', defaultFilenames: { diff --git a/lint/test/util/theme-support.spec.ts b/lint/test/theme-support.spec.ts similarity index 93% rename from lint/test/util/theme-support.spec.ts rename to lint/test/theme-support.spec.ts index 52e63b4fed6..2edf9594b62 100644 --- a/lint/test/util/theme-support.spec.ts +++ b/lint/test/theme-support.spec.ts @@ -6,7 +6,7 @@ * http://www.dspace.org/license/ */ -import { themeableComponents } from '../../src/util/theme-support'; +import { themeableComponents } from '../src/util/theme-support'; describe('theme-support', () => { describe('themeable component registry', () => { diff --git a/lint/tsconfig.json b/lint/tsconfig.json index 2c74bddb240..d3537a73762 100644 --- a/lint/tsconfig.json +++ b/lint/tsconfig.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "target": "ES2021", + "lib": [ + "es2021" + ], "module": "nodenext", "moduleResolution": "nodenext", "noImplicitReturns": true, @@ -7,14 +11,14 @@ "strict": true, "outDir": "./dist", "sourceMap": true, + "allowSyntheticDefaultImports": true, "types": [ "jasmine", "node" ] }, "include": [ - "src/**/*.ts", - "test/**/*.ts", + "**/*.ts", ], "exclude": [ "dist", diff --git a/package.json b/package.json index ed2abad2a8b..5ef876c5600 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "lint": "yarn build:lint && yarn lint:nobuild", "lint:nobuild": "ng lint", "lint-fix": "yarn build:lint && ng lint --fix=true", + "docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts", "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", From 6e22b5376a70b751265cd398e4b199602186473e Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Fri, 15 Mar 2024 13:19:47 +0100 Subject: [PATCH 13/50] Make rules more type-safe --- lint/generate-docs.ts | 2 +- lint/src/rules/html/index.ts | 2 +- .../src/rules/html/themed-component-usages.ts | 52 ++++--- lint/src/rules/ts/index.ts | 10 +- .../rules/ts/themed-component-selectors.ts | 49 +++--- lint/src/rules/ts/themed-component-usages.ts | 38 +++-- lint/src/util/angular.ts | 20 ++- lint/src/util/misc.ts | 30 +--- lint/src/util/structure.ts | 41 ++--- lint/src/util/theme-support.ts | 141 +++++++++++++----- lint/src/util/typescript.ts | 75 ++++++++++ lint/test/rules.spec.ts | 2 +- lint/test/testing.ts | 4 +- package.json | 1 + yarn.lock | 5 + 15 files changed, 314 insertions(+), 158 deletions(-) create mode 100644 lint/src/util/typescript.ts diff --git a/lint/generate-docs.ts b/lint/generate-docs.ts index b3a798628f5..ade2edea651 100644 --- a/lint/generate-docs.ts +++ b/lint/generate-docs.ts @@ -10,9 +10,9 @@ import { existsSync, mkdirSync, readFileSync, + rmSync, writeFileSync, } from 'fs'; -import { rmSync } from 'node:fs'; import { join } from 'path'; import { default as htmlPlugin } from './src/rules/html'; diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts index 0ea42a3c2be..7c1370ae2d4 100644 --- a/lint/src/rules/html/index.ts +++ b/lint/src/rules/html/index.ts @@ -5,7 +5,7 @@ * * http://www.dspace.org/license/ */ - +/* eslint-disable import/no-namespace */ import { bundle, RuleExports, diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts index 0c083f185df..82cfded2804 100644 --- a/lint/src/rules/html/themed-component-usages.ts +++ b/lint/src/rules/html/themed-component-usages.ts @@ -5,12 +5,23 @@ * * http://www.dspace.org/license/ */ +import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler'; +import { getTemplateParserServices } from '@angular-eslint/utils'; +import { + ESLintUtils, + TSESLint, +} from '@typescript-eslint/utils'; + import { fixture } from '../../../test/fixture'; -import { DSpaceESLintRuleInfo } from '../../util/structure'; +import { + DSpaceESLintRuleInfo, + NamedTests, +} from '../../util/structure'; import { DISALLOWED_THEME_SELECTORS, fixSelectors, } from '../../util/theme-support'; +import { getFilename } from '../../util/typescript'; export enum Message { WRONG_SELECTOR = 'mustUseThemedWrapperSelector', @@ -36,39 +47,38 @@ The only exception to this rule are unit tests, where we may want to use the bas defaultOptions: [], } as DSpaceESLintRuleInfo; -export const rule = { +export const rule = ESLintUtils.RuleCreator.withoutDocs({ ...info, - create(context: any) { - if (context.getFilename().includes('.spec.ts')) { + create(context: TSESLint.RuleContext) { + if (getFilename(context).includes('.spec.ts')) { // skip inline templates in unit tests return {}; } + const parserServices = getTemplateParserServices(context as any); + return { - [`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: any) { + [`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: TmplAstElement) { + const { startSourceSpan, endSourceSpan } = node; + const openStart = startSourceSpan.start.offset as number; + context.report({ messageId: Message.WRONG_SELECTOR, - node, - fix(fixer: any) { + loc: parserServices.convertNodeSourceSpanToLoc(startSourceSpan), + fix(fixer) { const oldSelector = node.name; const newSelector = fixSelectors(oldSelector); - const openTagRange = [ - node.startSourceSpan.start.offset + 1, - node.startSourceSpan.start.offset + 1 + oldSelector.length, - ]; - const ops = [ - fixer.replaceTextRange(openTagRange, newSelector), + fixer.replaceTextRange([openStart + 1, openStart + 1 + oldSelector.length], newSelector), ]; // make sure we don't mangle self-closing tags - if (node.startSourceSpan.end.offset !== node.endSourceSpan.end.offset) { - const closeTagRange = [ - node.endSourceSpan.start.offset + 2, - node.endSourceSpan.end.offset - 1, - ]; - ops.push(fixer.replaceTextRange(closeTagRange, newSelector)); + if (endSourceSpan !== null && startSourceSpan.end.offset !== endSourceSpan.end.offset) { + const closeStart = endSourceSpan.start.offset as number; + const closeEnd = endSourceSpan.end.offset as number; + + ops.push(fixer.replaceTextRange([closeStart + 2, closeEnd - 1], newSelector)); } return ops; @@ -77,7 +87,7 @@ export const rule = { }, }; }, -}; +}); export const tests = { plugin: info.name, @@ -167,6 +177,6 @@ class Test { `, }, ], -}; +} as NamedTests; export default rule; diff --git a/lint/src/rules/ts/index.ts b/lint/src/rules/ts/index.ts index 2983d94386e..4ff38bd0c30 100644 --- a/lint/src/rules/ts/index.ts +++ b/lint/src/rules/ts/index.ts @@ -1,9 +1,17 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ import { bundle, RuleExports, } from '../../util/structure'; -import * as themedComponentUsages from './themed-component-usages'; +/* eslint-disable import/no-namespace */ import * as themedComponentSelectors from './themed-component-selectors'; +import * as themedComponentUsages from './themed-component-usages'; const index = [ themedComponentUsages, diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts index 5c455bf1de3..d02883de742 100644 --- a/lint/src/rules/ts/themed-component-selectors.ts +++ b/lint/src/rules/ts/themed-component-selectors.ts @@ -5,9 +5,13 @@ * * http://www.dspace.org/license/ */ -import { ESLintUtils } from '@typescript-eslint/utils'; -import { fixture } from '../../../test/fixture'; +import { + ESLintUtils, + TSESLint, + TSESTree, +} from '@typescript-eslint/utils'; +import { fixture } from '../../../test/fixture'; import { getComponentSelectorNode } from '../../util/angular'; import { stringLiteral } from '../../util/misc'; import { DSpaceESLintRuleInfo } from '../../util/structure'; @@ -16,6 +20,7 @@ import { isThemeableComponent, isThemedComponentWrapper, } from '../../util/theme-support'; +import { getFilename } from '../../util/typescript'; export enum Message { BASE = 'wrongSelectorUnthemedComponent', @@ -53,41 +58,43 @@ Unit tests are exempt from this rule, because they may redefine components using export const rule = ESLintUtils.RuleCreator.withoutDocs({ ...info, - create(context: any): any { - if (context.getFilename()?.endsWith('.spec.ts')) { + create(context: TSESLint.RuleContext) { + const filename = getFilename(context); + + if (filename.endsWith('.spec.ts')) { return {}; } - function enforceWrapperSelector(selectorNode: any) { + function enforceWrapperSelector(selectorNode: TSESTree.StringLiteral) { if (selectorNode?.value.startsWith('ds-themed-')) { context.report({ messageId: Message.WRAPPER, node: selectorNode, - fix(fixer: any) { + fix(fixer) { return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-'))); }, }); } } - function enforceBaseSelector(selectorNode: any) { + function enforceBaseSelector(selectorNode: TSESTree.StringLiteral) { if (!selectorNode?.value.startsWith('ds-base-')) { context.report({ messageId: Message.BASE, node: selectorNode, - fix(fixer: any) { + fix(fixer) { return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-'))); }, }); } } - function enforceThemedSelector(selectorNode: any) { + function enforceThemedSelector(selectorNode: TSESTree.StringLiteral) { if (!selectorNode?.value.startsWith('ds-themed-')) { context.report({ messageId: Message.THEMED, node: selectorNode, - fix(fixer: any) { + fix(fixer) { return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-'))); }, }); @@ -95,11 +102,15 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ } return { - 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: any) { - // keep track of all @Component nodes by their selector + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) { const selectorNode = getComponentSelectorNode(node); + + if (selectorNode === undefined) { + return; + } + const selector = selectorNode?.value; - const classNode = node.parent; + const classNode = node.parent as TSESTree.ClassDeclaration; const className = classNode.id?.name; if (selector === undefined || className === undefined) { @@ -108,7 +119,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ if (isThemedComponentWrapper(node)) { enforceWrapperSelector(selectorNode); - } else if (inThemedComponentOverrideFile(context)) { + } else if (inThemedComponentOverrideFile(filename)) { enforceThemedSelector(selectorNode); } else if (isThemeableComponent(className)) { enforceBaseSelector(selectorNode); @@ -124,11 +135,11 @@ export const tests = { { name: 'Regular non-themeable component selector', code: ` - @Component({ - selector: 'ds-something', - }) - class Something { - } +@Component({ + selector: 'ds-something', +}) +class Something { +} `, }, { diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index 54b93363cb6..d9cc3127ed8 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -5,9 +5,13 @@ * * http://www.dspace.org/license/ */ -import { ESLintUtils } from '@typescript-eslint/utils'; +import { + ESLintUtils, + TSESLint, + TSESTree, +} from '@typescript-eslint/utils'; + import { fixture } from '../../../test/fixture'; -import { findUsages } from '../../util/misc'; import { DSpaceESLintRuleInfo } from '../../util/structure'; import { allThemeableComponents, @@ -17,6 +21,10 @@ import { inThemedComponentFile, isAllowedUnthemedUsage, } from '../../util/theme-support'; +import { + findUsages, + getFilename, +} from '../../util/typescript'; export enum Message { WRONG_CLASS = 'mustUseThemedWrapperClass', @@ -52,8 +60,10 @@ There are a few exceptions where the base class can still be used: export const rule = ESLintUtils.RuleCreator.withoutDocs({ ...info, - create(context: any, options: any): any { - function handleUnthemedUsagesInTypescript(node: any) { + create(context: TSESLint.RuleContext) { + const filename = getFilename(context); + + function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) { if (isAllowedUnthemedUsage(node)) { return; } @@ -68,24 +78,24 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ context.report({ messageId: Message.WRONG_CLASS, node: node, - fix(fixer: any) { + fix(fixer) { return fixer.replaceText(node, entry.wrapperClass); }, }); } - function handleThemedSelectorQueriesInTests(node: any) { + function handleThemedSelectorQueriesInTests(node: TSESTree.Literal) { context.report({ node, messageId: Message.WRONG_SELECTOR, - fix(fixer: any){ + fix(fixer){ const newSelector = fixSelectors(node.raw); return fixer.replaceText(node, newSelector); }, }); } - function handleUnthemedImportsInTypescript(specifierNode: any) { + function handleUnthemedImportsInTypescript(specifierNode: TSESTree.ImportSpecifier) { const allUsages = findUsages(context, specifierNode.local); const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage)); @@ -94,7 +104,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ } const importedNode = specifierNode.imported; - const declarationNode = specifierNode.parent; + const declarationNode = specifierNode.parent as TSESTree.ImportDeclaration; const entry = getThemeableComponentByBaseClass(importedNode.name); if (entry === undefined) { @@ -105,7 +115,7 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ context.report({ messageId: Message.WRONG_IMPORT, node: importedNode, - fix(fixer: any) { + fix(fixer) { const ops = []; const oldImportSource = declarationNode.source.value; @@ -128,17 +138,17 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ } // ignore tests and non-routing modules - if (context.getFilename()?.endsWith('.spec.ts')) { + if (filename.endsWith('.spec.ts')) { return { [`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, }; - } else if (context.getFilename()?.endsWith('.cy.ts')) { + } else if (filename.endsWith('.cy.ts')) { return { [`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, }; } else if ( - context.getFilename()?.match(/(?!routing).module.ts$/) - || context.getFilename()?.match(/themed-.+\.component\.ts$/) + filename.match(/(?!routing).module.ts$/) + || filename.match(/themed-.+\.component\.ts$/) || inThemedComponentFile(context) ) { // do nothing diff --git a/lint/src/util/angular.ts b/lint/src/util/angular.ts index cb122a16dcc..7bff24718c0 100644 --- a/lint/src/util/angular.ts +++ b/lint/src/util/angular.ts @@ -5,12 +5,24 @@ * * http://www.dspace.org/license/ */ +import { TSESTree } from '@typescript-eslint/utils'; -export function getComponentSelectorNode(componentDecoratorNode: any): any | undefined { - for (const property of componentDecoratorNode.expression.arguments[0].properties) { - if (property.key?.name === 'selector') { - return property?.value; +import { getObjectPropertyNodeByName } from './typescript'; + +export function getComponentSelectorNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.StringLiteral | undefined { + const initializer = (componentDecoratorNode.expression as TSESTree.CallExpression).arguments[0] as TSESTree.ObjectExpression; + const property = getObjectPropertyNodeByName(initializer, 'selector'); + + if (property !== undefined) { + // todo: support template literals as well + if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'string') { + return property as TSESTree.StringLiteral; } } + return undefined; } + +export function isPartOfViewChild(node: TSESTree.Identifier): boolean { + return (node.parent as any)?.callee?.name === 'ViewChild'; +} diff --git a/lint/src/util/misc.ts b/lint/src/util/misc.ts index 1cd610fcd7e..47357e7cd31 100644 --- a/lint/src/util/misc.ts +++ b/lint/src/util/misc.ts @@ -6,37 +6,11 @@ * http://www.dspace.org/license/ */ -export function stringLiteral(value: string): string { - return `'${value}'`; -} - export function match(rangeA: number[], rangeB: number[]) { return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1]; } -export function findUsages(context: any, localNode: any): any[] { - const ast = context.getSourceCode().ast; - - const usages: any[] = []; - - for (const token of ast.tokens) { - if (token.type === 'Identifier' && token.value === localNode.name && !match(token.range, localNode.range)) { - usages.push(context.getSourceCode().getNodeByRangeIndex(token.range[0])); - } - } - - return usages; -} - -export function isPartOfTypeExpression(node: any): boolean { - return node.parent.type.startsWith('TSType'); -} - -export function isClassDeclaration(node: any): boolean { - return node.parent.type === 'ClassDeclaration'; -} - -export function isPartOfViewChild(node: any): boolean { - return node.parent?.callee?.name === 'ViewChild'; +export function stringLiteral(value: string): string { + return `'${value}'`; } diff --git a/lint/src/util/structure.ts b/lint/src/util/structure.ts index 13535bfe17e..bfbf7ec7f27 100644 --- a/lint/src/util/structure.ts +++ b/lint/src/util/structure.ts @@ -10,53 +10,42 @@ import { RuleTester } from 'eslint'; import { EnumType } from 'typescript'; export type Meta = TSESLint.RuleMetaData; -export type Valid = RuleTester.ValidTestCase | TSESLint.ValidTestCase; -export type Invalid = RuleTester.InvalidTestCase | TSESLint.InvalidTestCase; - +export type Valid = TSESLint.ValidTestCase | RuleTester.ValidTestCase; +export type Invalid = TSESLint.InvalidTestCase | RuleTester.InvalidTestCase; export interface DSpaceESLintRuleInfo { name: string; meta: Meta, - defaultOptions: any[], + defaultOptions: unknown[], } -export interface DSpaceESLintTestInfo { - rule: string; +export interface NamedTests { + plugin: string; valid: Valid[]; invalid: Invalid[]; } -export interface DSpaceESLintPluginInfo { - name: string; - description: string; - rules: DSpaceESLintRuleInfo; - tests: DSpaceESLintTestInfo; -} - -export interface DSpaceESLintInfo { - html: DSpaceESLintPluginInfo; - ts: DSpaceESLintPluginInfo; -} - export interface RuleExports { Message: EnumType, info: DSpaceESLintRuleInfo, - rule: any, - tests: any, - default: any, + rule: TSESLint.RuleModule, + tests: NamedTests, + default: unknown, } -export function bundle( +export interface PluginExports { name: string, language: string, + rules: Record, index: RuleExports[], -): { +} + +export function bundle( name: string, language: string, - rules: Record, index: RuleExports[], -} { - return index.reduce((o: any, i: any) => { +): PluginExports { + return index.reduce((o: PluginExports, i: RuleExports) => { o.rules[i.info.name] = i.rule; return o; }, { diff --git a/lint/src/util/theme-support.ts b/lint/src/util/theme-support.ts index 18eed48452d..6a3807a536b 100644 --- a/lint/src/util/theme-support.ts +++ b/lint/src/util/theme-support.ts @@ -6,17 +6,18 @@ * http://www.dspace.org/license/ */ +import { TSESTree } from '@typescript-eslint/utils'; import { readFileSync } from 'fs'; import { basename } from 'path'; -import ts from 'typescript'; +import ts, { Identifier } from 'typescript'; +import { isPartOfViewChild } from './angular'; import { - isClassDeclaration, + AnyRuleContext, + getFilename, + isPartOfClassDeclaration, isPartOfTypeExpression, - isPartOfViewChild, -} from './misc'; - -const glob = require('glob'); +} from './typescript'; /** * Couples a themeable Component to its ThemedComponent wrapper @@ -31,6 +32,42 @@ export interface ThemeableComponentRegistryEntry { wrapperClass: string; } +function isAngularComponentDecorator(node: ts.Node) { + if (node.kind === ts.SyntaxKind.Decorator && node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + const decorator = node as ts.Decorator; + + if (decorator.expression.kind === ts.SyntaxKind.CallExpression) { + const method = decorator.expression as ts.CallExpression; + + if (method.expression.kind === ts.SyntaxKind.Identifier) { + return (method.expression as Identifier).escapedText === 'Component'; + } + } + } + + return false; +} + +function findImportDeclaration(source: ts.SourceFile, identifierName: string): ts.ImportDeclaration | undefined { + return ts.forEachChild(source, (topNode: ts.Node) => { + if (topNode.kind === ts.SyntaxKind.ImportDeclaration) { + const importDeclaration = topNode as ts.ImportDeclaration; + + if (importDeclaration.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) { + const namedImports = importDeclaration.importClause?.namedBindings as ts.NamedImports; + + for (const element of namedImports.elements) { + if (element.name.escapedText === identifierName) { + return importDeclaration; + } + } + } + } + + return undefined; + }); +} + /** * Listing of all themeable Components */ @@ -55,32 +92,45 @@ class ThemeableComponentRegistry { function registerWrapper(path: string) { const source = getSource(path); - function traverse(node: any) { - if (node.kind === ts.SyntaxKind.Decorator && node.expression.expression.escapedText === 'Component' && node.parent.kind === ts.SyntaxKind.ClassDeclaration) { - const wrapperClass = node.parent.name.escapedText; + function traverse(node: ts.Node) { + if (node.parent !== undefined && isAngularComponentDecorator(node)) { + const classNode = node.parent as ts.ClassDeclaration; + + if (classNode.name === undefined || classNode.heritageClauses === undefined) { + return; + } - for (const heritageClause of node.parent.heritageClauses) { + const wrapperClass = classNode.name?.escapedText as string; + + for (const heritageClause of classNode.heritageClauses) { for (const type of heritageClause.types) { - if (type.expression.escapedText === 'ThemedComponent') { - const baseClass = type.typeArguments[0].typeName?.escapedText; - - ts.forEachChild(source, (topNode: any) => { - if (topNode.kind === ts.SyntaxKind.ImportDeclaration) { - for (const element of topNode.importClause.namedBindings.elements) { - if (element.name.escapedText === baseClass) { - const basePath = resolveLocalPath(topNode.moduleSpecifier.text, path); - - themeableComponents.add({ - baseClass, - basePath: basePath.replace(new RegExp(`^${prefix}`), ''), - baseFileName: basename(basePath).replace(/\.ts$/, ''), - wrapperClass, - wrapperPath: path.replace(new RegExp(`^${prefix}`), ''), - wrapperFileName: basename(path).replace(/\.ts$/, ''), - }); - } - } - } + if ((type as any).expression.escapedText === 'ThemedComponent') { + if (type.kind !== ts.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) { + continue; + } + + const firstTypeArg = type.typeArguments[0] as ts.TypeReferenceNode; + const baseClass = (firstTypeArg.typeName as ts.Identifier)?.escapedText; + + if (baseClass === undefined) { + continue; + } + + const importDeclaration = findImportDeclaration(source, baseClass); + + if (importDeclaration === undefined) { + continue; + } + + const basePath = resolveLocalPath((importDeclaration.moduleSpecifier as ts.StringLiteral).text, path); + + themeableComponents.add({ + baseClass, + basePath: basePath.replace(new RegExp(`^${prefix}`), ''), + baseFileName: basename(basePath).replace(/\.ts$/, ''), + wrapperClass, + wrapperPath: path.replace(new RegExp(`^${prefix}`), ''), + wrapperFileName: basename(path).replace(/\.ts$/, ''), }); } } @@ -95,6 +145,8 @@ class ThemeableComponentRegistry { traverse(source); } + const glob = require('glob'); + const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found; for (const wrapper of wrappers) { @@ -142,8 +194,16 @@ function resolveLocalPath(path: string, relativeTo: string) { } } -export function isThemedComponentWrapper(node: any): boolean { - return node.parent.superClass?.name === 'ThemedComponent'; +export function isThemedComponentWrapper(decoratorNode: TSESTree.Decorator): boolean { + if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return false; + } + + if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) { + return false; + } + + return (decoratorNode.parent.superClass as any)?.name === 'ThemedComponent'; } export function isThemeableComponent(className: string): boolean { @@ -151,8 +211,8 @@ export function isThemeableComponent(className: string): boolean { return themeableComponents.byBaseClass.has(className); } -export function inThemedComponentOverrideFile(context: any): boolean { - const match = context.getFilename().match(/src\/themes\/[^\/]+\/(app\/.*)/); +export function inThemedComponentOverrideFile(filename: string): boolean { + const match = filename.match(/src\/themes\/[^\/]+\/(app\/.*)/); if (!match) { return false; @@ -162,13 +222,14 @@ export function inThemedComponentOverrideFile(context: any): boolean { return themeableComponents.byBasePath.has(`src/${match[1]}`); } -export function inThemedComponentFile(context: any): boolean { +export function inThemedComponentFile(context: AnyRuleContext): boolean { themeableComponents.initialize(); + const filename = getFilename(context); return [ - () => themeableComponents.byBasePath.has(context.getFilename()), - () => themeableComponents.byWrapperPath.has(context.getFilename()), - () => inThemedComponentOverrideFile(context), + () => themeableComponents.byBasePath.has(filename), + () => themeableComponents.byWrapperPath.has(filename), + () => inThemedComponentOverrideFile(filename), ].some(predicate => predicate()); } @@ -182,8 +243,8 @@ export function getThemeableComponentByBaseClass(baseClass: string): ThemeableCo return themeableComponents.byBaseClass.get(baseClass); } -export function isAllowedUnthemedUsage(usageNode: any) { - return isClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode); +export function isAllowedUnthemedUsage(usageNode: TSESTree.Identifier) { + return isPartOfClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode); } export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-'; diff --git a/lint/src/util/typescript.ts b/lint/src/util/typescript.ts new file mode 100644 index 00000000000..5b7bdb858dd --- /dev/null +++ b/lint/src/util/typescript.ts @@ -0,0 +1,75 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + TSESLint, + TSESTree, +} from '@typescript-eslint/utils'; + +import { match } from './misc'; + +export type AnyRuleContext = TSESLint.RuleContext; + +export function getFilename(context: AnyRuleContext): string { + // TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?) + // eslint-disable-next-line deprecation/deprecation + return context.getFilename(); +} + +export function getSourceCode(context: AnyRuleContext): TSESLint.SourceCode { + // TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?) + // eslint-disable-next-line deprecation/deprecation + return context.getSourceCode(); +} + +export function getObjectPropertyNodeByName(objectNode: TSESTree.ObjectExpression, propertyName: string): TSESTree.Node | undefined { + for (const propertyNode of objectNode.properties) { + if ( + propertyNode.type === TSESTree.AST_NODE_TYPES.Property + && ( + ( + propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Identifier + && propertyNode.key?.name === propertyName + ) || ( + propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Literal + && propertyNode.key?.value === propertyName + ) + ) + ) { + return propertyNode.value; + } + } + return undefined; +} + +export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifier): TSESTree.Identifier[] { + const source = getSourceCode(context); + + const usages: TSESTree.Identifier[] = []; + + for (const token of source.ast.tokens) { + if (token.type === 'Identifier' && token.value === localNode.name && !match(token.range, localNode.range)) { + const node = source.getNodeByRangeIndex(token.range[0]); + if (node !== null) { + usages.push(node as TSESTree.Identifier); + } + } + } + + return usages; +} + +export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean { + return node.parent.type.startsWith('TSType'); +} + +export function isPartOfClassDeclaration(node: TSESTree.Identifier): boolean { + if (node.parent === undefined) { + return false; + } + return node.parent.type === 'ClassDeclaration'; +} diff --git a/lint/test/rules.spec.ts b/lint/test/rules.spec.ts index a8c1b382b22..11c9bec46cf 100644 --- a/lint/test/rules.spec.ts +++ b/lint/test/rules.spec.ts @@ -15,7 +15,7 @@ import { describe('TypeScript rules', () => { for (const { info, rule, tests } of tsPlugin.index) { - tsRuleTester.run(info.name, rule, tests); + tsRuleTester.run(info.name, rule, tests as any); } }); diff --git a/lint/test/testing.ts b/lint/test/testing.ts index f4f92a0e631..f86870ec29e 100644 --- a/lint/test/testing.ts +++ b/lint/test/testing.ts @@ -8,13 +8,13 @@ import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester'; import { RuleTester } from 'eslint'; + +import { themeableComponents } from '../src/util/theme-support'; import { FIXTURE, fixture, } from './fixture'; -import { themeableComponents } from '../src/util/theme-support'; - // Register themed components from test fixture themeableComponents.initialize(FIXTURE); diff --git a/package.json b/package.json index 5ef876c5600..9351afde7b5 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "@angular-builders/custom-webpack": "~15.0.0", "@angular-devkit/build-angular": "^15.2.6", "@angular-eslint/builder": "15.2.1", + "@angular-eslint/bundled-angular-compiler": "^17.2.1", "@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/schematics": "15.2.1", diff --git a/yarn.lock b/yarn.lock index a137a12cbd1..e01899307c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -291,6 +291,11 @@ resolved "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-15.2.1.tgz" integrity sha512-LO7Am8eVCr7oh6a0VmKSL7K03CnQEQhFO7Wt/YtbfYOxVjrbwmYLwJn+wZPOT7A02t/BttOD/WXuDrOWtSMQ/Q== +"@angular-eslint/bundled-angular-compiler@^17.2.1": + version "17.2.1" + resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.2.1.tgz#d849b0845371b41856b9f598af81ce5bf799bca0" + integrity sha512-puC0itsZv2QlrDOCcWtq1KZH+DvfrpV+mV78HHhi6+h25R5iIhr8ARKcl3EQxFjvrFq34jhG8pSupxKvFbHVfA== + "@angular-eslint/eslint-plugin-template@15.2.1": version "15.2.1" resolved "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-15.2.1.tgz" From 568574585b90314c6c5765f5f8963f32b30f0039 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 21 Mar 2024 10:33:13 +0100 Subject: [PATCH 14/50] Workaround/document edge case where node can't be found by token --- lint/src/rules/ts/themed-component-usages.ts | 63 ++++++++++++++++++++ lint/src/util/typescript.ts | 8 +-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index d9cc3127ed8..1263e44b48a 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -335,6 +335,69 @@ cy.get('ds-themeable'); cy.get('#test > ds-themeable > #nest'); `, }, + { + name: 'edge case: unable to find usage node through usage token, but import is still flagged and fixed', + code: ` +import { Component } from '@angular/core'; + +import { Context } from '../../core/shared/context.model'; +import { TestThemeableComponent } from '../test/test-themeable.component.ts'; + +@Component({ + selector: 'ds-admin-search-page', + templateUrl: './admin-search-page.component.html', + styleUrls: ['./admin-search-page.component.scss'], + standalone: true, + imports: [ + TestThemeableComponent + ], +}) + +/** + * Component that represents a search page for administrators + */ +export class AdminSearchPageComponent { + /** + * The context of this page + */ + context: Context = Context.AdminSearch; +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { Component } from '@angular/core'; + +import { Context } from '../../core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; + +@Component({ + selector: 'ds-admin-search-page', + templateUrl: './admin-search-page.component.html', + styleUrls: ['./admin-search-page.component.scss'], + standalone: true, + imports: [ + ThemedTestThemeableComponent + ], +}) + +/** + * Component that represents a search page for administrators + */ +export class AdminSearchPageComponent { + /** + * The context of this page + */ + context: Context = Context.AdminSearch; +} + `, + }, ], }; diff --git a/lint/src/util/typescript.ts b/lint/src/util/typescript.ts index 5b7bdb858dd..90b0f2c49f2 100644 --- a/lint/src/util/typescript.ts +++ b/lint/src/util/typescript.ts @@ -54,6 +54,7 @@ export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifi for (const token of source.ast.tokens) { if (token.type === 'Identifier' && token.value === localNode.name && !match(token.range, localNode.range)) { const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) if (node !== null) { usages.push(node as TSESTree.Identifier); } @@ -64,12 +65,9 @@ export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifi } export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean { - return node.parent.type.startsWith('TSType'); + return node.parent?.type?.startsWith('TSType'); } export function isPartOfClassDeclaration(node: TSESTree.Identifier): boolean { - if (node.parent === undefined) { - return false; - } - return node.parent.type === 'ClassDeclaration'; + return node.parent?.type === 'ClassDeclaration'; } From e40b6ae612035135fe417ba9ec551c28fbbd5dce Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 21 Mar 2024 10:37:20 +0100 Subject: [PATCH 15/50] Update plugins to support standalone components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ThemedComponent wrappers should always import their base component. This ensures that it's always enough to only import the wrapper when we use it. - This implies that all themeable components must be standalone → added rules to enforce this → updated usage rule to improve declaration/import handling --- .eslintrc.json | 1 + lint/src/rules/ts/index.ts | 4 +- lint/src/rules/ts/themed-component-classes.ts | 378 ++++++++++++++++++ lint/src/rules/ts/themed-component-usages.ts | 201 +++++++--- lint/src/util/angular.ts | 59 ++- lint/src/util/fix.ts | 125 ++++++ lint/src/util/theme-support.ts | 25 +- lint/src/util/typescript.ts | 74 ++++ .../src/app/test/test-themeable.component.ts | 1 + .../test/themed-test-themeable.component.ts | 2 + .../app/test/other-themeable.component.ts | 16 + .../fixture/src/themes/test/test.module.ts | 2 + lint/test/fixture/tsconfig.json | 1 + lint/test/testing.ts | 1 + 14 files changed, 835 insertions(+), 55 deletions(-) create mode 100644 lint/src/rules/ts/themed-component-classes.ts create mode 100644 lint/src/util/fix.ts create mode 100644 lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts diff --git a/.eslintrc.json b/.eslintrc.json index ce5d1225b07..147aaeb463f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -245,6 +245,7 @@ "rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-classes": "error", "dspace-angular-ts/themed-component-selectors": "error", "dspace-angular-ts/themed-component-usages": "error" } diff --git a/lint/src/rules/ts/index.ts b/lint/src/rules/ts/index.ts index 4ff38bd0c30..a7fdfe41efe 100644 --- a/lint/src/rules/ts/index.ts +++ b/lint/src/rules/ts/index.ts @@ -10,12 +10,14 @@ import { RuleExports, } from '../../util/structure'; /* eslint-disable import/no-namespace */ +import * as themedComponentClasses from './themed-component-classes'; import * as themedComponentSelectors from './themed-component-selectors'; import * as themedComponentUsages from './themed-component-usages'; const index = [ - themedComponentUsages, + themedComponentClasses, themedComponentSelectors, + themedComponentUsages, ] as unknown as RuleExports[]; export = { diff --git a/lint/src/rules/ts/themed-component-classes.ts b/lint/src/rules/ts/themed-component-classes.ts new file mode 100644 index 00000000000..727785c6d70 --- /dev/null +++ b/lint/src/rules/ts/themed-component-classes.ts @@ -0,0 +1,378 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + ESLintUtils, + TSESLint, + TSESTree, +} from '@typescript-eslint/utils'; + +import { fixture } from '../../../test/fixture'; +import { + getComponentImportNode, + getComponentInitializer, + getComponentStandaloneNode, +} from '../../util/angular'; +import { appendObjectProperties } from '../../util/fix'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; +import { + getBaseComponentClassName, + inThemedComponentOverrideFile, + isThemeableComponent, + isThemedComponentWrapper, +} from '../../util/theme-support'; +import { getFilename } from '../../util/typescript'; + +export enum Message { + NOT_STANDALONE = 'mustBeStandalone', + NOT_STANDALONE_IMPORTS_BASE = 'mustBeStandaloneAndImportBase', + WRAPPER_IMPORTS_BASE = 'wrapperShouldImportBase', +} + +export const info = { + name: 'themed-component-classes', + meta: { + docs: { + description: `Formatting rules for themeable component classes`, + }, + type: 'problem', + fixable: 'code', + schema: [], + messages: { + [Message.NOT_STANDALONE]: 'Themeable components must be standalone', + [Message.NOT_STANDALONE_IMPORTS_BASE]: 'Themeable component wrapper classes must be standalone and import the base class', + [Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must import the base class', + }, + }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, + create(context: TSESLint.RuleContext) { + const filename = getFilename(context); + + if (filename.endsWith('.spec.ts')) { + return {}; + } + + function enforceStandalone(decoratorNode: TSESTree.Decorator, withBaseImport = false) { + const standaloneNode = getComponentStandaloneNode(decoratorNode); + + if (standaloneNode === undefined) { + // We may need to add these properties in one go + if (!withBaseImport) { + context.report({ + messageId: Message.NOT_STANDALONE, + node: decoratorNode, + fix(fixer) { + const initializer = getComponentInitializer(decoratorNode); + return appendObjectProperties(context, fixer, initializer, ['standalone: true']); + }, + }); + } + } else if (!standaloneNode.value) { + context.report({ + messageId: Message.NOT_STANDALONE, + node: standaloneNode, + fix(fixer) { + return fixer.replaceText(standaloneNode, 'true'); + }, + }); + } + + if (withBaseImport) { + const baseClass = getBaseComponentClassName(decoratorNode); + + if (baseClass === undefined) { + return; + } + + const importsNode = getComponentImportNode(decoratorNode); + + if (importsNode === undefined) { + if (standaloneNode === undefined) { + context.report({ + messageId: Message.NOT_STANDALONE_IMPORTS_BASE, + node: decoratorNode, + fix(fixer) { + const initializer = getComponentInitializer(decoratorNode); + return appendObjectProperties(context, fixer, initializer, ['standalone: true', `imports: [${baseClass}]`]); + }, + }); + } else { + context.report({ + messageId: Message.WRAPPER_IMPORTS_BASE, + node: decoratorNode, + fix(fixer) { + const initializer = getComponentInitializer(decoratorNode); + return appendObjectProperties(context, fixer, initializer, [`imports: [${baseClass}]`]); + }, + }); + } + } else { + // If we have an imports node, standalone: true will be enforced by another rule + + const imports = importsNode.elements.map(e => (e as TSESTree.Identifier).name); + + if (!imports.includes(baseClass) || imports.length > 1) { + // The wrapper should _only_ import the base component + context.report({ + messageId: Message.WRAPPER_IMPORTS_BASE, + node: importsNode, + fix(fixer) { + // todo: this may leave unused imports, but that's better than mangling things + return fixer.replaceText(importsNode, `[${baseClass}]`); + }, + }); + } + } + } + } + + return { + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) { + const classNode = node.parent as TSESTree.ClassDeclaration; + const className = classNode.id?.name; + + if (className === undefined) { + return; + } + + if (isThemedComponentWrapper(node)) { + enforceStandalone(node, true); + } else if (inThemedComponentOverrideFile(filename)) { + enforceStandalone(node); + } else if (isThemeableComponent(className)) { + enforceStandalone(node); + } + }, + }; + }, +}); + +export const tests = { + plugin: info.name, + valid: [ + { + name: 'Regular non-themeable component', + code: ` +@Component({ + selector: 'ds-something', + standalone: true, +}) +class Something { +} + `, + }, + { + name: 'Base component', + code: ` +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableTomponent { +} + `, + }, + { + name: 'Wrapper component', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + TestThemeableComponent, + ], +}) +class ThemedTestThemeableTomponent extends ThemedComponent { +} + `, + }, + { + name: 'Override component', + filename: fixture('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-test-themable', + standalone: true, +}) +class Override extends BaseComponent { +} + `, + }, + ], + invalid: [ + { + name: 'Base component must be standalone', + code: ` +@Component({ + selector: 'ds-base-test-themable', +}) +class TestThemeableComponent { +} + `, + errors:[ + { + messageId: Message.NOT_STANDALONE, + }, + ], + output: ` +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableComponent { +} + `, + }, + { + name: 'Wrapper component must be standalone and import base component', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors:[ + { + messageId: Message.NOT_STANDALONE_IMPORTS_BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + + { + name: 'Wrapper component must import base component (array present but empty)', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors:[ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Wrapper component must import base component (array is wrong)', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +import { SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + SomethingElse, + ], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors:[ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +import { SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, { + name: 'Wrapper component must import base component (array is wrong)', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +import { Something, SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + SomethingElse, + ], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + errors:[ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +import { Something, SomethingElse } from './somewhere-else'; + +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + name: 'Override component must be standalone', + filename: fixture('src/themes/test/app/test/test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-themed-test-themable', +}) +class Override extends BaseComponent { +} + `, + errors:[ + { + messageId: Message.NOT_STANDALONE, + }, + ], + output: ` +@Component({ + selector: 'ds-themed-test-themable', + standalone: true, +}) +class Override extends BaseComponent { +} + `, + }, + ], +}; diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index 1263e44b48a..2387ea76dd2 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -12,6 +12,10 @@ import { } from '@typescript-eslint/utils'; import { fixture } from '../../../test/fixture'; +import { + removeWithCommas, + replaceOrRemoveArrayIdentifier, +} from '../../util/fix'; import { DSpaceESLintRuleInfo } from '../../util/structure'; import { allThemeableComponents, @@ -22,14 +26,18 @@ import { isAllowedUnthemedUsage, } from '../../util/theme-support'; import { + findImportSpecifier, findUsages, + findUsagesByName, getFilename, + relativePath, } from '../../util/typescript'; export enum Message { WRONG_CLASS = 'mustUseThemedWrapperClass', WRONG_IMPORT = 'mustImportThemedWrapper', WRONG_SELECTOR = 'mustUseThemedWrapperSelector', + BASE_IN_MODULE = 'baseComponentNotNeededInModule', } export const info = { @@ -53,6 +61,7 @@ There are a few exceptions where the base class can still be used: [Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper', [Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper', [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules', }, }, defaultOptions: [], @@ -79,7 +88,11 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ messageId: Message.WRONG_CLASS, node: node, fix(fixer) { - return fixer.replaceText(node, entry.wrapperClass); + if (node.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) { + return replaceOrRemoveArrayIdentifier(context, fixer, node, entry.wrapperClass); + } else { + return fixer.replaceText(node, entry.wrapperClass); + } }, }); } @@ -118,18 +131,36 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ fix(fixer) { const ops = []; - const oldImportSource = declarationNode.source.value; - const newImportLine = `import { ${entry.wrapperClass} } from '${oldImportSource.replace(entry.baseFileName, entry.wrapperFileName)}';`; + const wrapperImport = findImportSpecifier(context, entry.wrapperClass); - if (declarationNode.specifiers.length === 1) { - if (allUsages.length === badUsages.length) { - ops.push(fixer.replaceText(declarationNode, newImportLine)); + if (findUsagesByName(context, entry.wrapperClass).length === 0) { + // Wrapper is not present in this file, safe to add import + + const newImportLine = `import { ${entry.wrapperClass} } from '${relativePath(filename, entry.wrapperPath)}';`; + + if (declarationNode.specifiers.length === 1) { + if (allUsages.length === badUsages.length) { + ops.push(fixer.replaceText(declarationNode, newImportLine)); + } else if (wrapperImport === undefined) { + ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine)); + } } else { - ops.push(fixer.insertTextAfter(declarationNode, newImportLine)); + ops.push(...removeWithCommas(context, fixer, specifierNode)); + if (wrapperImport === undefined) { + ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine)); + } } } else { - ops.push(fixer.replaceText(specifierNode, entry.wrapperClass)); - ops.push(fixer.insertTextAfter(declarationNode, newImportLine)); + // Wrapper already present in the file, remove import instead + + if (allUsages.length === badUsages.length) { + if (declarationNode.specifiers.length === 1) { + // Make sure we remove the newline as well + ops.push(fixer.removeRange([declarationNode.range[0], declarationNode.range[1] + 1])); + } else { + ops.push(...removeWithCommas(context, fixer, specifierNode)); + } + } } return ops; @@ -147,9 +178,8 @@ export const rule = ESLintUtils.RuleCreator.withoutDocs({ [`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests, }; } else if ( - filename.match(/(?!routing).module.ts$/) + filename.match(/(?!src\/themes\/).*(?!routing).module.ts$/) || filename.match(/themed-.+\.component\.ts$/) - || inThemedComponentFile(context) ) { // do nothing return {}; @@ -174,7 +204,7 @@ export const tests = { { name: 'allow wrapper class usages', code: ` -import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; const config = { a: ThemedTestThemeableComponent, @@ -192,7 +222,7 @@ export class TestThemeableComponent { { name: 'allow inheriting from base class', code: ` -import { TestThemeableComponent } from '../test/test-themeable.component.ts'; +import { TestThemeableComponent } from './app/test/test-themeable.component'; export class ThemedAdminSidebarComponent extends ThemedComponent { } @@ -201,7 +231,7 @@ export class ThemedAdminSidebarComponent extends ThemedComponent ds-themeable > #nest'); { name: 'disallow direct usages of base class', code: ` -import { TestThemeableComponent } from '../test/test-themeable.component.ts'; -import { TestComponent } from '../test/test.component.ts'; +import { TestThemeableComponent } from './app/test/test-themeable.component'; +import { TestComponent } from './app/test/test.component'; const config = { a: TestThemeableComponent, @@ -246,8 +276,8 @@ const config = { }, ], output: ` -import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; -import { TestComponent } from '../test/test.component.ts'; +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; +import { TestComponent } from './app/test/test.component'; const config = { a: ThemedTestThemeableComponent, @@ -255,6 +285,61 @@ const config = { } `, }, + { + name: 'disallow direct usages of base class, keep other imports', + code: ` +import { Something, TestThemeableComponent } from './app/test/test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, + c: Something, +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { Something } from './app/test/test-themeable.component'; +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, + c: Something, +} + `, + }, + { + name: 'handle array replacements correctly', + code: ` +const DECLARATIONS = [ + Something, + TestThemeableComponent, + Something, + ThemedTestThemeableComponent, +]; + `, + errors: [ + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +const DECLARATIONS = [ + Something, + Something, + ThemedTestThemeableComponent, +]; + `, + }, { name: 'disallow override selector in test queries', filename: fixture('src/app/test/test.component.spec.ts'), @@ -337,30 +422,18 @@ cy.get('#test > ds-themeable > #nest'); }, { name: 'edge case: unable to find usage node through usage token, but import is still flagged and fixed', + filename: fixture('src/themes/test/app/test/other-themeable.component.ts'), code: ` import { Component } from '@angular/core'; -import { Context } from '../../core/shared/context.model'; -import { TestThemeableComponent } from '../test/test-themeable.component.ts'; +import { Context } from './app/core/shared/context.model'; +import { TestThemeableComponent } from '../../../../app/test/test-themeable.component'; @Component({ - selector: 'ds-admin-search-page', - templateUrl: './admin-search-page.component.html', - styleUrls: ['./admin-search-page.component.scss'], standalone: true, - imports: [ - TestThemeableComponent - ], + imports: [TestThemeableComponent], }) - -/** - * Component that represents a search page for administrators - */ -export class AdminSearchPageComponent { - /** - * The context of this page - */ - context: Context = Context.AdminSearch; +export class UsageComponent { } `, errors: [ @@ -374,27 +447,53 @@ export class AdminSearchPageComponent { output: ` import { Component } from '@angular/core'; -import { Context } from '../../core/shared/context.model'; -import { ThemedTestThemeableComponent } from '../test/themed-test-themeable.component.ts'; +import { Context } from './app/core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; @Component({ - selector: 'ds-admin-search-page', - templateUrl: './admin-search-page.component.html', - styleUrls: ['./admin-search-page.component.scss'], standalone: true, - imports: [ - ThemedTestThemeableComponent - ], + imports: [ThemedTestThemeableComponent], }) +export class UsageComponent { +} + `, + }, + { + name: 'edge case edge case: both are imported, only wrapper is retained', + filename: fixture('src/themes/test/app/test/other-themeable.component.ts'), + code: ` +import { Component } from '@angular/core'; -/** - * Component that represents a search page for administrators - */ -export class AdminSearchPageComponent { - /** - * The context of this page - */ - context: Context = Context.AdminSearch; +import { Context } from './app/core/shared/context.model'; +import { TestThemeableComponent } from '../../../../app/test/test-themeable.component'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [TestThemeableComponent, ThemedTestThemeableComponent], +}) +export class UsageComponent { +} + `, + errors: [ + { + messageId: Message.WRONG_IMPORT, + }, + { + messageId: Message.WRONG_CLASS, + }, + ], + output: ` +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [ThemedTestThemeableComponent], +}) +export class UsageComponent { } `, }, diff --git a/lint/src/util/angular.ts b/lint/src/util/angular.ts index 7bff24718c0..70ee903fb81 100644 --- a/lint/src/util/angular.ts +++ b/lint/src/util/angular.ts @@ -10,8 +10,7 @@ import { TSESTree } from '@typescript-eslint/utils'; import { getObjectPropertyNodeByName } from './typescript'; export function getComponentSelectorNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.StringLiteral | undefined { - const initializer = (componentDecoratorNode.expression as TSESTree.CallExpression).arguments[0] as TSESTree.ObjectExpression; - const property = getObjectPropertyNodeByName(initializer, 'selector'); + const property = getComponentInitializerNodeByName(componentDecoratorNode, 'selector'); if (property !== undefined) { // todo: support template literals as well @@ -23,6 +22,62 @@ export function getComponentSelectorNode(componentDecoratorNode: TSESTree.Decora return undefined; } +export function getComponentStandaloneNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.BooleanLiteral | undefined { + const property = getComponentInitializerNodeByName(componentDecoratorNode, 'standalone'); + + if (property !== undefined) { + if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'boolean') { + return property as TSESTree.BooleanLiteral; + } + } + + return undefined; +} +export function getComponentImportNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.ArrayExpression | undefined { + const property = getComponentInitializerNodeByName(componentDecoratorNode, 'imports'); + + if (property !== undefined) { + if (property.type === TSESTree.AST_NODE_TYPES.ArrayExpression) { + return property as TSESTree.ArrayExpression; + } + } + + return undefined; +} + +export function getComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined { + if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return undefined; + } + + if (decoratorNode.parent.id?.type !== TSESTree.AST_NODE_TYPES.Identifier) { + return undefined; + } + + return decoratorNode.parent.id.name; +} + +export function getComponentSuperClassName(decoratorNode: TSESTree.Decorator): string | undefined { + if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return undefined; + } + + if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) { + return undefined; + } + + return decoratorNode.parent.superClass.name; +} + +export function getComponentInitializer(componentDecoratorNode: TSESTree.Decorator): TSESTree.ObjectExpression { + return (componentDecoratorNode.expression as TSESTree.CallExpression).arguments[0] as TSESTree.ObjectExpression; +} + +export function getComponentInitializerNodeByName(componentDecoratorNode: TSESTree.Decorator, name: string): TSESTree.Node | undefined { + const initializer = getComponentInitializer(componentDecoratorNode); + return getObjectPropertyNodeByName(initializer, name); +} + export function isPartOfViewChild(node: TSESTree.Identifier): boolean { return (node.parent as any)?.callee?.name === 'ViewChild'; } diff --git a/lint/src/util/fix.ts b/lint/src/util/fix.ts new file mode 100644 index 00000000000..10408cc316c --- /dev/null +++ b/lint/src/util/fix.ts @@ -0,0 +1,125 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { TSESTree } from '@typescript-eslint/utils'; +import { + RuleContext, + RuleFix, + RuleFixer, +} from '@typescript-eslint/utils/ts-eslint'; + +import { getSourceCode } from './typescript'; + + + +export function appendObjectProperties(context: RuleContext, fixer: RuleFixer, objectNode: TSESTree.ObjectExpression, properties: string[]): RuleFix { + // todo: may not handle empty objects too well + const lastProperty = objectNode.properties[objectNode.properties.length - 1]; + const source = getSourceCode(context); + const nextToken = source.getTokenAfter(lastProperty); + + // todo: newline & indentation are hardcoded for @Component({}) + // todo: we're assuming that we need trailing commas, what if we don't? + const newPart = '\n' + properties.map(p => ` ${p},`).join('\n'); + + if (nextToken !== null && nextToken.value === ',') { + return fixer.insertTextAfter(nextToken, newPart); + } else { + return fixer.insertTextAfter(lastProperty, ',' + newPart); + } +} + +export function appendArrayElement(context: RuleContext, fixer: RuleFixer, arrayNode: TSESTree.ArrayExpression, value: string): RuleFix { + const source = getSourceCode(context); + + if (arrayNode.elements.length === 0) { + // This is the first element + const openArray = source.getTokenByRangeStart(arrayNode.range[0]); + + if (openArray == null) { + throw new Error('Unexpected null token for opening square bracket'); + } + + // safe to assume the list is single-line + return fixer.insertTextAfter(openArray, `${value}`); + } else { + const lastElement = arrayNode.elements[arrayNode.elements.length - 1]; + + if (lastElement == null) { + throw new Error('Unexpected null node in array'); + } + + const nextToken = source.getTokenAfter(lastElement); + + // todo: we don't know if the list is chopped or not, so we can't make any assumptions -- may produce output that will be flagged by other rules on the next run! + // todo: we're assuming that we need trailing commas, what if we don't? + if (nextToken !== null && nextToken.value === ',') { + return fixer.insertTextAfter(nextToken, ` ${value},`); + } else { + return fixer.insertTextAfter(lastElement, `, ${value},`); + } + } + +} + +export function isLast(elementNode: TSESTree.Node): boolean { + if (!elementNode.parent) { + return false; + } + + let siblingNodes: (TSESTree.Node | null)[] = [null]; + if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) { + siblingNodes = elementNode.parent.elements; + } else if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ImportDeclaration) { + siblingNodes = elementNode.parent.specifiers; + } + + return elementNode === siblingNodes[siblingNodes.length - 1]; +} + +export function removeWithCommas(context: RuleContext, fixer: RuleFixer, elementNode: TSESTree.Node): RuleFix[] { + const ops = []; + + const source = getSourceCode(context); + let nextToken = source.getTokenAfter(elementNode); + let prevToken = source.getTokenBefore(elementNode); + + if (nextToken !== null && prevToken !== null) { + if (nextToken.value === ',') { + nextToken = source.getTokenAfter(nextToken); + if (nextToken !== null) { + ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]])); + } + } + if (isLast(elementNode) && prevToken.value === ',') { + prevToken = source.getTokenBefore(prevToken); + if (prevToken !== null) { + ops.push(fixer.removeRange([prevToken.range[1], elementNode.range[1]])); + } + } + } else if (nextToken !== null) { + ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]])); + } + + return ops; +} + +export function replaceOrRemoveArrayIdentifier(context: RuleContext, fixer: RuleFixer, identifierNode: TSESTree.Identifier, newValue: string): RuleFix[] { + if (identifierNode.parent.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) { + throw new Error('Parent node is not an array expression!'); + } + + const array = identifierNode.parent as TSESTree.ArrayExpression; + + for (const element of array.elements) { + if (element !== null && element.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === newValue) { + return removeWithCommas(context, fixer, identifierNode); + } + } + + return [fixer.replaceText(identifierNode, newValue)]; +} diff --git a/lint/src/util/theme-support.ts b/lint/src/util/theme-support.ts index 6a3807a536b..2458b3f665c 100644 --- a/lint/src/util/theme-support.ts +++ b/lint/src/util/theme-support.ts @@ -11,7 +11,10 @@ import { readFileSync } from 'fs'; import { basename } from 'path'; import ts, { Identifier } from 'typescript'; -import { isPartOfViewChild } from './angular'; +import { + getComponentClassName, + isPartOfViewChild, +} from './angular'; import { AnyRuleContext, getFilename, @@ -74,12 +77,14 @@ function findImportDeclaration(source: ts.SourceFile, identifierName: string): t class ThemeableComponentRegistry { public readonly entries: Set; public readonly byBaseClass: Map; + public readonly byWrapperClass: Map; public readonly byBasePath: Map; public readonly byWrapperPath: Map; constructor() { this.entries = new Set(); this.byBaseClass = new Map(); + this.byWrapperClass = new Map(); this.byBasePath = new Map(); this.byWrapperPath = new Map(); } @@ -157,6 +162,7 @@ class ThemeableComponentRegistry { private add(entry: ThemeableComponentRegistryEntry) { this.entries.add(entry); this.byBaseClass.set(entry.baseClass, entry); + this.byWrapperClass.set(entry.wrapperClass, entry); this.byBasePath.set(entry.basePath, entry); this.byWrapperPath.set(entry.wrapperPath, entry); } @@ -206,6 +212,23 @@ export function isThemedComponentWrapper(decoratorNode: TSESTree.Decorator): boo return (decoratorNode.parent.superClass as any)?.name === 'ThemedComponent'; } +export function getBaseComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined { + const wrapperClass = getComponentClassName(decoratorNode); + + if (wrapperClass === undefined) { + return; + } + + themeableComponents.initialize(); + const entry = themeableComponents.byWrapperClass.get(wrapperClass); + + if (entry === undefined) { + return undefined; + } + + return entry.baseClass; +} + export function isThemeableComponent(className: string): boolean { themeableComponents.initialize(); return themeableComponents.byBaseClass.has(className); diff --git a/lint/src/util/typescript.ts b/lint/src/util/typescript.ts index 90b0f2c49f2..dca83637d78 100644 --- a/lint/src/util/typescript.ts +++ b/lint/src/util/typescript.ts @@ -64,6 +64,24 @@ export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifi return usages; } +export function findUsagesByName(context: AnyRuleContext, identifier: string): TSESTree.Identifier[] { + const source = getSourceCode(context); + + const usages: TSESTree.Identifier[] = []; + + for (const token of source.ast.tokens) { + if (token.type === 'Identifier' && token.value === identifier) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node !== null) { + usages.push(node as TSESTree.Identifier); + } + } + } + + return usages; +} + export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean { return node.parent?.type?.startsWith('TSType'); } @@ -71,3 +89,59 @@ export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean { export function isPartOfClassDeclaration(node: TSESTree.Identifier): boolean { return node.parent?.type === 'ClassDeclaration'; } + +function fromSrc(path: string): string { + const m = path.match(/^.*(src\/.+)(\.(ts|json|js)?)$/); + + if (m) { + return m[1]; + } else { + throw new Error(`Can't infer project-absolute TS/resource path from: ${path}`); + } +} + + +export function relativePath(thisFile: string, importFile: string): string { + const fromParts = fromSrc(thisFile).split('/'); + const toParts = fromSrc(importFile).split('/'); + + let lastCommon = 0; + for (let i = 0; i < fromParts.length - 1; i++) { + if (fromParts[i] === toParts[i]) { + lastCommon++; + } else { + break; + } + } + + const path = toParts.slice(lastCommon, toParts.length).join('/'); + const backtrack = fromParts.length - lastCommon - 1; + + let prefix: string; + if (backtrack > 0) { + prefix = '../'.repeat(backtrack); + } else { + prefix = './'; + } + + return prefix + path; +} + + +export function findImportSpecifier(context: AnyRuleContext, identifier: string): TSESTree.ImportSpecifier | undefined { + const source = getSourceCode(context); + + const usages: TSESTree.Identifier[] = []; + + for (const token of source.ast.tokens) { + if (token.type === 'Identifier' && token.value === identifier) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node && node.parent && node.parent.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) { + return node.parent; + } + } + } + + return undefined; +} diff --git a/lint/test/fixture/src/app/test/test-themeable.component.ts b/lint/test/fixture/src/app/test/test-themeable.component.ts index bd731d8afae..b445040539c 100644 --- a/lint/test/fixture/src/app/test/test-themeable.component.ts +++ b/lint/test/fixture/src/app/test/test-themeable.component.ts @@ -10,6 +10,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'ds-base-test-themeable', template: '', + standalone: true, }) export class TestThemeableComponent { } diff --git a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts index a45f89b6062..2697a8c598e 100644 --- a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts +++ b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts @@ -13,6 +13,8 @@ import { TestThemeableComponent } from './test-themeable.component'; @Component({ selector: 'ds-test-themeable', template: '', + standalone: true, + imports: [TestThemeableComponent], }) export class ThemedTestThemeableComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts b/lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts new file mode 100644 index 00000000000..f72161b2bfc --- /dev/null +++ b/lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts @@ -0,0 +1,16 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-themed-test-themeable', + template: '', +}) +export class OtherThemeableComponent { + +} diff --git a/lint/test/fixture/src/themes/test/test.module.ts b/lint/test/fixture/src/themes/test/test.module.ts index 7aac91b07a7..ff6ec3b2c0e 100644 --- a/lint/test/fixture/src/themes/test/test.module.ts +++ b/lint/test/fixture/src/themes/test/test.module.ts @@ -8,11 +8,13 @@ // @ts-ignore import { NgModule } from '@angular/core'; +import { OtherThemeableComponent } from './app/test/other-themeable.component'; import { TestThemeableComponent } from './app/test/test-themeable.component'; @NgModule({ declarations: [ TestThemeableComponent, + OtherThemeableComponent, ], }) export class TestModule { diff --git a/lint/test/fixture/tsconfig.json b/lint/test/fixture/tsconfig.json index 1fd3745ec84..0b61883e35f 100644 --- a/lint/test/fixture/tsconfig.json +++ b/lint/test/fixture/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../../tsconfig.json", "include": [ + "src/**/*.ts", "src/**/*.ts" ], "exclude": ["dist"] diff --git a/lint/test/testing.ts b/lint/test/testing.ts index f86870ec29e..cfa54c5b85c 100644 --- a/lint/test/testing.ts +++ b/lint/test/testing.ts @@ -20,6 +20,7 @@ import { themeableComponents.initialize(FIXTURE); TypeScriptRuleTester.itOnly = fit; +TypeScriptRuleTester.itSkip = xit; export const tsRuleTester = new TypeScriptRuleTester({ parser: '@typescript-eslint/parser', From 6051b82821ff0d4099107558d9657d76f9f76001 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Thu, 28 Mar 2024 14:47:46 +0100 Subject: [PATCH 16/50] Automatically migrate to new themeable component convention --- cypress/e2e/login-modal.cy.ts | 26 +++++++++---------- cypress/e2e/search-navbar.cy.ts | 8 +++--- lint/src/rules/ts/themed-component-usages.ts | 1 - .../browse/bulk-access-browse.component.html | 4 +-- .../epeople-registry.component.html | 2 +- .../eperson-form/eperson-form.component.html | 4 +-- .../groups-registry.component.html | 2 +- .../admin-notify-logs-result.component.html | 4 +-- .../admin-search-page.component.ts | 4 +-- .../admin-sidebar/admin-sidebar.component.ts | 2 +- .../themed-admin-sidebar.component.ts | 3 ++- .../admin-workflow-page.component.ts | 4 +-- src/app/app.component.html | 4 +-- .../edit-bitstream-page.component.html | 6 ++--- .../edit-bitstream-page.component.ts | 2 +- .../themed-edit-bitstream-page.component.ts | 3 ++- src/app/breadcrumbs/breadcrumbs.component.ts | 2 +- .../themed-breadcrumbs.component.ts | 3 ++- .../browse-by-date.component.ts | 2 -- .../browse-by-metadata.component.html | 8 +++--- .../browse-by-metadata.component.ts | 2 -- .../browse-by-taxonomy.component.ts | 2 -- .../browse-by-title.component.ts | 2 -- .../collection-item-mapper.component.html | 4 +-- .../collection-page.component.html | 12 ++++----- .../collection-page.component.ts | 2 +- .../collection-source.component.html | 2 +- .../edit-item-template-page.component.html | 4 +-- .../edit-item-template-page.component.ts | 4 +-- ...hemed-edit-item-template-page.component.ts | 3 ++- .../themed-collection-page.component.ts | 3 ++- .../community-list-page.component.html | 2 +- .../community-list-page.component.ts | 2 +- .../community-list.component.html | 6 ++--- .../community-list.component.ts | 2 +- .../themed-community-list.component.ts | 3 ++- .../themed-community-list-page.component.ts | 3 ++- .../community-page.component.html | 10 +++---- .../community-page.component.ts | 2 +- ...ty-page-sub-collection-list.component.html | 2 +- ...nity-page-sub-collection-list.component.ts | 2 +- ...nity-page-sub-collection-list.component.ts | 3 ++- .../sub-com-col-section.component.html | 8 +++--- ...ity-page-sub-community-list.component.html | 2 +- ...unity-page-sub-community-list.component.ts | 2 +- ...unity-page-sub-community-list.component.ts | 3 ++- .../themed-community-page.component.ts | 3 ++- .../dso-edit-metadata-value.component.html | 2 +- .../dso-edit-metadata-value.component.spec.ts | 4 +-- .../dso-edit-metadata.component.ts | 6 ++--- .../themed-dso-edit-metadata.component.ts | 3 ++- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-list-element.component.html | 2 +- ...ue-search-result-list-element.component.ts | 4 +-- ...-search-result-list-element.component.html | 2 +- ...me-search-result-list-element.component.ts | 4 +-- ...-search-result-list-element.component.html | 2 +- ...al-search-result-list-element.component.ts | 4 +-- .../journal-issue.component.html | 8 +++--- .../journal-volume.component.html | 8 +++--- .../item-pages/journal/journal.component.html | 8 +++--- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-list-element.component.html | 2 +- ...it-search-result-list-element.component.ts | 4 +-- ...-search-result-list-element.component.html | 2 +- ...on-search-result-list-element.component.ts | 4 +-- ...-search-result-list-element.component.html | 2 +- ...ct-search-result-list-element.component.ts | 4 +-- .../org-unit/org-unit.component.html | 10 +++---- .../item-pages/person/person.component.html | 10 +++---- .../item-pages/project/project.component.html | 14 +++++----- ...esult-list-submission-element.component.ts | 4 +-- src/app/footer/footer.component.ts | 2 +- src/app/footer/themed-footer.component.ts | 3 ++- src/app/forbidden/forbidden.component.ts | 2 +- .../forbidden/themed-forbidden.component.ts | 3 ++- .../forgot-email.component.html | 4 +-- .../forgot-email.component.ts | 9 +++---- .../themed-forgot-email.component.ts | 3 ++- .../forgot-password-form.component.ts | 2 +- .../themed-forgot-password-form.component.ts | 3 ++- .../header-navbar-wrapper.component.html | 4 +-- .../header-navbar-wrapper.component.ts | 2 +- .../themed-header-navbar-wrapper.component.ts | 3 ++- src/app/header/header.component.html | 6 ++--- src/app/header/header.component.ts | 5 ++-- src/app/header/themed-header.component.ts | 3 ++- .../home-news/home-news.component.ts | 2 +- .../home-news/themed-home-news.component.ts | 3 ++- src/app/home-page/home-page.component.html | 8 +++--- src/app/home-page/home-page.component.ts | 6 ++--- .../recent-item-list.component.ts | 4 +-- .../home-page/themed-home-page.component.ts | 3 ++- ...emed-top-level-community-list.component.ts | 3 ++- .../top-level-community-list.component.html | 2 +- .../top-level-community-list.component.ts | 2 +- .../import-external-page.component.html | 2 +- .../end-user-agreement.component.ts | 2 +- .../themed-end-user-agreement.component.ts | 3 ++- .../feedback-form/feedback-form.component.ts | 2 +- .../themed-feedback-form.component.ts | 3 ++- src/app/info/feedback/feedback.component.html | 2 +- src/app/info/feedback/feedback.component.ts | 2 +- .../feedback/themed-feedback.component.ts | 3 ++- src/app/info/privacy/privacy.component.ts | 2 +- .../info/privacy/themed-privacy.component.ts | 3 ++- .../item-page/alerts/item-alerts.component.ts | 2 +- .../alerts/themed-item-alerts.component.ts | 3 ++- .../item-bitstreams.component.html | 2 +- ...rag-and-drop-bitstream-list.component.html | 2 +- .../item-collection-mapper.component.html | 4 +-- .../edit-relationship-list.component.html | 2 +- .../item-relationships.component.html | 2 +- .../item-status/item-status.component.ts | 2 +- .../themed-item-status.component.ts | 3 ++- .../full-file-section.component.html | 12 ++++----- .../full-file-section.component.ts | 2 +- .../themed-full-file-section.component.ts | 3 ++- .../full/full-item-page.component.html | 8 +++--- .../full/full-item-page.component.ts | 2 +- .../full/themed-full-item-page.component.ts | 3 ++- .../media-viewer-image.component.ts | 2 +- .../themed-media-viewer-image.component.ts | 3 ++- .../media-viewer-video.component.ts | 2 +- .../themed-media-viewer-video.component.ts | 3 ++- .../media-viewer/media-viewer.component.html | 12 ++++----- .../media-viewer.component.spec.ts | 4 +-- .../media-viewer/media-viewer.component.ts | 6 ++--- .../themed-media-viewer.component.ts | 3 ++- .../orcid-page/orcid-page.component.ts | 4 +-- .../orcid-queue/orcid-queue.component.ts | 4 +-- .../file-section/file-section.component.html | 6 ++--- .../file-section.component.spec.ts | 6 ++--- .../file-section/file-section.component.ts | 2 +- .../themed-file-section.component.ts | 3 ++- .../title/item-page-title-field.component.ts | 2 +- .../title/themed-item-page-field.component.ts | 3 ++- .../item-page/simple/item-page.component.html | 4 +-- .../simple/item-page.component.spec.ts | 2 +- .../item-page/simple/item-page.component.ts | 2 +- .../publication/publication.component.html | 16 ++++++------ .../untyped-item/untyped-item.component.html | 16 ++++++------ ...etadata-representation-list.component.html | 2 +- .../metadata-representation-list.component.ts | 2 +- ...-metadata-representation-list.component.ts | 3 ++- .../related-entities-search.component.ts | 4 +-- .../related-items.component.html | 2 +- .../simple/themed-item-page.component.ts | 3 ++- .../item-versions-summary-modal.component.ts | 4 +-- src/app/login-page/login-page.component.html | 4 +-- src/app/login-page/login-page.component.ts | 5 ++-- .../login-page/themed-login-page.component.ts | 3 ++- src/app/logout-page/logout-page.component.ts | 2 +- .../themed-logout-page.component.ts | 3 ++- .../objectnotfound.component.ts | 2 +- .../themed-objectnotfound.component.ts | 3 ++- .../collection-selector.component.html | 4 +-- ...space-new-submission-dropdown.component.ts | 4 +-- .../my-dspace-page.component.html | 4 +-- .../my-dspace-page.component.ts | 2 +- .../themed-my-dspace-page.component.ts | 3 ++- .../expandable-navbar-section.component.ts | 2 +- ...med-expandable-navbar-section.component.ts | 3 ++- src/app/navbar/navbar.component.html | 2 +- src/app/navbar/navbar.component.ts | 5 ++-- src/app/navbar/themed-navbar.component.ts | 3 ++- .../quality-assurance-events.component.ts | 4 +-- .../project-entry-import-modal.component.html | 4 +-- .../project-entry-import-modal.component.ts | 4 +-- .../quality-assurance-source.component.ts | 4 +-- .../quality-assurance-topics.component.ts | 4 +-- .../suggestion-actions.component.ts | 4 +-- .../publication-claim.component.ts | 4 +-- src/app/page-error/page-error.component.ts | 2 +- .../page-error/themed-page-error.component.ts | 3 ++- .../page-internal-server-error.component.ts | 2 +- ...ed-page-internal-server-error.component.ts | 3 ++- .../pagenotfound/pagenotfound.component.ts | 2 +- .../themed-pagenotfound.component.ts | 3 ++- .../detail/process-detail.component.html | 8 +++--- .../process-overview-table.component.html | 2 +- .../profile-page/profile-page.component.ts | 2 +- .../themed-profile-page.component.ts | 3 ++- .../register-email-form.component.ts | 2 +- .../themed-registry-email-form.component.ts | 3 ++- .../create-profile.component.ts | 2 +- .../themed-create-profile.component.ts | 3 ++- .../register-email.component.html | 4 +-- .../register-email.component.ts | 9 +++---- .../themed-register-email.component.ts | 3 ++- .../deny-request-copy.component.html | 4 +-- .../deny-request-copy.component.ts | 2 +- .../themed-deny-request-copy.component.ts | 3 ++- .../email-request-copy.component.ts | 2 +- .../themed-email-request-copy.component.ts | 3 ++- .../grant-deny-request-copy.component.html | 2 +- .../grant-request-copy.component.html | 6 ++--- .../grant-request-copy.component.ts | 2 +- .../themed-grant-request-copy.component.ts | 3 ++- src/app/root/root.component.html | 12 ++++----- src/app/root/root.component.ts | 2 +- src/app/root/themed-root.component.ts | 3 ++- .../search-navbar/search-navbar.component.ts | 2 +- .../themed-search-navbar.component.ts | 3 ++- .../configuration-search-page.component.ts | 2 +- .../search-page/search-page.component.html | 2 +- src/app/search-page/search-page.component.ts | 2 +- ...med-configuration-search-page.component.ts | 3 ++- .../themed-search-page.component.ts | 3 ++- .../auth-nav-menu.component.html | 6 ++--- .../auth-nav-menu.component.spec.ts | 2 +- .../auth-nav-menu/auth-nav-menu.component.ts | 6 ++--- .../themed-auth-nav-menu.component.ts | 3 ++- .../user-menu/themed-user-menu.component.ts | 3 ++- .../user-menu/user-menu.component.html | 2 +- .../user-menu/user-menu.component.ts | 2 +- .../shared/browse-by/browse-by.component.html | 6 ++--- .../browse-by/browse-by.component.spec.ts | 2 +- .../shared/browse-by/browse-by.component.ts | 2 +- .../browse-by/themed-browse-by.component.ts | 3 ++- .../collection-dropdown.component.html | 4 +-- .../collection-dropdown.component.ts | 2 +- .../themed-collection-dropdown.component.ts | 3 ++- .../comcol-role/comcol-role.component.html | 2 +- .../comcol-page-browse-by.component.ts | 2 +- .../themed-comcol-page-browse-by.component.ts | 3 ++- .../comcol-page-handle.component.ts | 2 +- .../themed-comcol-page-handle.component.ts | 3 ++- .../comcol-search-section.component.html | 4 +-- ...tem-withdrawn-reinstate-modal.component.ts | 4 +-- .../dso-selector/dso-selector.component.html | 2 +- ...te-collection-parent-selector.component.ts | 2 +- ...te-collection-parent-selector.component.ts | 3 ++- ...ate-community-parent-selector.component.ts | 2 +- ...ate-community-parent-selector.component.ts | 3 ++- .../create-item-parent-selector.component.ts | 2 +- ...d-create-item-parent-selector.component.ts | 3 ++- .../edit-collection-selector.component.ts | 2 +- ...emed-edit-collection-selector.component.ts | 3 ++- .../edit-community-selector.component.ts | 2 +- ...hemed-edit-community-selector.component.ts | 3 ++- .../edit-item-selector.component.ts | 2 +- .../themed-edit-item-selector.component.ts | 3 ++- .../entity-dropdown.component.html | 4 +-- .../file-download-link.component.ts | 2 +- .../themed-file-download-link.component.ts | 3 ++- ...sting-metadata-list-element.component.html | 2 +- ...sting-relation-list-element.component.html | 2 +- .../dynamic-relation-group.component.html | 2 +- ...namic-lookup-relation-modal.component.html | 10 +++---- ...elation-external-source-tab.component.html | 8 +++--- ...tion-external-source-tab.component.spec.ts | 2 +- ...-relation-external-source-tab.component.ts | 2 +- ...nal-source-entry-import-modal.component.ts | 6 ++--- ...nal-source-entry-import-modal.component.ts | 3 ++- ...-relation-external-source-tab.component.ts | 3 ++- ...-lookup-relation-search-tab.component.html | 4 +-- ...ic-lookup-relation-search-tab.component.ts | 2 +- ...ic-lookup-relation-search-tab.component.ts | 3 ++- .../vocabulary-treeview.component.html | 2 +- .../lang-switch/lang-switch.component.ts | 2 +- .../themed-lang-switch.component.ts | 3 ++- src/app/shared/loading/loading.component.ts | 2 +- .../loading/themed-loading.component.ts | 3 ++- src/app/shared/log-in/log-in.component.html | 2 +- src/app/shared/log-in/log-in.component.ts | 2 +- .../shared/log-in/themed-log-in.component.ts | 3 ++- src/app/shared/menu/menu-section.decorator.ts | 5 ++-- .../object-collection.component.html | 4 +-- .../object-collection.component.spec.ts | 6 ++--- .../access-status-badge.component.ts | 2 +- .../themed-access-status-badge.component.ts | 3 ++- .../shared/badges/badges.component.html | 8 +++--- .../shared/badges/badges.component.ts | 2 +- .../my-dspace-status-badge.component.ts | 2 +- ...themed-my-dspace-status-badge.component.ts | 3 ++- .../status-badge/status-badge.component.ts | 2 +- .../themed-status-badge.component.ts | 3 ++- .../shared/badges/themed-badges.component.ts | 3 ++- .../type-badge/themed-type-badge.component.ts | 3 ++- .../badges/type-badge/type-badge.component.ts | 2 +- .../item-detail-preview.component.html | 8 +++--- .../object-detail.component.html | 2 +- .../collection-grid-element.component.html | 8 +++--- .../community-grid-element.component.html | 8 +++--- .../object-grid/object-grid.component.html | 2 +- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-grid-element.component.html | 10 +++---- ...-search-result-list-element.component.html | 4 +-- ...-search-result-list-element.component.html | 4 +-- ...-search-result-list-element.component.html | 4 +-- ...-search-result-list-element.component.html | 4 +-- .../item-list-preview.component.html | 2 +- .../item-list-preview.component.spec.ts | 2 +- .../item-list-preview.component.ts | 6 ++--- .../themed-item-list-preview.component.ts | 3 ++- ...ult-list-element-submission.component.html | 4 +-- ...-search-result-list-element.component.html | 4 +-- ...-search-result-list-element.component.html | 4 +-- ...-search-result-list-element.component.html | 4 +-- .../object-list/object-list.component.ts | 2 +- ...-search-result-list-element.component.html | 2 +- ...-search-result-list-element.component.html | 2 +- ...-search-result-list-element.component.html | 2 +- ...em-search-result-list-element.component.ts | 4 +-- .../themed-object-list.component.ts | 3 ++- .../collection-select.component.html | 2 +- .../item-select/item-select.component.html | 2 +- .../object-table/object-table.component.html | 2 +- .../results-back-button.component.ts | 2 +- .../themed-results-back-button.component.ts | 3 ++- .../search-form/search-form.component.ts | 2 +- .../themed-search-form.component.ts | 3 ++- .../search-filters.component.ts | 2 +- .../themed-search-filters.component.ts | 3 ++- .../search-results.component.html | 2 +- .../search-results.component.ts | 2 +- .../themed-search-results.component.ts | 3 ++- .../search-settings.component.ts | 2 +- .../themed-search-settings.component.ts | 3 ++- .../search-sidebar.component.html | 8 +++--- .../search-sidebar.component.ts | 2 +- .../themed-search-sidebar.component.ts | 3 ++- src/app/shared/search/search.component.html | 16 ++++++------ src/app/shared/search/search.component.ts | 2 +- .../shared/search/themed-search.component.ts | 3 ++- .../subscription-modal.component.html | 2 +- .../subscription-view.component.html | 2 +- .../subscription-view.component.spec.ts | 2 +- .../collection-statistics-page.component.ts | 2 +- ...ed-collection-statistics-page.component.ts | 3 ++- .../community-statistics-page.component.ts | 2 +- ...med-community-statistics-page.component.ts | 3 ++- .../item-statistics-page.component.ts | 2 +- .../themed-item-statistics-page.component.ts | 3 ++- .../site-statistics-page.component.ts | 2 +- .../themed-site-statistics-page.component.ts | 3 ++- .../statistics-page.component.html | 2 +- .../edit/submission-edit.component.ts | 2 +- .../edit/themed-submission-edit.component.ts | 3 ++- .../submission-form-collection.component.html | 4 +-- .../form/submission-form.component.html | 2 +- ...-import-external-collection.component.html | 6 ++--- ...port-external-collection.component.spec.ts | 2 +- .../submission-import-external.component.html | 4 +-- .../submission-import-external.component.ts | 2 +- ...ed-submission-import-external.component.ts | 3 ++- ...mission-section-cc-licenses.component.html | 4 +-- .../sections/form/section-form.component.html | 2 +- .../file/section-upload-file.component.html | 4 +-- .../file/section-upload-file.component.ts | 2 +- .../themed-section-upload-file.component.ts | 3 ++- .../upload/section-upload.component.html | 4 +-- .../submit/submission-submit.component.ts | 2 +- .../themed-submission-submit.component.ts | 3 ++- .../subscriptions-page.component.html | 2 +- .../suggestions-page.component.ts | 4 +-- .../thumbnail/themed-thumbnail.component.ts | 3 ++- src/app/thumbnail/thumbnail.component.html | 2 +- src/app/thumbnail/thumbnail.component.spec.ts | 4 +-- src/app/thumbnail/thumbnail.component.ts | 2 +- .../themed-workflow-item-delete.component.ts | 3 ++- .../workflow-item-delete.component.ts | 2 +- ...hemed-workflow-item-send-back.component.ts | 3 ++- .../workflow-item-send-back.component.ts | 2 +- ...ed-workspaceitems-delete-page.component.ts | 11 ++------ .../workspaceitems-delete-page.component.ts | 2 +- .../workspaceitems-edit-page-routes.ts | 3 +-- .../admin-sidebar/admin-sidebar.component.ts | 2 +- .../edit-bitstream-page.component.ts | 2 +- .../app/breadcrumbs/breadcrumbs.component.ts | 2 +- .../browse-by-date.component.ts | 2 -- .../browse-by-metadata.component.ts | 2 -- .../browse-by-taxonomy.component.ts | 2 -- .../browse-by-title.component.ts | 2 -- .../collection-page.component.ts | 2 +- .../edit-item-template-page.component.ts | 2 +- .../community-list-page.component.ts | 2 +- .../community-list.component.ts | 2 +- .../community-page.component.ts | 2 +- ...nity-page-sub-collection-list.component.ts | 2 +- ...unity-page-sub-community-list.component.ts | 2 +- .../dso-edit-metadata.component.ts | 6 ++--- .../custom/app/footer/footer.component.ts | 2 +- .../app/forbidden/forbidden.component.ts | 2 +- .../forgot-email.component.ts | 4 +-- .../forgot-password-form.component.ts | 2 +- .../header-navbar-wrapper.component.ts | 2 +- .../custom/app/header/header.component.ts | 5 ++-- .../home-news/home-news.component.ts | 2 +- .../app/home-page/home-page.component.ts | 6 ++--- .../top-level-community-list.component.ts | 2 +- .../end-user-agreement.component.ts | 2 +- .../feedback-form/feedback-form.component.ts | 2 +- .../app/info/feedback/feedback.component.ts | 2 +- .../app/info/privacy/privacy.component.ts | 2 +- .../item-page/alerts/item-alerts.component.ts | 2 +- .../item-status/item-status.component.ts | 2 +- .../full-file-section.component.ts | 2 +- .../full/full-item-page.component.ts | 2 +- .../media-viewer-image.component.ts | 2 +- .../media-viewer-video.component.ts | 2 +- .../media-viewer/media-viewer.component.ts | 6 ++--- .../file-section/file-section.component.ts | 2 +- .../title/item-page-title-field.component.ts | 2 +- .../item-page/simple/item-page.component.ts | 2 +- .../metadata-representation-list.component.ts | 2 +- .../app/login-page/login-page.component.html | 4 +-- .../app/login-page/login-page.component.ts | 5 ++-- .../app/logout-page/logout-page.component.ts | 2 +- .../objectnotfound.component.ts | 2 +- .../my-dspace-page.component.ts | 2 +- .../expandable-navbar-section.component.ts | 2 +- .../custom/app/navbar/navbar.component.ts | 5 ++-- .../pagenotfound/pagenotfound.component.ts | 2 +- .../profile-page/profile-page.component.ts | 2 +- .../register-email-form.component.ts | 2 +- .../create-profile.component.ts | 2 +- .../register-email.component.ts | 4 +-- .../deny-request-copy.component.ts | 2 +- .../email-request-copy.component.ts | 2 +- .../grant-request-copy.component.ts | 2 +- src/themes/custom/app/root/root.component.ts | 2 +- .../search-navbar/search-navbar.component.ts | 2 +- .../configuration-search-page.component.ts | 2 +- .../app/search-page/search-page.component.ts | 2 +- .../auth-nav-menu/auth-nav-menu.component.ts | 6 ++--- .../user-menu/user-menu.component.ts | 2 +- .../shared/browse-by/browse-by.component.ts | 2 +- .../collection-dropdown.component.ts | 2 +- .../comcol-page-browse-by.component.ts | 2 +- .../comcol-page-handle.component.ts | 2 +- ...te-collection-parent-selector.component.ts | 2 +- ...ate-community-parent-selector.component.ts | 2 +- .../create-item-parent-selector.component.ts | 2 +- .../edit-collection-selector.component.ts | 2 +- .../edit-community-selector.component.ts | 2 +- .../edit-item-selector.component.ts | 2 +- .../file-download-link.component.ts | 2 +- ...-relation-external-source-tab.component.ts | 2 +- ...nal-source-entry-import-modal.component.ts | 6 ++--- ...ic-lookup-relation-search-tab.component.ts | 2 +- .../lang-switch/lang-switch.component.ts | 2 +- .../app/shared/loading/loading.component.ts | 2 +- .../app/shared/log-in/log-in.component.ts | 2 +- .../access-status-badge.component.ts | 2 +- .../shared/badges/badges.component.ts | 2 +- .../my-dspace-status-badge.component.ts | 2 +- .../status-badge/status-badge.component.ts | 2 +- .../badges/type-badge/type-badge.component.ts | 2 +- .../object-list/object-list.component.ts | 2 +- ...em-search-result-list-element.component.ts | 4 +-- .../results-back-button.component.ts | 2 +- .../search-form/search-form.component.ts | 2 +- .../search-filters.component.ts | 2 +- .../search-results.component.ts | 2 +- .../search-settings.component.ts | 2 +- .../search-sidebar.component.ts | 2 +- .../collection-statistics-page.component.ts | 2 +- .../community-statistics-page.component.ts | 2 +- .../item-statistics-page.component.ts | 2 +- .../site-statistics-page.component.ts | 2 +- .../edit/submission-edit.component.ts | 2 +- .../submission-import-external.component.ts | 2 +- .../file/section-upload-file.component.ts | 2 +- .../submit/submission-submit.component.ts | 2 +- .../app/thumbnail/thumbnail.component.ts | 2 +- .../workflow-item-delete.component.ts | 2 +- .../workflow-item-send-back.component.ts | 2 +- .../workspace-items-delete.component.ts | 2 +- .../header-navbar-wrapper.component.html | 4 +-- .../header-navbar-wrapper.component.ts | 2 +- .../dspace/app/header/header.component.html | 8 +++--- .../dspace/app/header/header.component.ts | 5 ++-- .../home-news/home-news.component.ts | 2 +- .../dspace/app/navbar/navbar.component.html | 2 +- .../dspace/app/navbar/navbar.component.ts | 7 ++--- 482 files changed, 849 insertions(+), 802 deletions(-) diff --git a/cypress/e2e/login-modal.cy.ts b/cypress/e2e/login-modal.cy.ts index 190f3ff9271..3d978dfaca2 100644 --- a/cypress/e2e/login-modal.cy.ts +++ b/cypress/e2e/login-modal.cy.ts @@ -3,31 +3,31 @@ import { testA11y } from 'cypress/support/utils'; const page = { openLoginMenu() { // Click the "Log In" dropdown menu in header - cy.get('ds-themed-header [data-test="login-menu"]').click(); + cy.get('ds-header [data-test="login-menu"]').click(); }, openUserMenu() { // Once logged in, click the User menu in header - cy.get('ds-themed-header [data-test="user-menu"]').click(); + cy.get('ds-header [data-test="user-menu"]').click(); }, submitLoginAndPasswordByPressingButton(email, password) { // Enter email - cy.get('ds-themed-header [data-test="email"]').type(email); + cy.get('ds-header [data-test="email"]').type(email); // Enter password - cy.get('ds-themed-header [data-test="password"]').type(password); + cy.get('ds-header [data-test="password"]').type(password); // Click login button - cy.get('ds-themed-header [data-test="login-button"]').click(); + cy.get('ds-header [data-test="login-button"]').click(); }, submitLoginAndPasswordByPressingEnter(email, password) { // In opened Login modal, fill out email & password, then click Enter - cy.get('ds-themed-header [data-test="email"]').type(email); - cy.get('ds-themed-header [data-test="password"]').type(password); - cy.get('ds-themed-header [data-test="password"]').type('{enter}'); + cy.get('ds-header [data-test="email"]').type(email); + cy.get('ds-header [data-test="password"]').type(password); + cy.get('ds-header [data-test="password"]').type('{enter}'); }, submitLogoutByPressingButton() { // This is the POST command that will actually log us out cy.intercept('POST', '/server/api/authn/logout').as('logout'); // Click logout button - cy.get('ds-themed-header [data-test="logout-button"]').click(); + cy.get('ds-header [data-test="logout-button"]').click(); // Wait until above POST command responds before continuing // (This ensures next action waits until logout completes) cy.wait('@logout'); @@ -102,10 +102,10 @@ describe('Login Modal', () => { page.openLoginMenu(); // Registration link should be visible - cy.get('ds-themed-header [data-test="register"]').should('be.visible'); + cy.get('ds-header [data-test="register"]').should('be.visible'); // Click registration link & you should go to registration page - cy.get('ds-themed-header [data-test="register"]').click(); + cy.get('ds-header [data-test="register"]').click(); cy.location('pathname').should('eq', '/register'); cy.get('ds-register-email').should('exist'); @@ -119,10 +119,10 @@ describe('Login Modal', () => { page.openLoginMenu(); // Forgot password link should be visible - cy.get('ds-themed-header [data-test="forgot"]').should('be.visible'); + cy.get('ds-header [data-test="forgot"]').should('be.visible'); // Click link & you should go to Forgot Password page - cy.get('ds-themed-header [data-test="forgot"]').click(); + cy.get('ds-header [data-test="forgot"]').click(); cy.location('pathname').should('eq', '/forgot'); cy.get('ds-forgot-email').should('exist'); diff --git a/cypress/e2e/search-navbar.cy.ts b/cypress/e2e/search-navbar.cy.ts index b1682199161..0613e5e7124 100644 --- a/cypress/e2e/search-navbar.cy.ts +++ b/cypress/e2e/search-navbar.cy.ts @@ -1,15 +1,15 @@ const page = { fillOutQueryInNavBar(query) { // Click the magnifying glass - cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + cy.get('ds-header [data-test="header-search-icon"]').click(); // Fill out a query in input that appears - cy.get('ds-themed-header [data-test="header-search-box"]').type(query); + cy.get('ds-header [data-test="header-search-box"]').type(query); }, submitQueryByPressingEnter() { - cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}'); + cy.get('ds-header [data-test="header-search-box"]').type('{enter}'); }, submitQueryByPressingIcon() { - cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + cy.get('ds-header [data-test="header-search-icon"]').click(); }, }; diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts index 2387ea76dd2..3f7c2e7fdc3 100644 --- a/lint/src/rules/ts/themed-component-usages.ts +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -22,7 +22,6 @@ import { DISALLOWED_THEME_SELECTORS, fixSelectors, getThemeableComponentByBaseClass, - inThemedComponentFile, isAllowedUnthemedUsage, } from '../../util/theme-support'; import { diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html index 6e967b53b5e..e24eee3a24f 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html @@ -23,10 +23,10 @@ {{'admin.access-control.bulk-access-browse.search.header' | translate}}
- + [showThumbnails]="false">
diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index 92968d2e28b..ff92354722d 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -41,7 +41,7 @@ - +

{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}

- + {{messagePrefix + 'search.head' | tra
- + - + > diff --git a/src/app/admin/admin-search-page/admin-search-page.component.ts b/src/app/admin/admin-search-page/admin-search-page.component.ts index d6215be971a..99909b8257f 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.ts +++ b/src/app/admin/admin-search-page/admin-search-page.component.ts @@ -1,14 +1,14 @@ import { Component } from '@angular/core'; import { Context } from '../../core/shared/context.model'; -import { ConfigurationSearchPageComponent } from '../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; @Component({ selector: 'ds-admin-search-page', templateUrl: './admin-search-page.component.html', styleUrls: ['./admin-search-page.component.scss'], standalone: true, - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }) /** diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts index cd26a11995e..00514d2afc3 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts @@ -41,7 +41,7 @@ import { ThemeService } from '../../shared/theme-support/theme.service'; * Component representing the admin sidebar */ @Component({ - selector: 'ds-admin-sidebar', + selector: 'ds-base-admin-sidebar', templateUrl: './admin-sidebar.component.html', styleUrls: ['./admin-sidebar.component.scss'], animations: [slideSidebar], diff --git a/src/app/admin/admin-sidebar/themed-admin-sidebar.component.ts b/src/app/admin/admin-sidebar/themed-admin-sidebar.component.ts index 165f48384a0..6127fd42a99 100644 --- a/src/app/admin/admin-sidebar/themed-admin-sidebar.component.ts +++ b/src/app/admin/admin-sidebar/themed-admin-sidebar.component.ts @@ -11,10 +11,11 @@ import { AdminSidebarComponent } from './admin-sidebar.component'; * Themed wrapper for AdminSidebarComponent */ @Component({ - selector: 'ds-themed-admin-sidebar', + selector: 'ds-admin-sidebar', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [AdminSidebarComponent], }) export class ThemedAdminSidebarComponent extends ThemedComponent { diff --git a/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts index fdc34fe4ab9..62a66039aff 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts @@ -1,14 +1,14 @@ import { Component } from '@angular/core'; import { Context } from '../../core/shared/context.model'; -import { ConfigurationSearchPageComponent } from '../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; @Component({ selector: 'ds-admin-workflow-page', templateUrl: './admin-workflow-page.component.html', styleUrls: ['./admin-workflow-page.component.scss'], standalone: true, - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }) /** diff --git a/src/app/app.component.html b/src/app/app.component.html index fb9983a6ef9..9016f42dc1e 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,3 +1,3 @@ - + [shouldShowRouteLoader]="isRouteLoading$ | async"> diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html index b306eb27214..f7d2c608324 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html @@ -2,7 +2,7 @@
- +
@@ -27,7 +27,7 @@

{{dsoNameService.getName(bitstreamRD?.payload)}} - +

diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index f4d285ac74d..36b0816ade1 100644 --- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -80,7 +80,7 @@ import { VarDirective } from '../../shared/utils/var.directive'; import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component'; @Component({ - selector: 'ds-edit-bitstream-page', + selector: 'ds-base-edit-bitstream-page', styleUrls: ['./edit-bitstream-page.component.scss'], templateUrl: './edit-bitstream-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/bitstream-page/edit-bitstream-page/themed-edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/themed-edit-bitstream-page.component.ts index 4d8a9946f54..7e922485cb6 100644 --- a/src/app/bitstream-page/edit-bitstream-page/themed-edit-bitstream-page.component.ts +++ b/src/app/bitstream-page/edit-bitstream-page/themed-edit-bitstream-page.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { EditBitstreamPageComponent } from './edit-bitstream-page.component'; @Component({ - selector: 'ds-themed-edit-bitstream-page', + selector: 'ds-edit-bitstream-page', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [EditBitstreamPageComponent], }) export class ThemedEditBitstreamPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 2483335245b..9820d672c98 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -18,7 +18,7 @@ import { BreadcrumbsService } from './breadcrumbs.service'; * Component representing the breadcrumbs of a page */ @Component({ - selector: 'ds-breadcrumbs', + selector: 'ds-base-breadcrumbs', templateUrl: './breadcrumbs.component.html', styleUrls: ['./breadcrumbs.component.scss'], standalone: true, diff --git a/src/app/breadcrumbs/themed-breadcrumbs.component.ts b/src/app/breadcrumbs/themed-breadcrumbs.component.ts index 2e471fd92de..255af605dc5 100644 --- a/src/app/breadcrumbs/themed-breadcrumbs.component.ts +++ b/src/app/breadcrumbs/themed-breadcrumbs.component.ts @@ -7,10 +7,11 @@ import { BreadcrumbsComponent } from './breadcrumbs.component'; * Themed wrapper for BreadcrumbsComponent */ @Component({ - selector: 'ds-themed-breadcrumbs', + selector: 'ds-breadcrumbs', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [BreadcrumbsComponent], }) export class ThemedBreadcrumbsComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/browse-by/browse-by-date/browse-by-date.component.ts b/src/app/browse-by/browse-by-date/browse-by-date.component.ts index 3382a46f583..c84fc4cb77f 100644 --- a/src/app/browse-by/browse-by-date/browse-by-date.component.ts +++ b/src/app/browse-by/browse-by-date/browse-by-date.component.ts @@ -35,7 +35,6 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; import { Item } from '../../core/shared/item.model'; -import { BrowseByComponent } from '../../shared/browse-by/browse-by.component'; import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component'; import { ComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/comcol-page-content.component'; import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component'; @@ -72,7 +71,6 @@ import { ComcolPageContentComponent, DsoEditMenuComponent, ThemedComcolPageBrowseByComponent, - BrowseByComponent, TranslateModule, ThemedLoadingComponent, ThemedBrowseByComponent, diff --git a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html index 69c3c31c466..22e564ac27c 100644 --- a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.html @@ -1,6 +1,6 @@
diff --git a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts index c3bad2a9232..3c893f5259e 100644 --- a/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts +++ b/src/app/browse-by/browse-by-metadata/browse-by-metadata.component.ts @@ -45,7 +45,6 @@ import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { Context } from '../../core/shared/context.model'; import { Item } from '../../core/shared/item.model'; import { getFirstSucceededRemoteData } from '../../core/shared/operators'; -import { BrowseByComponent } from '../../shared/browse-by/browse-by.component'; import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component'; import { ComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/comcol-page-content.component'; import { ThemedComcolPageHandleComponent } from '../../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component'; @@ -78,7 +77,6 @@ export const BBM_PAGINATION_ID = 'bbm'; ComcolPageContentComponent, DsoEditMenuComponent, ThemedComcolPageBrowseByComponent, - BrowseByComponent, TranslateModule, ThemedLoadingComponent, ThemedBrowseByComponent, diff --git a/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts index 39a90de83ef..94956f257d5 100644 --- a/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts +++ b/src/app/browse-by/browse-by-taxonomy/browse-by-taxonomy.component.ts @@ -27,7 +27,6 @@ import { Context } from '../../core/shared/context.model'; import { HierarchicalBrowseDefinition } from '../../core/shared/hierarchical-browse-definition.model'; import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; -import { BrowseByComponent } from '../../shared/browse-by/browse-by.component'; import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component'; import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component'; import { ComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/comcol-page-content.component'; @@ -55,7 +54,6 @@ import { BrowseByDataType } from '../browse-by-switcher/browse-by-data-type'; ComcolPageContentComponent, DsoEditMenuComponent, ThemedComcolPageBrowseByComponent, - BrowseByComponent, TranslateModule, ThemedLoadingComponent, ThemedBrowseByComponent, diff --git a/src/app/browse-by/browse-by-title/browse-by-title.component.ts b/src/app/browse-by/browse-by-title/browse-by-title.component.ts index 38c4c1333c2..d99332baf18 100644 --- a/src/app/browse-by/browse-by-title/browse-by-title.component.ts +++ b/src/app/browse-by/browse-by-title/browse-by-title.component.ts @@ -15,7 +15,6 @@ import { SortDirection, SortOptions, } from '../../core/cache/models/sort-options.model'; -import { BrowseByComponent } from '../../shared/browse-by/browse-by.component'; import { ThemedBrowseByComponent } from '../../shared/browse-by/themed-browse-by.component'; import { ThemedComcolPageBrowseByComponent } from '../../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component'; import { ComcolPageContentComponent } from '../../shared/comcol/comcol-page-content/comcol-page-content.component'; @@ -47,7 +46,6 @@ import { ComcolPageContentComponent, DsoEditMenuComponent, ThemedComcolPageBrowseByComponent, - BrowseByComponent, TranslateModule, ThemedLoadingComponent, ThemedBrowseByComponent, diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html index 77f85d5f78e..3ca9665b1cc 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -28,14 +28,14 @@

{{'collection.edit.item-mapper.head' | translate}}

- - +
diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index d5da37c12f7..f9739a4aa93 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -17,10 +17,10 @@ - - +
- - +
@@ -55,7 +55,7 @@
- +
\ No newline at end of file diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index c015213ee1d..5b5d0a84d7f 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -54,7 +54,7 @@ import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-trac import { getCollectionPageRoute } from './collection-page-routing-paths'; @Component({ - selector: 'ds-collection-page', + selector: 'ds-base-collection-page', styleUrls: ['./collection-page.component.scss'], templateUrl: './collection-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html index 7b4ac97421d..22940850963 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.html @@ -25,7 +25,7 @@

{{ 'collection.edit.tabs.source.head' | translate }}

- +

{{ 'collection.edit.tabs.source.form.head' | translate }}

diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html index 8d095dd2296..7f0b2efba22 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -3,10 +3,10 @@

{{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}

- +
- +
diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts index eb44ffdc9be..f7c5dc4b14a 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -24,7 +24,6 @@ import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { Item } from '../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; -import { DsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/dso-edit-metadata.component'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertType } from '../../shared/alert/alert-type'; @@ -33,11 +32,10 @@ import { VarDirective } from '../../shared/utils/var.directive'; import { getCollectionEditRoute } from '../collection-page-routing-paths'; @Component({ - selector: 'ds-edit-item-template-page', + selector: 'ds-base-edit-item-template-page', templateUrl: './edit-item-template-page.component.html', imports: [ ThemedDsoEditMetadataComponent, - DsoEditMetadataComponent, RouterLink, AsyncPipe, VarDirective, diff --git a/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts index 2dff5578351..421049990ae 100644 --- a/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts +++ b/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { EditItemTemplatePageComponent } from './edit-item-template-page.component'; @Component({ - selector: 'ds-themed-edit-item-template-page', + selector: 'ds-edit-item-template-page', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [EditItemTemplatePageComponent], }) /** * Component for editing the item template of a collection diff --git a/src/app/collection-page/themed-collection-page.component.ts b/src/app/collection-page/themed-collection-page.component.ts index e095e6eb68a..c84d7c5fb44 100644 --- a/src/app/collection-page/themed-collection-page.component.ts +++ b/src/app/collection-page/themed-collection-page.component.ts @@ -7,10 +7,11 @@ import { CollectionPageComponent } from './collection-page.component'; * Themed wrapper for CollectionPageComponent */ @Component({ - selector: 'ds-themed-collection-page', + selector: 'ds-collection-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [CollectionPageComponent], }) export class ThemedCollectionPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/community-list-page/community-list-page.component.html b/src/app/community-list-page/community-list-page.component.html index 4392fb87d03..ca052014780 100644 --- a/src/app/community-list-page/community-list-page.component.html +++ b/src/app/community-list-page/community-list-page.component.html @@ -1,4 +1,4 @@

{{ 'communityList.title' | translate }}

- +
diff --git a/src/app/community-list-page/community-list-page.component.ts b/src/app/community-list-page/community-list-page.component.ts index aaf7bc2000a..ca0db89f53d 100644 --- a/src/app/community-list-page/community-list-page.component.ts +++ b/src/app/community-list-page/community-list-page.component.ts @@ -8,7 +8,7 @@ import { ThemedCommunityListComponent } from './community-list/themed-community- * navigated to with community-list.page.routing.module */ @Component({ - selector: 'ds-community-list-page', + selector: 'ds-base-community-list-page', templateUrl: './community-list-page.component.html', standalone: true, imports: [ThemedCommunityListComponent, TranslateModule], diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index ea34e02d56e..a59b0590267 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -1,4 +1,4 @@ - + {{ 'communityList.showMore' | translate }} - +
@@ -61,7 +61,7 @@ - +
diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index e0456ca700f..5819471d7e5 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -38,7 +38,7 @@ import { FlatNode } from '../flat-node.model'; * Which nodes were expanded is kept in the store, so this persists across pages. */ @Component({ - selector: 'ds-community-list', + selector: 'ds-base-community-list', templateUrl: './community-list.component.html', styleUrls: ['./community-list.component.scss'], standalone: true, diff --git a/src/app/community-list-page/community-list/themed-community-list.component.ts b/src/app/community-list-page/community-list/themed-community-list.component.ts index dc6c0aa3453..5340384ed5a 100644 --- a/src/app/community-list-page/community-list/themed-community-list.component.ts +++ b/src/app/community-list-page/community-list/themed-community-list.component.ts @@ -5,10 +5,11 @@ import { CommunityListComponent } from './community-list.component'; @Component({ - selector: 'ds-themed-community-list', + selector: 'ds-community-list', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityListComponent], }) export class ThemedCommunityListComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/community-list-page/themed-community-list-page.component.ts b/src/app/community-list-page/themed-community-list-page.component.ts index 4f0575c7db7..d427b1bec1c 100644 --- a/src/app/community-list-page/themed-community-list-page.component.ts +++ b/src/app/community-list-page/themed-community-list-page.component.ts @@ -7,10 +7,11 @@ import { CommunityListPageComponent } from './community-list-page.component'; * Themed wrapper for CommunityListPageComponent */ @Component({ - selector: 'ds-themed-community-list-page', + selector: 'ds-community-list-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityListPageComponent], }) export class ThemedCommunityListPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index b3e577af7d3..a695e2019a3 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -10,8 +10,8 @@ - - + + @@ -25,8 +25,8 @@
- - + +
@@ -39,5 +39,5 @@ - + diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index ce3d05aef9f..bdb3a50c6b7 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -47,7 +47,7 @@ import { ThemedCollectionPageSubCollectionListComponent } from './sections/sub-c import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component'; @Component({ - selector: 'ds-community-page', + selector: 'ds-base-community-page', styleUrls: ['./community-page.component.scss'], templateUrl: './community-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html index b5fbf1a01dd..59d7b3bb5e2 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html @@ -9,5 +9,5 @@

{{'community.sub-collection-list.head' | translate}}

- + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts index 2935f255958..1e8ff1d46c3 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -36,7 +36,7 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagina import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ - selector: 'ds-community-page-sub-collection-list', + selector: 'ds-base-community-page-sub-collection-list', styleUrls: ['./community-page-sub-collection-list.component.scss'], templateUrl: './community-page-sub-collection-list.component.html', animations: [fadeIn], diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index ff5d057b312..4a965bc9264 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts @@ -8,10 +8,11 @@ import { ThemedComponent } from '../../../../shared/theme-support/themed.compone import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; @Component({ - selector: 'ds-themed-community-page-sub-collection-list', + selector: 'ds-community-page-sub-collection-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityPageSubCollectionListComponent], }) export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent { @Input() community: Community; diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html index 515e08ffdfe..a811014bcc9 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html @@ -1,8 +1,8 @@ - - - + - + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html index 0834d08ba58..7f9840f6b7f 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html @@ -9,5 +9,5 @@

{{'community.sub-community-list.head' | translate}}

- + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 4f74eff601b..36bd9919bb8 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts @@ -35,7 +35,7 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagina import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ - selector: 'ds-community-page-sub-community-list', + selector: 'ds-base-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', animations: [fadeIn], diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 11b62d68e46..5988ad0f5ea 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts @@ -8,10 +8,11 @@ import { ThemedComponent } from '../../../../shared/theme-support/themed.compone import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; @Component({ - selector: 'ds-themed-community-page-sub-community-list', + selector: 'ds-community-page-sub-community-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityPageSubCommunityListComponent], }) export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent { diff --git a/src/app/community-page/themed-community-page.component.ts b/src/app/community-page/themed-community-page.component.ts index 41a29607190..b655452041a 100644 --- a/src/app/community-page/themed-community-page.component.ts +++ b/src/app/community-page/themed-community-page.component.ts @@ -7,10 +7,11 @@ import { CommunityPageComponent } from './community-page.component'; * Themed wrapper for CommunityPageComponent */ @Component({ - selector: 'ds-themed-community-page', + selector: 'ds-community-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityPageComponent], }) export class ThemedCommunityPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html index b79b185b40d..0c1088e3fd8 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html @@ -55,7 +55,7 @@
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index ccfcfca93c4..fbbfe982aac 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -221,7 +221,7 @@ describe('DsoEditMetadataValueComponent', () => { it('should not show a badge', () => { expect( - fixture.debugElement.query(By.css('ds-themed-type-badge')), + fixture.debugElement.query(By.css('ds-type-badge')), ).toBeNull(); }); @@ -289,7 +289,7 @@ describe('DsoEditMetadataValueComponent', () => { it('should show a badge', () => { expect( - fixture.debugElement.query(By.css('ds-themed-type-badge')), + fixture.debugElement.query(By.css('ds-type-badge')), ).toBeTruthy(); }); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts index 6365f1ea990..648a6388309 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -53,7 +53,7 @@ import { hasValue, isNotEmpty, } from '../../shared/empty.util'; -import { LoadingComponent } from '../../shared/loading/loading.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { DsoEditMetadataFieldValuesComponent } from './dso-edit-metadata-field-values/dso-edit-metadata-field-values.component'; import { DsoEditMetadataForm } from './dso-edit-metadata-form'; @@ -63,11 +63,11 @@ import { DsoEditMetadataValueHeadersComponent } from './dso-edit-metadata-value- import { MetadataFieldSelectorComponent } from './metadata-field-selector/metadata-field-selector.component'; @Component({ - selector: 'ds-dso-edit-metadata', + selector: 'ds-base-dso-edit-metadata', styleUrls: ['./dso-edit-metadata.component.scss'], templateUrl: './dso-edit-metadata.component.html', standalone: true, - imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, LoadingComponent, AsyncPipe, TranslateModule], + imports: [NgIf, DsoEditMetadataHeadersComponent, MetadataFieldSelectorComponent, DsoEditMetadataValueHeadersComponent, DsoEditMetadataValueComponent, NgFor, DsoEditMetadataFieldValuesComponent, AlertComponent, ThemedLoadingComponent, AsyncPipe, TranslateModule], }) /** * Component showing a table of all metadata on a DSpaceObject and options to modify them diff --git a/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts index 9de9c539a29..063263b6701 100644 --- a/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts @@ -9,10 +9,11 @@ import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { DsoEditMetadataComponent } from './dso-edit-metadata.component'; @Component({ - selector: 'ds-themed-dso-edit-metadata', + selector: 'ds-dso-edit-metadata', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [DsoEditMetadataComponent], }) export class ThemedDsoEditMetadataComponent extends ThemedComponent { diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index 33ce615305c..bca1a1b3b34 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -8,18 +8,18 @@ [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
- - + +
- - + +
- +

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index de80448e8db..3b14a1d8b0f 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -8,18 +8,18 @@ [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
- - + +
- - + +
- +

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index b537fb60d92..20084f21ba3 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -8,18 +8,18 @@ [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
- - + +
- - + +
- +

diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index 545e8c67a6b..59d2ea4dc9d 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -12,7 +12,7 @@
- +
- + +
- - + +
- + +
- - + +
- + +
- - + +
- +
- - + +
- - + +
- +

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index 976d2c55e50..7b444d261c7 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -8,18 +8,18 @@ [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
- - + +
- - + +
- +

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index 1025b4c35d5..2181a4eb07d 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -8,18 +8,18 @@ [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
- - + +
- - + +
- +

diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html index 60251c15b84..ed1181a4077 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html @@ -18,7 +18,7 @@
- +
- +
- + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + - - + diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts index 49d6ecb59c8..1145fb78e32 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -29,7 +29,7 @@ import { listableObjectComponent } from '../../../../../shared/object-collection import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; -import { ThumbnailComponent } from '../../../../../thumbnail/thumbnail.component'; +import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbnail.component'; import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component'; import { PersonInputSuggestionsComponent } from './person-suggestions/person-input-suggestions.component'; @@ -39,7 +39,7 @@ import { PersonInputSuggestionsComponent } from './person-suggestions/person-inp styleUrls: ['./person-search-result-list-submission-element.component.scss'], templateUrl: './person-search-result-list-submission-element.component.html', standalone: true, - imports: [NgIf, ThumbnailComponent, NgClass, PersonInputSuggestionsComponent, FormsModule, NgFor, AsyncPipe], + imports: [NgIf, ThemedThumbnailComponent, NgClass, PersonInputSuggestionsComponent, FormsModule, NgFor, AsyncPipe], }) /** diff --git a/src/app/footer/footer.component.ts b/src/app/footer/footer.component.ts index 3043cf32847..aa124a88ce7 100644 --- a/src/app/footer/footer.component.ts +++ b/src/app/footer/footer.component.ts @@ -19,7 +19,7 @@ import { KlaroService } from '../shared/cookies/klaro.service'; import { hasValue } from '../shared/empty.util'; @Component({ - selector: 'ds-footer', + selector: 'ds-base-footer', styleUrls: ['footer.component.scss'], templateUrl: 'footer.component.html', standalone: true, diff --git a/src/app/footer/themed-footer.component.ts b/src/app/footer/themed-footer.component.ts index 1c3ae830261..a09484ebca5 100644 --- a/src/app/footer/themed-footer.component.ts +++ b/src/app/footer/themed-footer.component.ts @@ -7,10 +7,11 @@ import { FooterComponent } from './footer.component'; * Themed wrapper for FooterComponent */ @Component({ - selector: 'ds-themed-footer', + selector: 'ds-footer', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [FooterComponent], }) export class ThemedFooterComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/forbidden/forbidden.component.ts b/src/app/forbidden/forbidden.component.ts index 03441f1bffb..74cfdf31ceb 100644 --- a/src/app/forbidden/forbidden.component.ts +++ b/src/app/forbidden/forbidden.component.ts @@ -12,7 +12,7 @@ import { ServerResponseService } from '../core/services/server-response.service' * This component representing the `Forbidden` DSpace page. */ @Component({ - selector: 'ds-forbidden', + selector: 'ds-base-forbidden', templateUrl: './forbidden.component.html', styleUrls: ['./forbidden.component.scss'], standalone: true, diff --git a/src/app/forbidden/themed-forbidden.component.ts b/src/app/forbidden/themed-forbidden.component.ts index 85efec18ee4..4d1b6d6fb74 100644 --- a/src/app/forbidden/themed-forbidden.component.ts +++ b/src/app/forbidden/themed-forbidden.component.ts @@ -7,10 +7,11 @@ import { ForbiddenComponent } from './forbidden.component'; * Themed wrapper for ForbiddenComponent */ @Component({ - selector: 'ds-themed-forbidden', + selector: 'ds-forbidden', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [ForbiddenComponent], }) export class ThemedForbiddenComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.html b/src/app/forgot-password/forgot-password-email/forgot-email.component.html index aaa0c27b466..995108cdbc3 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.html +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.html @@ -1,3 +1,3 @@ - - + diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.ts b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts index a7455c4ca9f..2ab05e6518a 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.ts +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts @@ -1,17 +1,14 @@ import { Component } from '@angular/core'; import { ThemedRegisterEmailFormComponent } from 'src/app/register-email-form/themed-registry-email-form.component'; -import { - RegisterEmailFormComponent, - TYPE_REQUEST_FORGOT, -} from '../../register-email-form/register-email-form.component'; +import { TYPE_REQUEST_FORGOT } from '../../register-email-form/register-email-form.component'; @Component({ - selector: 'ds-forgot-email', + selector: 'ds-base-forgot-email', styleUrls: ['./forgot-email.component.scss'], templateUrl: './forgot-email.component.html', imports: [ - RegisterEmailFormComponent, ThemedRegisterEmailFormComponent, + ThemedRegisterEmailFormComponent, ], standalone: true, }) diff --git a/src/app/forgot-password/forgot-password-email/themed-forgot-email.component.ts b/src/app/forgot-password/forgot-password-email/themed-forgot-email.component.ts index 936815a49cc..af9f557fbb7 100644 --- a/src/app/forgot-password/forgot-password-email/themed-forgot-email.component.ts +++ b/src/app/forgot-password/forgot-password-email/themed-forgot-email.component.ts @@ -7,10 +7,11 @@ import { ForgotEmailComponent } from './forgot-email.component'; * Themed wrapper for ForgotEmailComponent */ @Component({ - selector: 'ds-themed-forgot-email', + selector: 'ds-forgot-email', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [ForgotEmailComponent], }) export class ThemedForgotEmailComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts index 3d8d6e3cd7e..442e4bf9fa2 100644 --- a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts @@ -30,7 +30,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe'; @Component({ - selector: 'ds-forgot-password-form', + selector: 'ds-base-forgot-password-form', styleUrls: ['./forgot-password-form.component.scss'], templateUrl: './forgot-password-form.component.html', imports: [ diff --git a/src/app/forgot-password/forgot-password-form/themed-forgot-password-form.component.ts b/src/app/forgot-password/forgot-password-form/themed-forgot-password-form.component.ts index e74fed2f368..956568e2bfe 100644 --- a/src/app/forgot-password/forgot-password-form/themed-forgot-password-form.component.ts +++ b/src/app/forgot-password/forgot-password-form/themed-forgot-password-form.component.ts @@ -7,10 +7,11 @@ import { ForgotPasswordFormComponent } from './forgot-password-form.component'; * Themed wrapper for ForgotPasswordFormComponent */ @Component({ - selector: 'ds-themed-forgot-password-form', + selector: 'ds-forgot-password-form', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [ForgotPasswordFormComponent], }) export class ThemedForgotPasswordFormComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.html b/src/app/header-nav-wrapper/header-navbar-wrapper.component.html index ba3060ff698..60b38dcabf7 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.html +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.html @@ -1,4 +1,4 @@
- - + +
diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.ts b/src/app/header-nav-wrapper/header-navbar-wrapper.component.ts index 862173b9bb0..53f10575315 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.ts +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.ts @@ -21,7 +21,7 @@ import { MenuID } from '../shared/menu/menu-id.model'; * This component represents a wrapper for the horizontal navbar and the header */ @Component({ - selector: 'ds-header-navbar-wrapper', + selector: 'ds-base-header-navbar-wrapper', styleUrls: ['header-navbar-wrapper.component.scss'], templateUrl: 'header-navbar-wrapper.component.html', standalone: true, diff --git a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts index 5895530e8aa..64d36edae35 100644 --- a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts +++ b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts @@ -7,10 +7,11 @@ import { HeaderNavbarWrapperComponent } from './header-navbar-wrapper.component' * Themed wrapper for {@link HeaderNavbarWrapperComponent} */ @Component({ - selector: 'ds-themed-header-navbar-wrapper', + selector: 'ds-header-navbar-wrapper', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [HeaderNavbarWrapperComponent], }) export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index e98959d1626..e59086134a7 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -6,10 +6,10 @@
diff --git a/src/app/request-copy/grant-request-copy/grant-request-copy.component.html b/src/app/request-copy/grant-request-copy/grant-request-copy.component.html index 179206566ed..d2c2cfc3c8e 100644 --- a/src/app/request-copy/grant-request-copy/grant-request-copy.component.html +++ b/src/app/request-copy/grant-request-copy/grant-request-copy.component.html @@ -3,7 +3,7 @@

{{'grant-request-copy.header' | translate}}

{{'grant-request-copy.intro' | translate}}

- +

{{ 'grant-deny-request-copy.email.permissions.info' | translate }}

@@ -11,7 +11,7 @@

{{'grant-request-copy.header' | translate}}

-
+
- +
diff --git a/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts b/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts index e2e2fe88432..b14f15f34b8 100644 --- a/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts +++ b/src/app/request-copy/grant-request-copy/grant-request-copy.component.ts @@ -37,7 +37,7 @@ import { RequestCopyEmail } from '../email-request-copy/request-copy-email.model import { ThemedEmailRequestCopyComponent } from '../email-request-copy/themed-email-request-copy.component'; @Component({ - selector: 'ds-grant-request-copy', + selector: 'ds-base-grant-request-copy', styleUrls: ['./grant-request-copy.component.scss'], templateUrl: './grant-request-copy.component.html', standalone: true, diff --git a/src/app/request-copy/grant-request-copy/themed-grant-request-copy.component.ts b/src/app/request-copy/grant-request-copy/themed-grant-request-copy.component.ts index 7c6f9f61a6e..654f7588bb9 100644 --- a/src/app/request-copy/grant-request-copy/themed-grant-request-copy.component.ts +++ b/src/app/request-copy/grant-request-copy/themed-grant-request-copy.component.ts @@ -7,10 +7,11 @@ import { GrantRequestCopyComponent } from './grant-request-copy.component'; * Themed wrapper for grant-request-copy.component */ @Component({ - selector: 'ds-themed-grant-request-copy', + selector: 'ds-grant-request-copy', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [GrantRequestCopyComponent], }) export class ThemedGrantRequestCopyComponent extends ThemedComponent { diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index 533efea9d18..8fcbd593edf 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -6,22 +6,22 @@ value: ((isSidebarVisible$ | async) !== true ? 'hidden' : (slideSidebarOver$ | async) ? 'unpinned' : 'pinned'), params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) } }"> - +
- - + +
- +
- +
@@ -29,5 +29,5 @@
- +
diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index d98fd85b0a7..5b504b878d2 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -42,7 +42,7 @@ import { CSSVariableService } from '../shared/sass-helper/css-variable.service'; import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banner/system-wide-alert-banner.component'; @Component({ - selector: 'ds-root', + selector: 'ds-base-root', templateUrl: './root.component.html', styleUrls: ['./root.component.scss'], animations: [slideSidebarPadding], diff --git a/src/app/root/themed-root.component.ts b/src/app/root/themed-root.component.ts index b2ea91c75da..fe88efa546d 100644 --- a/src/app/root/themed-root.component.ts +++ b/src/app/root/themed-root.component.ts @@ -7,10 +7,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { RootComponent } from './root.component'; @Component({ - selector: 'ds-themed-root', + selector: 'ds-root', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [RootComponent], }) export class ThemedRootComponent extends ThemedComponent { /** diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts index 006a217d57e..28971a26444 100644 --- a/src/app/search-navbar/search-navbar.component.ts +++ b/src/app/search-navbar/search-navbar.component.ts @@ -20,7 +20,7 @@ import { ClickOutsideDirective } from '../shared/utils/click-outside.directive'; * The search box in the header that expands on focus and collapses on focus out */ @Component({ - selector: 'ds-search-navbar', + selector: 'ds-base-search-navbar', templateUrl: './search-navbar.component.html', styleUrls: ['./search-navbar.component.scss'], animations: [expandSearchInput], diff --git a/src/app/search-navbar/themed-search-navbar.component.ts b/src/app/search-navbar/themed-search-navbar.component.ts index 240b39314b8..82348f79430 100644 --- a/src/app/search-navbar/themed-search-navbar.component.ts +++ b/src/app/search-navbar/themed-search-navbar.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { SearchNavbarComponent } from './search-navbar.component'; @Component({ - selector: 'ds-themed-search-navbar', + selector: 'ds-search-navbar', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [SearchNavbarComponent], }) export class ThemedSearchNavbarComponent extends ThemedComponent { diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 3b28ed447a0..afbe34d550c 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -34,7 +34,7 @@ import { ViewModeSwitchComponent } from '../shared/view-mode-switch/view-mode-sw * This component renders a search page using a configuration as input. */ @Component({ - selector: 'ds-configuration-search-page', + selector: 'ds-base-configuration-search-page', styleUrls: ['../shared/search/search.component.scss'], templateUrl: '../shared/search/search.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/search-page/search-page.component.html b/src/app/search-page/search-page.component.html index 36ba53a885e..8d310607833 100644 --- a/src/app/search-page/search-page.component.html +++ b/src/app/search-page/search-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/search-page/search-page.component.ts b/src/app/search-page/search-page.component.ts index 272987a795c..817cc6818e1 100644 --- a/src/app/search-page/search-page.component.ts +++ b/src/app/search-page/search-page.component.ts @@ -5,7 +5,7 @@ import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-configuration import { ThemedSearchComponent } from '../shared/search/themed-search.component'; @Component({ - selector: 'ds-search-page', + selector: 'ds-base-search-page', templateUrl: './search-page.component.html', providers: [ { diff --git a/src/app/search-page/themed-configuration-search-page.component.ts b/src/app/search-page/themed-configuration-search-page.component.ts index fc9497268c3..78ca3d34ca3 100644 --- a/src/app/search-page/themed-configuration-search-page.component.ts +++ b/src/app/search-page/themed-configuration-search-page.component.ts @@ -12,10 +12,11 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co * Themed wrapper for ConfigurationSearchPageComponent */ @Component({ - selector: 'ds-themed-configuration-search-page', + selector: 'ds-configuration-search-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [ConfigurationSearchPageComponent], }) export class ThemedConfigurationSearchPageComponent extends ThemedComponent { /** diff --git a/src/app/search-page/themed-search-page.component.ts b/src/app/search-page/themed-search-page.component.ts index 30e6e6db11d..649ad8a2466 100644 --- a/src/app/search-page/themed-search-page.component.ts +++ b/src/app/search-page/themed-search-page.component.ts @@ -7,10 +7,11 @@ import { SearchPageComponent } from './search-page.component'; * Themed wrapper for SearchPageComponent */ @Component({ - selector: 'ds-themed-search-page', + selector: 'ds-search-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [SearchPageComponent], }) export class ThemedSearchPageComponent extends ThemedComponent { diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index 98749da7b54..74ae8216349 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -13,8 +13,8 @@
@@ -30,7 +30,7 @@ ngbDropdownToggle>
- +
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 224620c6e1f..74b7261a3fb 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -265,7 +265,7 @@ describe('AuthNavMenuComponent', () => { component = null; }); it('should render UserMenuComponent component', () => { - const logoutDropdownMenu = deNavMenuItem.query(By.css('ds-themed-user-menu')); + const logoutDropdownMenu = deNavMenuItem.query(By.css('ds-user-menu')); expect(logoutDropdownMenu.nativeElement).toBeDefined(); }); }); diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index d24b1db8530..16899cf73e8 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -48,19 +48,17 @@ import { } from '../animations/fade'; import { isNotUndefined } from '../empty.util'; import { HostWindowService } from '../host-window.service'; -import { LogInComponent } from '../log-in/log-in.component'; import { ThemedLogInComponent } from '../log-in/themed-log-in.component'; import { BrowserOnlyPipe } from '../utils/browser-only.pipe'; import { ThemedUserMenuComponent } from './user-menu/themed-user-menu.component'; -import { UserMenuComponent } from './user-menu/user-menu.component'; @Component({ - selector: 'ds-auth-nav-menu', + selector: 'ds-base-auth-nav-menu', templateUrl: './auth-nav-menu.component.html', styleUrls: ['./auth-nav-menu.component.scss'], animations: [fadeInOut, fadeOut], standalone: true, - imports: [NgClass, NgIf, NgbDropdownModule, LogInComponent, ThemedLogInComponent, RouterLink, RouterLinkActive, UserMenuComponent, ThemedUserMenuComponent, AsyncPipe, TranslateModule, BrowserOnlyPipe], + imports: [NgClass, NgIf, NgbDropdownModule, ThemedLogInComponent, RouterLink, RouterLinkActive, ThemedUserMenuComponent, AsyncPipe, TranslateModule, BrowserOnlyPipe], }) export class AuthNavMenuComponent implements OnInit { /** diff --git a/src/app/shared/auth-nav-menu/themed-auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/themed-auth-nav-menu.component.ts index d15a10d9cee..9ed244a929f 100644 --- a/src/app/shared/auth-nav-menu/themed-auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/themed-auth-nav-menu.component.ts @@ -7,10 +7,11 @@ import { AuthNavMenuComponent } from './auth-nav-menu.component'; * Themed wrapper for {@link AuthNavMenuComponent} */ @Component({ - selector: 'ds-themed-auth-nav-menu', + selector: 'ds-auth-nav-menu', styleUrls: [], templateUrl: '../theme-support/themed.component.html', standalone: true, + imports: [AuthNavMenuComponent], }) export class ThemedAuthNavMenuComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/shared/auth-nav-menu/user-menu/themed-user-menu.component.ts b/src/app/shared/auth-nav-menu/user-menu/themed-user-menu.component.ts index feb3f90b351..38112cfa7c8 100644 --- a/src/app/shared/auth-nav-menu/user-menu/themed-user-menu.component.ts +++ b/src/app/shared/auth-nav-menu/user-menu/themed-user-menu.component.ts @@ -10,10 +10,11 @@ import { UserMenuComponent } from './user-menu.component'; * This component represents the user nav menu. */ @Component({ - selector: 'ds-themed-user-menu', + selector: 'ds-user-menu', templateUrl: './../../theme-support/themed.component.html', styleUrls: [], standalone: true, + imports: [UserMenuComponent], }) export class ThemedUserMenuComponent extends ThemedComponent{ diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html index a6d4188f923..4c36cc2f24b 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.ts index d330c0b6fff..0fef74b3896 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.ts +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.ts @@ -72,7 +72,7 @@ export interface CollectionListEntry { } @Component({ - selector: 'ds-collection-dropdown', + selector: 'ds-base-collection-dropdown', templateUrl: './collection-dropdown.component.html', styleUrls: ['./collection-dropdown.component.scss'], standalone: true, diff --git a/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts index 2f7055d4e79..643cd1f7115 100644 --- a/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts +++ b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts @@ -12,10 +12,11 @@ import { } from './collection-dropdown.component'; @Component({ - selector: 'ds-themed-collection-dropdown', + selector: 'ds-collection-dropdown', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [CollectionDropdownComponent], }) export class ThemedCollectionDropdownComponent extends ThemedComponent { diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html index ec470fbac5d..9cf74fd170b 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html @@ -13,7 +13,7 @@

- +
{{'comcol-role.edit.no-group' | translate}}
V diff --git a/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts index c61be492cd3..1dcbe2afa2d 100644 --- a/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -53,7 +53,7 @@ export interface ComColPageNavOption { * It expects the ID of the Community or Collection as input to be passed on as a scope */ @Component({ - selector: 'ds-comcol-page-browse-by', + selector: 'ds-base-comcol-page-browse-by', styleUrls: ['./comcol-page-browse-by.component.scss'], templateUrl: './comcol-page-browse-by.component.html', imports: [ diff --git a/src/app/shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts b/src/app/shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts index 2133bbe312d..148e0466b4b 100644 --- a/src/app/shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component.ts @@ -10,10 +10,11 @@ import { ComcolPageBrowseByComponent } from './comcol-page-browse-by.component'; * Themed wrapper for ComcolPageBrowseByComponent */ @Component({ - selector: 'ds-themed-comcol-page-browse-by', + selector: 'ds-comcol-page-browse-by', styleUrls: [], templateUrl: '../../theme-support/themed.component.html', standalone: true, + imports: [ComcolPageBrowseByComponent], }) export class ThemedComcolPageBrowseByComponent extends ThemedComponent { /** diff --git a/src/app/shared/comcol/comcol-page-handle/comcol-page-handle.component.ts b/src/app/shared/comcol/comcol-page-handle/comcol-page-handle.component.ts index 4487c20982c..e7023fd28b5 100644 --- a/src/app/shared/comcol/comcol-page-handle/comcol-page-handle.component.ts +++ b/src/app/shared/comcol/comcol-page-handle/comcol-page-handle.component.ts @@ -11,7 +11,7 @@ import { TranslateModule } from '@ngx-translate/core'; */ @Component({ - selector: 'ds-comcol-page-handle', + selector: 'ds-base-comcol-page-handle', styleUrls: ['./comcol-page-handle.component.scss'], templateUrl: './comcol-page-handle.component.html', imports: [NgIf, TranslateModule], diff --git a/src/app/shared/comcol/comcol-page-handle/themed-comcol-page-handle.component.ts b/src/app/shared/comcol/comcol-page-handle/themed-comcol-page-handle.component.ts index 590a7136bd6..98f137f9349 100644 --- a/src/app/shared/comcol/comcol-page-handle/themed-comcol-page-handle.component.ts +++ b/src/app/shared/comcol/comcol-page-handle/themed-comcol-page-handle.component.ts @@ -10,10 +10,11 @@ import { ComcolPageHandleComponent } from './comcol-page-handle.component'; * Themed wrapper for BreadcrumbsComponent */ @Component({ - selector: 'ds-themed-comcol-page-handle', + selector: 'ds-comcol-page-handle', styleUrls: [], templateUrl: '../../theme-support/themed.component.html', standalone: true, + imports: [ComcolPageHandleComponent], }) diff --git a/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html index 7c97dabf43a..efaef0a5b71 100644 --- a/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html +++ b/src/app/shared/comcol/sections/comcol-search-section/comcol-search-section.component.html @@ -1,7 +1,7 @@ - - + diff --git a/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.ts b/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.ts index ab8cb689e81..decd7c8d38c 100644 --- a/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.ts +++ b/src/app/shared/correction-suggestion/item-withdrawn-reinstate-modal.component.ts @@ -14,7 +14,7 @@ import { BehaviorSubject } from 'rxjs'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { ModalBeforeDismiss } from '../interfaces/modal-before-dismiss.interface'; -import { LoadingComponent } from '../loading/loading.component'; +import { ThemedLoadingComponent } from '../loading/themed-loading.component'; @Component({ selector: 'ds-item-withdrawn-reinstate-modal', @@ -23,7 +23,7 @@ import { LoadingComponent } from '../loading/loading.component'; imports: [ NgIf, TranslateModule, - LoadingComponent, + ThemedLoadingComponent, FormsModule, AsyncPipe, ], diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 97a5a554a0b..63b50620e7c 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -32,7 +32,7 @@
diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts index 5c552323186..05efb987a33 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts @@ -33,7 +33,7 @@ import { */ @Component({ - selector: 'ds-create-collection-parent-selector', + selector: 'ds-base-create-collection-parent-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts index 5ecd017f5c8..d521d981ca8 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component.ts @@ -7,10 +7,11 @@ import { CreateCollectionParentSelectorComponent } from './create-collection-par * Themed wrapper for CreateCollectionParentSelectorComponent */ @Component({ - selector: 'ds-themed-create-collection-parent-selector', + selector: 'ds-create-collection-parent-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [CreateCollectionParentSelectorComponent], }) export class ThemedCreateCollectionParentSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts index 27b09b749cb..6b1e51dbf5b 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts @@ -36,7 +36,7 @@ import { */ @Component({ - selector: 'ds-create-community-parent-selector', + selector: 'ds-base-create-community-parent-selector', styleUrls: ['./create-community-parent-selector.component.scss'], templateUrl: './create-community-parent-selector.component.html', standalone: true, diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts index 9568cc261da..e9f66099666 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts @@ -7,10 +7,11 @@ import { CreateCommunityParentSelectorComponent } from './create-community-paren * Themed wrapper for CreateCommunityParentSelectorComponent */ @Component({ - selector: 'ds-themed-create-community-parent-selector', + selector: 'ds-create-community-parent-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [CreateCommunityParentSelectorComponent], }) export class ThemedCreateCommunityParentSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index 4c649ba85b1..ede3cc36095 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -31,7 +31,7 @@ import { */ @Component({ - selector: 'ds-create-item-parent-selector', + selector: 'ds-base-create-item-parent-selector', // styleUrls: ['./create-item-parent-selector.component.scss'], // templateUrl: '../dso-selector-modal-wrapper.component.html', templateUrl: './create-item-parent-selector.component.html', diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts index 68bf918c45e..f6cd552629f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts @@ -10,10 +10,11 @@ import { CreateItemParentSelectorComponent } from './create-item-parent-selector * Themed wrapper for CreateItemParentSelectorComponent */ @Component({ - selector: 'ds-themed-create-item-parent-selector', + selector: 'ds-create-item-parent-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [CreateItemParentSelectorComponent], }) export class ThemedCreateItemParentSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts index 65424e643da..611a4f13dec 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts @@ -30,7 +30,7 @@ import { */ @Component({ - selector: 'ds-edit-collection-selector', + selector: 'ds-base-edit-collection-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts index 834b4a5f0b3..28604c18b66 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts @@ -7,10 +7,11 @@ import { EditCollectionSelectorComponent } from './edit-collection-selector.comp * Themed wrapper for EditCollectionSelectorComponent */ @Component({ - selector: 'ds-themed-edit-collection-selector', + selector: 'ds-edit-collection-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [EditCollectionSelectorComponent], }) export class ThemedEditCollectionSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts index 6f781cb9eb8..3f7ede0de0d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts @@ -30,7 +30,7 @@ import { */ @Component({ - selector: 'ds-edit-community-selector', + selector: 'ds-base-edit-community-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts index 27798879ff6..0e33746edc9 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts @@ -7,10 +7,11 @@ import { EditCommunitySelectorComponent } from './edit-community-selector.compon * Themed wrapper for EditCommunitySelectorComponent */ @Component({ - selector: 'ds-themed-edit-community-selector', + selector: 'ds-edit-community-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [EditCommunitySelectorComponent], }) export class ThemedEditCommunitySelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts index ebcc936a3ec..903f4327344 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts @@ -26,7 +26,7 @@ import { */ @Component({ - selector: 'ds-edit-item-selector', + selector: 'ds-base-edit-item-selector', templateUrl: 'edit-item-selector.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts index e7b0a456d21..fbb6850a021 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts @@ -7,10 +7,11 @@ import { EditItemSelectorComponent } from './edit-item-selector.component'; * Themed wrapper for EditItemSelectorComponent */ @Component({ - selector: 'ds-themed-edit-item-selector', + selector: 'ds-edit-item-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [EditItemSelectorComponent], }) export class ThemedEditItemSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.html b/src/app/shared/entity-dropdown/entity-dropdown.component.html index 374c589e3e5..049f5c81bed 100644 --- a/src/app/shared/entity-dropdown/entity-dropdown.component.html +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.html @@ -23,8 +23,8 @@
  • diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index 991f5ba8645..9b01cd799d4 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -32,7 +32,7 @@ import { } from '../empty.util'; @Component({ - selector: 'ds-file-download-link', + selector: 'ds-base-file-download-link', templateUrl: './file-download-link.component.html', styleUrls: ['./file-download-link.component.scss'], standalone: true, diff --git a/src/app/shared/file-download-link/themed-file-download-link.component.ts b/src/app/shared/file-download-link/themed-file-download-link.component.ts index e4085b06f91..c0fc4597f5c 100644 --- a/src/app/shared/file-download-link/themed-file-download-link.component.ts +++ b/src/app/shared/file-download-link/themed-file-download-link.component.ts @@ -9,10 +9,11 @@ import { ThemedComponent } from '../theme-support/themed.component'; import { FileDownloadLinkComponent } from './file-download-link.component'; @Component({ - selector: 'ds-themed-file-download-link', + selector: 'ds-file-download-link', styleUrls: [], templateUrl: '../theme-support/themed.component.html', standalone: true, + imports: [FileDownloadLinkComponent], }) export class ThemedFileDownloadLinkComponent extends ThemedComponent { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html index 001e0bc339c..944e4650abd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -1,7 +1,7 @@
    - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html index 5a5bd2ff2ae..08efff42197 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html @@ -1,7 +1,7 @@
    - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html index e661c458b67..38f15a5ed1c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html @@ -57,7 +57,7 @@
    - +
    {{ ('submission.sections.describe.relat
    - - +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}

    @@ -21,8 +21,8 @@

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + [importConfig]="importConfig" (importObject)="import($event)"> - +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts index d7090b55c8f..79831fe8ca2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts @@ -174,7 +174,7 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { }); it('should display a ds-themed-loading component', () => { - const loading = fixture.debugElement.query(By.css('ds-themed-loading')); + const loading = fixture.debugElement.query(By.css('ds-loading')); expect(loading).not.toBeNull(); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index 484abce7d65..5a1f13544ff 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -63,7 +63,7 @@ import { ExternalSourceEntryImportModalComponent } from './external-source-entry import { ThemedExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/themed-external-source-entry-import-modal.component'; @Component({ - selector: 'ds-dynamic-lookup-relation-external-source-tab', + selector: 'ds-base-dynamic-lookup-relation-external-source-tab', styleUrls: ['./dynamic-lookup-relation-external-source-tab.component.scss'], templateUrl: './dynamic-lookup-relation-external-source-tab.component.html', providers: [ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index d9581dcd85c..c2516ca9a81 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -46,7 +46,7 @@ import { SelectableListService } from '../../../../../../object-list/selectable- import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model'; import { PaginatedSearchOptions } from '../../../../../../search/models/paginated-search-options.model'; import { SearchResult } from '../../../../../../search/models/search-result.model'; -import { SearchResultsComponent } from '../../../../../../search/search-results/search-results.component'; +import { ThemedSearchResultsComponent } from '../../../../../../search/search-results/themed-search-results.component'; import { RelationshipOptions } from '../../../../models/relationship-options.model'; /** @@ -61,12 +61,12 @@ export enum ImportType { } @Component({ - selector: 'ds-external-source-entry-import-modal', + selector: 'ds-base-external-source-entry-import-modal', styleUrls: ['./external-source-entry-import-modal.component.scss'], templateUrl: './external-source-entry-import-modal.component.html', imports: [ TranslateModule, - SearchResultsComponent, + ThemedSearchResultsComponent, NgIf, AsyncPipe, ], diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts index f9d2137f9bb..35dba9ca543 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../../../../../../theme-support/themed.componen import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal.component'; @Component({ - selector: 'ds-themed-external-source-entry-import-modal', + selector: 'ds-external-source-entry-import-modal', styleUrls: [], templateUrl: '../../../../../../../shared/theme-support/themed.component.html', standalone: true, + imports: [ExternalSourceEntryImportModalComponent], }) export class ThemedExternalSourceEntryImportModalComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts index 0673bd41af3..9e4df4f5f8e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts @@ -15,10 +15,11 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component'; @Component({ - selector: 'ds-themed-dynamic-lookup-relation-external-source-tab', + selector: 'ds-dynamic-lookup-relation-external-source-tab', styleUrls: [], templateUrl: '../../../../../theme-support/themed.component.html', standalone: true, + imports: [DsDynamicLookupRelationExternalSourceTabComponent], }) export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent { protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId', diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index 12bc93ab0e6..cfd2763b924 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -1,4 +1,4 @@ -
    - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index 8bf1c9c8b4c..06b4946e124 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -53,7 +53,7 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' @Component({ - selector: 'ds-dynamic-lookup-relation-search-tab', + selector: 'ds-base-dynamic-lookup-relation-search-tab', styleUrls: ['./dynamic-lookup-relation-search-tab.component.scss'], templateUrl: './dynamic-lookup-relation-search-tab.component.html', providers: [ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts index cc82967080a..71d55e6494d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts @@ -18,10 +18,11 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' import { DsDynamicLookupRelationSearchTabComponent } from './dynamic-lookup-relation-search-tab.component'; @Component({ - selector: 'ds-themed-dynamic-lookup-relation-search-tab', + selector: 'ds-dynamic-lookup-relation-search-tab', styleUrls: [], templateUrl: '../../../../../theme-support/themed.component.html', standalone: true, + imports: [DsDynamicLookupRelationSearchTabComponent], }) export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedComponent { protected inAndOutputNames: (keyof DsDynamicLookupRelationSearchTabComponent & keyof this)[] = ['relationship', 'listId', diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index 54a1992b976..412c732b019 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -22,7 +22,7 @@

    - +

    {{'vocabulary-treeview.search.no-result' | translate}}

    diff --git a/src/app/shared/lang-switch/lang-switch.component.ts b/src/app/shared/lang-switch/lang-switch.component.ts index c29b3f375a2..3acb1694194 100644 --- a/src/app/shared/lang-switch/lang-switch.component.ts +++ b/src/app/shared/lang-switch/lang-switch.component.ts @@ -18,7 +18,7 @@ import { environment } from '../../../environments/environment'; import { LocaleService } from '../../core/locale/locale.service'; @Component({ - selector: 'ds-lang-switch', + selector: 'ds-base-lang-switch', styleUrls: ['lang-switch.component.scss'], templateUrl: 'lang-switch.component.html', standalone: true, diff --git a/src/app/shared/lang-switch/themed-lang-switch.component.ts b/src/app/shared/lang-switch/themed-lang-switch.component.ts index 2e4d7f50e54..685d0f1799d 100644 --- a/src/app/shared/lang-switch/themed-lang-switch.component.ts +++ b/src/app/shared/lang-switch/themed-lang-switch.component.ts @@ -7,10 +7,11 @@ import { LangSwitchComponent } from './lang-switch.component'; * Themed wrapper for {@link LangSwitchComponent} */ @Component({ - selector: 'ds-themed-lang-switch', + selector: 'ds-lang-switch', styleUrls: [], templateUrl: '../theme-support/themed.component.html', standalone: true, + imports: [LangSwitchComponent], }) export class ThemedLangSwitchComponent extends ThemedComponent { diff --git a/src/app/shared/loading/loading.component.ts b/src/app/shared/loading/loading.component.ts index 03846fe3946..b982af42082 100644 --- a/src/app/shared/loading/loading.component.ts +++ b/src/app/shared/loading/loading.component.ts @@ -11,7 +11,7 @@ import { Subscription } from 'rxjs'; import { hasValue } from '../empty.util'; @Component({ - selector: 'ds-loading', + selector: 'ds-base-loading', styleUrls: ['./loading.component.scss'], templateUrl: './loading.component.html', standalone: true, diff --git a/src/app/shared/loading/themed-loading.component.ts b/src/app/shared/loading/themed-loading.component.ts index 983bdadff0f..d16e0b988e7 100644 --- a/src/app/shared/loading/themed-loading.component.ts +++ b/src/app/shared/loading/themed-loading.component.ts @@ -12,10 +12,11 @@ import { LoadingComponent } from './loading.component'; * Themed wrapper for LoadingComponent */ @Component({ - selector: 'ds-themed-loading', + selector: 'ds-loading', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [LoadingComponent], }) export class ThemedLoadingComponent extends ThemedComponent { diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html index d80513d5d04..309e226772e 100644 --- a/src/app/shared/log-in/log-in.component.html +++ b/src/app/shared/log-in/log-in.component.html @@ -1,4 +1,4 @@ - +