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 e86de70c81..c18fa8234e 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 3e5ec0b9da..a4bfb810f3 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 7ea55d4454..3c4d425df3 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 e5ecb034ea..469b32be92 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 26a9cb0f28..97fc36cbb4 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 36ee9c36c1..589a6dc04c 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 5e045c88ed..f15fbc4238 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 9ab43709ad..891bb1d9d4 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 d69f0e48e9..2168d7e2bf 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 3dca7558b3..08a93f76d3 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 30d1913a51..ba9f9291cf 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 24efcf3462..a932dc637c 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 d0281681ef..6220a6e74c 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 6b520142cc..e124181b24 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 a4c71d1f42..6b7ac65a58 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 7d48ad2d00..8ae0ccfedf 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 1f9bde8a7f..fca16b28e3 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 0761cfe60e..0ce50081e4 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 115829f8d3..ae10dbd386 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 855e464f21..259f8a60e1 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 fca16b28e3..f414715f5b 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 0a32498173..2bcf8b5f13 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 b0050877c6..1e48139191 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 0ccb529167..67c0255d12 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 7412991511..ec2172523f 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 74aa1209e8..759f357f28 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 e38c05389d..c0c3daa304 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 38cadc4450..fee36bec92 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 99d646c8a0..f760813146 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 194e35b39f..646fee9dec 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 3d44d29d36..2ed45803ed 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 c15e62e698..3384071f6e 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 772b717fc0..80b3f9ee83 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 62ea4b5b74..4bb36ad4d0 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 ece9efa78d..236f7ed0ee 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 1be9662c31..a56a27d77f 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 c80025a81a..9eeeeea35c 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 f74787aa16..d87931a488 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 eeeb74cef8..8141bf23e0 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 365ced69de..196718ab87 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 a3863f5557..303d63cad5 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 08a1cf71ba..c5e33b51ae 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 236f7ed0ee..b2a50395e1 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 c5e33b51ae..cfab7c36e3 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 50a9be3d59..a18f5873b4 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 52f20470a3..e7d0e46f66 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 0000000000..6b6bf3270b --- /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 0000000000..5fff29b1b2 --- /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 0000000000..d3f310d23b --- /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 0000000000..f19e18756a --- /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 0000000000..dfacd41a96 --- /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 0000000000..ef0b7a87ed --- /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 0000000000..6184805a2b --- /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 0000000000..b33135d7b0 --- /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 0000000000..e150bb41a8 --- /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 0000000000..5934eb5e2e --- /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 0000000000..cb122a16dc --- /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 0000000000..1cd610fcd7 --- /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 0000000000..bf7c265e2e --- /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 0000000000..b19ae11b55 --- /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 0000000000..d3a16bb6d6 --- /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 0000000000..bd731d8afa --- /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 0000000000..2300ac4a56 --- /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 0000000000..c01f104c98 --- /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 0000000000..633ef492fb --- /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 0000000000..81eb59d418 --- /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 0000000000..e69de29bb2 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 0000000000..05ba4e3d1b --- /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 0000000000..6d7601bd52 --- /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 0000000000..1fd3745ec8 --- /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 0000000000..bd648d007f --- /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 0000000000..2f2e9786c2 --- /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 0000000000..2f5dbcec20 --- /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 0000000000..631d956b0b --- /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 0000000000..52e63b4fed --- /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 0000000000..2c74bddb24 --- /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 c0a3843605..b8f6402cfb 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 afd00f8568..66921afd47 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 849cbe2eda..a137a12cbd 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 6184805a2b..160d93326b 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 2f5dbcec20..4ab3588ef6 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 5e597d4d30..a9b31ab4c0 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 5934eb5e2e..2a3f18bd44 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 0000000000..2300ac4a56 --- /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 4ab3588ef6..89250cced0 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 a18f5873b4..ce5d1225b0 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 a9b31ab4c0..15a36e2904 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 160d93326b..df0d775acb 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 e150bb41a8..cc195efa47 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 2a3f18bd44..1032b1ef76 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 bf7c265e2e..18eed48452 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 d3a16bb6d6..1ccbccc599 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 633ef492fb..a37396ef45 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 81eb59d418..a45f89b606 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 05ba4e3d1b..d2b02ca9f1 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 6d7601bd52..7aac91b07a 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 2f2e9786c2..864c41d598 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 89250cced0..4cbb135684 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 631d956b0b..f9507c00c3 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 b8f6402cfb..ed2abad2a8 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 5fff29b1b2..1ea1fd5b65 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 0000000000..b3a798628f --- /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 ef0b7a87ed..0ea42a3c2b 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 df0d775acb..0c083f185d 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 b33135d7b0..2983d94386 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 cc195efa47..5c455bf1de 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 1032b1ef76..54b93363cb 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 0000000000..13535bfe17 --- /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 0000000000..7ce8c15d6b --- /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 0000000000..ac5df5815d --- /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 0000000000..1d4f33f7e2 --- /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 0000000000..a8c1b382b2 --- /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 864c41d598..0000000000 --- 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 4cbb135684..0000000000 --- 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 0000000000..24e69e42d9 --- /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 f9507c00c3..f4f92a0e63 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 52e63b4fed..2edf9594b6 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 2c74bddb24..d3537a7376 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 ed2abad2a8..5ef876c560 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 | 45 +++--- lint/src/util/theme-support.ts | 137 +++++++++++++----- 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 b3a798628f..ade2edea65 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 0ea42a3c2b..7c1370ae2d 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 0c083f185d..82cfded280 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 2983d94386..4ff38bd0c3 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 5c455bf1de..d02883de74 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 54b93363cb..d9cc3127ed 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 cb122a16dc..7bff24718c 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 1cd610fcd7..47357e7cd3 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 13535bfe17..bfbf7ec7f2 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 interface PluginExports { + name: string, + language: string, + rules: Record, + index: RuleExports[], } export function bundle( name: string, language: string, index: RuleExports[], -): { - 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 18eed48452..6a3807a536 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; - for (const heritageClause of node.parent.heritageClauses) { + if (classNode.name === undefined || classNode.heritageClauses === undefined) { + return; + } + + 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; + if ((type as any).expression.escapedText === 'ThemedComponent') { + if (type.kind !== ts.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) { + continue; + } - 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); + const firstTypeArg = type.typeArguments[0] as ts.TypeReferenceNode; + const baseClass = (firstTypeArg.typeName as ts.Identifier)?.escapedText; - 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 (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 0000000000..5b7bdb858d --- /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 a8c1b382b2..11c9bec46c 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 f4f92a0e63..f86870ec29 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 5ef876c560..9351afde7b 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 a137a12cbd..e01899307c 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 d9cc3127ed..1263e44b48 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 5b7bdb858d..90b0f2c49f 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 ce5d1225b0..147aaeb463 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 4ff38bd0c3..a7fdfe41ef 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 0000000000..727785c6d7 --- /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 1263e44b48..2387ea76dd 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 7bff24718c..70ee903fb8 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 0000000000..10408cc316 --- /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 6a3807a536..2458b3f665 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 90b0f2c49f..dca83637d7 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 bd731d8afa..b445040539 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 a45f89b606..2697a8c598 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 0000000000..f72161b2bf --- /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 7aac91b07a..ff6ec3b2c0 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 1fd3745ec8..0b61883e35 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 f86870ec29..cfa54c5b85 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 190f3ff927..3d978dfaca 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 b168219916..0613e5e712 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 2387ea76dd..3f7c2e7fdc 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 6e967b53b5..e24eee3a24 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 92968d2e28..ff92354722 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}}

- + - + - + >
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 d6215be971..99909b8257 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 cd26a11995..00514d2afc 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 165f48384a..6127fd42a9 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 fdc34fe4ab..62a66039af 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 fb9983a6ef..9016f42dc1 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 b306eb2721..f7d2c60832 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 @@
- +
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 f4d285ac74..36b0816ade 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 4d8a9946f5..7e922485cb 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 2483335245..9820d672c9 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 2e471fd92d..255af605dc 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 3382a46f58..c84fc4cb77 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 69c3c31c46..22e564ac27 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 c3bad2a923..3c893f5259 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 39a90de83e..94956f257d 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 38c4c1333c..d99332baf1 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 77f85d5f78..3ca9665b1c 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 @@
- - +
diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index d5da37c12f..f9739a4aa9 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 c015213ee1..5b5d0a84d7 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 7b4ac97421..2294085096 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.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 8d095dd229..7f0b2efba2 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 eb44ffdc9b..f7c5dc4b14 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 2dff557835..421049990a 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 e095e6eb68..c84d7c5fb4 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 4392fb87d0..ca05201478 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 aaf7bc2000..ca0db89f53 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 ea34e02d56..a59b059026 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 e0456ca700..5819471d7e 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 dc6c0aa345..5340384ed5 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 4f0575c7db..d427b1bec1 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 b3e577af7d..a695e2019a 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 ce3d05aef9..bdb3a50c6b 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 b5fbf1a01d..59d7b3bb5e 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 @@ - + 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 2935f25595..1e8ff1d46c 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 ff5d057b31..4a965bc926 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 515e08ffdf..a811014bcc 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 0834d08ba5..7f9840f6b7 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 @@ - + 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 4f74eff601..36bd9919bb 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 11b62d68e4..5988ad0f5e 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 41a2960719..b655452041 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 b79b185b40..0c1088e3fd 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 ccfcfca93c..fbbfe982aa 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 6365f1ea99..648a638830 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 9de9c539a2..063263b670 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 33ce615305..bca1a1b3b3 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 de80448e8d..3b14a1d8b0 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 b537fb60d9..20084f21ba 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 545e8c67a6..59d2ea4dc9 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 976d2c55e5..7b444d261c 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 1025b4c35d..2181a4eb07 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 60251c15b8..ed1181a407 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 49d6ecb59c..1145fb78e3 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 3043cf3284..aa124a88ce 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 1c3ae83026..a09484ebca 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 03441f1bff..74cfdf31ce 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 85efec18ee..4d1b6d6fb7 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 aaa0c27b46..995108cdbc 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 a7455c4ca9..2ab05e6518 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 936815a49c..af9f557fbb 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 3d8d6e3cd7..442e4bf9fa 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 e74fed2f36..956568e2bf 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 ba3060ff69..60b38dcabf 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 862173b9bb..53f1057531 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 5895530e8a..64d36edae3 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 e98959d162..e59086134a 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -6,10 +6,10 @@