From 1a1aed6425eed37d0a84ea5dea41e8d9f9c363aa Mon Sep 17 00:00:00 2001 From: Yanick Minder <79108296+kcinay055679@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:45:31 +0100 Subject: [PATCH] Feature/1135 formatter enable rules rebased (#1260) * apply eslint squash branch update eslint config run formatter fix tests revert to single quotes regen package-lock move eslint dependendencies to devDependencies and update comment in eslint-config remove not needed parent clings fix typo in comment in scoring component tests put both statements inside if statement change asignment of args in global ts add curly to eslint config put curly into quotes update comments to clarify that they are for lint replace multiple ifs with one switch replace multiple ifs with one switch replace multiple ifs with one switch turn no-non-null-assertion off and bring back all usages of add strict equality fix eslint errors remove not needed comment add todo for error handling oauth interceptor update comments so its clear that linter needs them revert change made accidentally fix frontend unit tests revert changes made accidentally bring back all not null assertions and create some switch statements in scoring component for readability * fix formatting errors * regen package-lock --------- Co-authored-by: Nevio Di Gennaro --- frontend/cypress/e2e/check-in.cy.ts | 18 +- frontend/cypress/e2e/objective-backlog.cy.ts | 16 +- frontend/cypress/e2e/objective-crud.cy.ts | 14 +- frontend/cypress/e2e/objective.cy.ts | 18 +- frontend/cypress/e2e/team-management.cy.ts | 6 +- frontend/cypress/support/commands.ts | 24 +- frontend/cypress/support/component.ts | 2 +- .../dom-helper/dialogs/objectiveDialog.ts | 2 +- .../support/helper/dom-helper/filterHelper.ts | 4 +- .../helper/dom-helper/pages/overviewPage.ts | 4 +- .../cypress/support/helper/scoringSupport.ts | 82 ++-- frontend/cypress/support/helper/utils.ts | 4 +- frontend/eslint.config.mjs | 112 +++-- frontend/package-lock.json | 402 +++++++++++++++++- frontend/package.json | 2 + frontend/src/app/app.component.spec.ts | 40 +- frontend/src/app/app.component.ts | 6 +- .../action-plan/action-plan.component.spec.ts | 4 +- .../action-plan/action-plan.component.ts | 21 +- .../metric-check-in-directive.spec.ts | 2 +- .../metric-check-in-directive.ts | 13 +- .../check-in-form-ordinal.component.html | 18 +- .../check-in-form/check-in-form.component.ts | 8 +- .../confidence/confidence.component.spec.ts | 6 +- .../key-result-detail.component.html | 11 +- .../key-result-form.component.ts | 8 +- .../key-result-type.component.ts | 9 +- .../objective-detail.component.html | 6 +- .../objective/ObjectiveMenuActions.ts | 8 +- .../overview/overview.component.html | 2 +- .../overview/overview.component.spec.ts | 29 +- .../team-filter/team-filter.component.spec.ts | 4 - .../team-filter/team-filter.component.ts | 3 +- .../src/app/components/team/team.component.ts | 4 +- .../interceptors/error.interceptor.spec.ts | 113 +++-- .../src/app/interceptors/error.interceptor.ts | 34 +- .../app/interceptors/o-auth.interceptor.ts | 22 +- frontend/src/app/services/action.service.ts | 2 +- .../src/app/services/check-in.service.spec.ts | 1 - .../src/app/services/customization.service.ts | 6 +- .../src/app/services/dialog.service.spec.ts | 8 +- .../src/app/services/keyresult.service.ts | 4 +- .../objective-menu-actions.service.ts | 6 +- .../src/app/services/overview.service.spec.ts | 2 +- frontend/src/app/services/team.service.ts | 3 +- .../src/app/services/user.service.spec.ts | 10 +- frontend/src/app/services/user.service.ts | 2 +- frontend/src/app/shared/common.spec.ts | 38 +- frontend/src/app/shared/common.ts | 31 +- .../custom/scoring/scoring.component.html | 2 +- .../custom/scoring/scoring.component.spec.ts | 4 +- .../custom/scoring/scoring.component.ts | 10 +- .../complete-dialog.component.html | 4 +- .../complete-dialog.component.ts | 7 +- .../objective-form.component.html | 5 +- .../objective-form.component.spec.ts | 133 +++--- .../objective-form.component.ts | 60 +-- .../unit-transformation.pipe.spec.ts | 2 +- .../unit-transformation.pipe.ts | 2 +- frontend/src/app/shared/routeUtils.spec.ts | 10 +- frontend/src/app/shared/routeUtils.ts | 2 +- .../shared/side-panel/side-panel.component.ts | 5 +- .../add-edit-team-dialog.component.spec.ts | 14 +- .../add-edit-team-dialog.component.ts | 27 +- .../add-member-to-team-dialog.component.ts | 18 +- .../add-user-team.component.spec.ts | 3 - .../add-user-team/add-user-team.component.ts | 22 +- .../delete-user/delete-user.component.ts | 38 +- .../member-detail/member-detail.component.ts | 12 +- .../member-list-mobile.component.ts | 6 +- .../member-list-table.component.ts | 6 +- .../member-list/member-list.component.spec.ts | 29 +- .../member-list/member-list.component.ts | 15 +- .../search-team-management.component.ts | 7 +- .../team-list/team-list.component.spec.ts | 4 - .../team-management-banner.component.spec.ts | 4 +- .../team-management-banner.component.ts | 6 +- .../team-management/team-management.module.ts | 4 +- frontend/src/global.ts | 5 +- 79 files changed, 1053 insertions(+), 607 deletions(-) diff --git a/frontend/cypress/e2e/check-in.cy.ts b/frontend/cypress/e2e/check-in.cy.ts index d7cdd63fef..ef3c48c1a2 100644 --- a/frontend/cypress/e2e/check-in.cy.ts +++ b/frontend/cypress/e2e/check-in.cy.ts @@ -17,7 +17,7 @@ describe('okr check-in', () => { cy.loginAsUser(users.gl); }); - it(`should create check-in metric`, () => { + it('should create check-in metric', () => { overviewPage .addKeyResult() .fillKeyResultTitle('Very important keyresult') @@ -39,7 +39,7 @@ describe('okr check-in', () => { cy.contains('We bought a new house'); }); - it(`should create check-in metric with confidence 0`, () => { + it('should create check-in metric with confidence 0', () => { overviewPage .addKeyResult() .fillKeyResultTitle('Very important keyresult') @@ -61,7 +61,7 @@ describe('okr check-in', () => { cy.contains('We bought a new house'); }); - it(`should create check-in metric with value below baseline`, () => { + it('should create check-in metric with value below baseline', () => { overviewPage .addKeyResult() .fillKeyResultTitle('This will not be good') @@ -249,7 +249,7 @@ describe('okr check-in', () => { cy.contains('STRETCH'); }); - it(`should display confirm dialog when creating check-in on draft objective`, () => { + it('should display confirm dialog when creating check-in on draft objective', () => { overviewPage.addObjective() .fillObjectiveTitle('draft objective title') .selectQuarter('3') @@ -270,7 +270,7 @@ describe('okr check-in', () => { .checkDescription('Dein Objective befindet sich noch im DRAFT Status. Möchtest du das Check-in trotzdem erfassen?'); }); - it(`should only display last value div if last check-in is present`, () => { + it('should only display last value div if last check-in is present', () => { const objectiveName = uniqueSuffix('new objective'); overviewPage.addObjective() @@ -310,8 +310,12 @@ function getCurrentDate() { let dd_str = '' + dd; let mm_str = '' + mm; - if (dd < 10) dd_str = '0' + dd_str; - if (mm < 10) mm_str = '0' + mm_str; + if (dd < 10) { + dd_str = '0' + dd_str; + } + if (mm < 10) { + mm_str = '0' + mm_str; + } return dd_str + '.' + mm_str + '.' + yyyy; } diff --git a/frontend/cypress/e2e/objective-backlog.cy.ts b/frontend/cypress/e2e/objective-backlog.cy.ts index d3df96c62e..13870e8967 100644 --- a/frontend/cypress/e2e/objective-backlog.cy.ts +++ b/frontend/cypress/e2e/objective-backlog.cy.ts @@ -10,7 +10,7 @@ describe('okr objective backlog', () => { cy.loginAsUser(users.gl); }); - it(`should not have save button when creating objective in backlog quarter`, () => { + it('should not have save button when creating objective in backlog quarter', () => { overviewPage .addObjective() .fillObjectiveTitle('Objective in quarter backlog') @@ -27,7 +27,7 @@ describe('okr objective backlog', () => { cy.contains('Objective in quarter backlog'); }); - it(`should edit objective and move it to backlog`, () => { + it('should edit objective and move it to backlog', () => { overviewPage.addObjective() .fillObjectiveTitle('Move to another quarter on edit') .submitDraftObjective(); @@ -50,7 +50,7 @@ describe('okr objective backlog', () => { cy.contains('This goes now to backlog'); }); - it(`should not be able to select backlog quarter when editing ongoing objective`, () => { + it('should not be able to select backlog quarter when editing ongoing objective', () => { overviewPage.addObjective() .fillObjectiveTitle('We can not move this to backlog') .submit(); @@ -67,7 +67,7 @@ describe('okr objective backlog', () => { .should('not.contain', 'Backlog'); }); - it(`should release objective to another quarter from backlog`, () => { + it('should release objective to another quarter from backlog', () => { overviewPage.visitBacklogQuarter(); overviewPage.addObjective() .fillObjectiveTitle('We can not release this') @@ -105,7 +105,7 @@ describe('okr objective backlog', () => { cy.contains('This is our first released objective'); }); - it(`should edit objective title in backlog`, () => { + it('should edit objective title in backlog', () => { overviewPage.visitBacklogQuarter(); overviewPage.addObjective() .fillObjectiveTitle('This is possible for edit') @@ -124,7 +124,7 @@ describe('okr objective backlog', () => { overviewPage.getObjectiveByNameAndState('My new title', 'draft'); }); - it(`should edit objective in backlog and change quarter`, () => { + it('should edit objective in backlog and change quarter', () => { overviewPage.visitBacklogQuarter(); overviewPage.addObjective() .fillObjectiveTitle('This goes to other quarter later') @@ -144,7 +144,7 @@ describe('okr objective backlog', () => { overviewPage.getObjectiveByNameAndState('This goes to other quarter later', 'draft'); }); - it(`should duplicate objective in backlog`, () => { + it('should duplicate objective in backlog', () => { overviewPage.visitBacklogQuarter(); overviewPage.addObjective() .fillObjectiveTitle('Ready for duplicate in backlog') @@ -189,7 +189,7 @@ describe('okr objective backlog', () => { .should('exist'); }); - it(`should duplicate ongoing objective to backlog`, () => { + it('should duplicate ongoing objective to backlog', () => { overviewPage.addObjective() .fillObjectiveTitle('Possible to duplicate into backlog') .submit(); diff --git a/frontend/cypress/e2e/objective-crud.cy.ts b/frontend/cypress/e2e/objective-crud.cy.ts index 22bf882b66..a7d52c3db0 100644 --- a/frontend/cypress/e2e/objective-crud.cy.ts +++ b/frontend/cypress/e2e/objective-crud.cy.ts @@ -18,7 +18,7 @@ describe('crud operations', () => { 'draft-icon.svg']].forEach(([objectiveTitle, buttonTestId, icon]) => { - it(`should create objective without key-results`, () => { + it('should create objective without key-results', () => { overviewPage.addObjective() .fillObjectiveTitle(objectiveTitle) .selectQuarter('3'); @@ -32,7 +32,7 @@ describe('crud operations', () => { }); }); - it(`should display error message when title not set`, () => { + it('should display error message when title not set', () => { overviewPage.addObjective(); cy.getByTestId('title') .first() @@ -47,7 +47,7 @@ describe('crud operations', () => { .should('not.be.disabled'); }); - it(`should cancel creating an objective`, () => { + it('should cancel creating an objective', () => { const objectiveTitle = 'this is a canceled objective'; overviewPage.addObjective() .selectQuarter('3') @@ -57,7 +57,7 @@ describe('crud operations', () => { .should('not.exist'); }); - it(`should delete existing objective`, () => { + it('should delete existing objective', () => { overviewPage.getFirstObjective() .findByTestId('three-dot-menu') .click(); @@ -69,7 +69,7 @@ describe('crud operations', () => { .submit(); }); - it(`should open objective detail view via click`, () => { + it('should open objective detail view via click', () => { overviewPage.getFirstObjective() .find('.title') .click(); @@ -77,7 +77,7 @@ describe('crud operations', () => { .should('include', 'objective'); }); - it(`should edit objective`, () => { + it('should edit objective', () => { const updatedTitle = 'This is an updated title'; overviewPage.getFirstObjective() .findByTestId('three-dot-menu') @@ -90,7 +90,7 @@ describe('crud operations', () => { .should('exist'); }); - it(`should duplicate objective`, () => { + it('should duplicate objective', () => { const duplicatedTitle = 'This is a duplicated objective'; overviewPage.getFirstObjective() .findByTestId('three-dot-menu') diff --git a/frontend/cypress/e2e/objective.cy.ts b/frontend/cypress/e2e/objective.cy.ts index f437e050d7..43ee55abaf 100644 --- a/frontend/cypress/e2e/objective.cy.ts +++ b/frontend/cypress/e2e/objective.cy.ts @@ -11,7 +11,7 @@ describe('okr objective', () => { }); describe('tests via click', () => { - it(`should release objective from draft to ongoing`, () => { + it('should release objective from draft to ongoing', () => { overviewPage.addObjective() .fillObjectiveTitle('A objective in state draft') .submitDraftObjective(); @@ -31,7 +31,7 @@ describe('okr objective', () => { .should('exist'); }); - it(`should complete objective with successful`, () => { + it('should complete objective with successful', () => { overviewPage.addObjective() .fillObjectiveTitle('We want to complete this successful') .submit(); @@ -58,7 +58,7 @@ describe('okr objective', () => { overviewPage.getObjectiveByNameAndState('We want to complete this successful', 'successful'); }); - it(`should complete objective with not-successful`, () => { + it('should complete objective with not-successful', () => { overviewPage.addObjective() .fillObjectiveTitle('A not successful objective') .submit(); @@ -84,7 +84,7 @@ describe('okr objective', () => { overviewPage.getObjectiveByNameAndState('A not successful objective', 'not-successful'); }); - it(`should reopen successful objective`, () => { + it('should reopen successful objective', () => { overviewPage.addObjective() .fillObjectiveTitle('This objective will be reopened after') .submit(); @@ -119,7 +119,7 @@ describe('okr objective', () => { .should('exist'); }); - it(`should cancel reopening successful objective`, () => { + it('should cancel reopening successful objective', () => { overviewPage.addObjective() .fillObjectiveTitle('The reopening of this objective will be canceled') .submit(); @@ -198,7 +198,7 @@ describe('okr objective', () => { .should('exist'); }); - it(`should search and find objectives`, () => { + it('should search and find objectives', () => { overviewPage.addObjective() .fillObjectiveTitle('Search after this objective') .submit(); @@ -258,7 +258,7 @@ describe('okr objective', () => { .should('not.exist'); }); - it(`should create objective in other quarter`, () => { + it('should create objective in other quarter', () => { overviewPage.addObjective() .fillObjectiveTitle('Objective in quarter 3') .selectQuarter('3') @@ -272,7 +272,7 @@ describe('okr objective', () => { cy.contains('Objective in quarter 3'); }); - it(`should edit objective and move it to another quarter`, () => { + it('should edit objective and move it to another quarter', () => { overviewPage.addObjective() .fillObjectiveTitle('Move to another quarter on edit') .submit(); @@ -296,7 +296,7 @@ describe('okr objective', () => { }); describe('tests via keyboard', () => { - it(`should open objective aside via enter`, () => { + it('should open objective aside via enter', () => { cy.getByTestId('objective') .first() .find('[tabindex]') diff --git a/frontend/cypress/e2e/team-management.cy.ts b/frontend/cypress/e2e/team-management.cy.ts index f41ac166ec..8f2333955a 100644 --- a/frontend/cypress/e2e/team-management.cy.ts +++ b/frontend/cypress/e2e/team-management.cy.ts @@ -304,7 +304,7 @@ describe('okr team-management', () => { return true; }) .then(() => { - expect(foundEsha).to.be.true; + expect(foundEsha).to.equal(true); }); }); @@ -461,7 +461,7 @@ describe('okr team-management', () => { .trim() !== 'Findus Peterson') { return; } - $row.find(`[data-testId='edit-role']`) + $row.find('[data-testId=\'edit-role\']') .click(); cy.wait(500); // wait for dialog to open }) @@ -495,7 +495,7 @@ describe('okr team-management', () => { ConfirmDialog.do() .checkTitle('Mitglied entfernen') - .checkDescription(`Möchtest du Findus Peterson wirklich aus dem Team '/BBT' entfernen?`) + .checkDescription('Möchtest du Findus Peterson wirklich aus dem Team \'/BBT\' entfernen?') .submit(); cy.get('app-member-detail') diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index e8a1ce80f8..e4c4efdc80 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -19,19 +19,17 @@ Cypress.Commands.add('getByTestId', (testId: string, text?: string): Chainable = } }); -Cypress.Commands.add('findByTestId', - { prevSubject: true }, - (subject: JQuery, testId: string, text?: string): Chainable => { - const selector = `[data-testId=${testId}]`; - if (text) { - return cy.wrap(subject) - .find(selector) - .contains(text); - } else { - return cy.wrap(subject) - .find(selector); - } - }); +Cypress.Commands.add('findByTestId', { prevSubject: true }, (subject: JQuery, testId: string, text?: string): Chainable => { + const selector = `[data-testId=${testId}]`; + if (text) { + return cy.wrap(subject) + .find(selector) + .contains(text); + } else { + return cy.wrap(subject) + .find(selector); + } +}); Cypress.Commands.add('pressUntilContains', (text: string, key: keyof typeof keyCodeDefinitions) => { pressUntilContains(text, key); diff --git a/frontend/cypress/support/component.ts b/frontend/cypress/support/component.ts index 2733f4b08b..3d45dba0b0 100644 --- a/frontend/cypress/support/component.ts +++ b/frontend/cypress/support/component.ts @@ -2,7 +2,7 @@ import './commands'; import { keyCodeDefinitions } from 'cypress-real-events/keyCodeDefinitions'; declare global { - namespace Cypress { + export namespace Cypress { interface Chainable { loginAsUser(user: any): Chainable; getByTestId(testsId: string, text?: string): Chainable; diff --git a/frontend/cypress/support/helper/dom-helper/dialogs/objectiveDialog.ts b/frontend/cypress/support/helper/dom-helper/dialogs/objectiveDialog.ts index 4957e72663..6252040b00 100644 --- a/frontend/cypress/support/helper/dom-helper/dialogs/objectiveDialog.ts +++ b/frontend/cypress/support/helper/dom-helper/dialogs/objectiveDialog.ts @@ -21,7 +21,7 @@ export default class ObjectiveDialog extends Dialog { toggleCreateKeyResults() { cy.getByTestId('keyResult-checkbox') - .find("[type='checkbox']") + .find('[type=\'checkbox\']') .check(); return this; } diff --git a/frontend/cypress/support/helper/dom-helper/filterHelper.ts b/frontend/cypress/support/helper/dom-helper/filterHelper.ts index aab16bf130..0b377d8ea4 100644 --- a/frontend/cypress/support/helper/dom-helper/filterHelper.ts +++ b/frontend/cypress/support/helper/dom-helper/filterHelper.ts @@ -1,7 +1,9 @@ import { PageObjectMapperBase } from './pageObjectMapperBase'; export default class FilterHelper extends PageObjectMapperBase { - validatePage(): void {} + validatePage(): void { + // Does not need to be implemented this comment is for making linter happy + } optionShouldBeSelected(text: string, isOnOverview = true): this { if (isOnOverview) { diff --git a/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts b/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts index 87b20f8ccc..68a806b995 100644 --- a/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts +++ b/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts @@ -196,7 +196,9 @@ export default class CyOverviewPage extends Page { return ''; } - validatePage(): void {} + validatePage(): void { + // Does not need to be implemented this comment is for making linter happy + } protected doVisit(): void { this.elements.logo() diff --git a/frontend/cypress/support/helper/scoringSupport.ts b/frontend/cypress/support/helper/scoringSupport.ts index ccb55625de..cfa2c75aca 100644 --- a/frontend/cypress/support/helper/scoringSupport.ts +++ b/frontend/cypress/support/helper/scoringSupport.ts @@ -20,7 +20,9 @@ export function validateScoring(isOverview: boolean, percentage: number) { validateScoringWidth('commit', scoringValue.commitPercent, isOverview); validateScoringWidth('target', scoringValue.targetPercent, isOverview); - if (percentage == 0) return; + if (percentage == 0) { + return; + } validateScoringColor('fail', rgbCode, isOverview); validateScoringColor('commit', rgbCode, isOverview); validateScoringColor('target', rgbCode, isOverview); @@ -30,15 +32,22 @@ export function getPercentageMetric(baseline: number, stretchGoal: number, value if (isLastCheckInNegative(baseline, stretchGoal, value)) { return -1; } - return (Math.abs(value - baseline) / Math.abs(stretchGoal - baseline)) * 100; + return Math.abs(value - baseline) / Math.abs(stretchGoal - baseline) * 100; } export function getPercentageOrdinal(zone: string) { - if (zone == 'stretch') return 101; - if (zone == 'target') return 99.99; - if (zone == 'commit') return 70; - if (zone == 'fail') return 30; - return 0; + switch (zone) { + case 'fail': + return 30; + case 'commit': + return 70; + case 'target': + return 99.99; + case 'stretch': + return 101; + default: + return 0; + } } function validateScoringWidth(zone: string, percent: number, isOverview: boolean) { @@ -46,7 +55,7 @@ function validateScoringWidth(zone: string, percent: number, isOverview: boolean .parent() .invoke('width') .then((parentWidth) => { - expect(parentWidth).not.to.be.undefined; + expect(parentWidth).not.to.equal(undefined); cy.getZone(zone, isOverview) .invoke('width') .should('be.within', parentWidth! * (percent / 100) - 3, parentWidth! * (percent / 100) + 3); @@ -77,30 +86,43 @@ function checkVisibilityOfScoringComponent(isOverview: boolean, displayProperty: } function colorFromPercentage(percentage: number) { - if (percentage >= 100) return 'rgba(0, 0, 0, 0)'; - if (percentage > 70) return 'rgb(30, 138, 41)'; - if (percentage > 30) return 'rgb(255, 214, 0)'; - return 'rgb(186, 56, 56)'; + switch (true) { + case percentage >= 100: + return 'rgba(0, 0, 0, 0)'; + case percentage > 70: + return 'rgb(30, 138, 41)'; + case percentage > 30: + return 'rgb(255, 214, 0)'; + default: + return 'rgb(186, 56, 56)'; + } } function scoringValueFromPercentage(percentage: number): ScoringValue { - if (percentage >= 100) { - return { failPercent: 0, - commitPercent: 0, - targetPercent: 0 }; - } else if (percentage > 70) { - const targetPercent = (percentage - 70) * (100 / 30); - return { failPercent: 100, - commitPercent: 100, - targetPercent: targetPercent }; - } else if (percentage > 30) { - const commitPercent = (percentage - 30) * (100 / 40); - return { failPercent: 100, - commitPercent: commitPercent, - targetPercent: -1 }; + switch (true) { + case percentage >= 100: + return { + failPercent: 0, + commitPercent: 0, + targetPercent: 0 + }; + case percentage > 70: + return { + failPercent: 100, + commitPercent: 100, + targetPercent: (percentage - 70) * (100 / 30) + }; + case percentage > 30: + return { + failPercent: 100, + commitPercent: (percentage - 30) * (100 / 40), + targetPercent: -1 + }; + default: + return { + failPercent: percentage * (100 / 30), + commitPercent: -1, + targetPercent: -1 + }; } - const failPercent = percentage * (100 / 30); - return { failPercent: failPercent, - commitPercent: -1, - targetPercent: -1 }; } diff --git a/frontend/cypress/support/helper/utils.ts b/frontend/cypress/support/helper/utils.ts index 454ba2433c..3600ff1b54 100644 --- a/frontend/cypress/support/helper/utils.ts +++ b/frontend/cypress/support/helper/utils.ts @@ -30,7 +30,9 @@ function doUntil( limit = 100, count = 0 ) { - if (count >= limit) return; + if (count >= limit) { + return; + } cy.focused() .then((element) => { diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 3b2afd4e25..bc3c1546ad 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -7,6 +7,9 @@ import angular from 'angular-eslint' import htmlParser from '@html-eslint/parser' export default tsEslint.config( + { + ignores: ['cypress/downloads/**/*'], + }, { files: ['**/*.ts'], extends: [ @@ -16,35 +19,83 @@ export default tsEslint.config( ...angular.configs.tsRecommended, ], processor: angular.processInlineTemplates, + languageOptions: { + globals: { + //Cypress things not recognized by eslint + cy: 'readonly', + Cypress: 'readonly', + it: 'readonly', + describe: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + before: 'readonly', + //Dom things not recognized by eslint + localStorage: 'readonly', + console: 'readonly', + window: 'readonly', + document: 'readonly', + //Event not recognized by eslint + MouseEvent: 'readonly', + KeyboardEvent: 'readonly', + Event: 'readonly', + //HTML Elements not recognized by eslint + HTMLDivElement: 'readonly', + HTMLInputElement: 'readonly', + HTMLSpanElement: 'readonly', + HTMLElement: 'readonly', + HTMLTitleElement: 'readonly', + HTMLHtmlElement: 'readonly', + //Others not recognized by eslint + ResizeObserver: 'readonly', + ResizeObserverEntry: 'readonly', + setTimeout: 'readonly', + JQuery: 'readonly', + Document: 'readonly', + URL: 'readonly', + }, + }, rules: { ...stylistic.configs['all-flat'].rules, + //eslint rules 'unused-imports/no-unused-imports': 'error', + 'no-undef': 'error', + curly: 'error', + 'prefer-rest-params': 'error', + 'space-before-function-paren': ['error', 'never'], - // ToDo: Disable rules so eslint passes, fix in followup ticket - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'no-undef': 'off', + //Typescript eslint rules + '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/no-unused-expressions': [ + 'error', + { + allowTernary: true, + }, + ], + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'none', + }, + ], '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-namespace': 'off', - 'prefer-rest-params': 'off', - '@typescript-eslint/no-empty-function': ['off'], - '@stylistic/lines-around-comment': ['off'], - '@angular-eslint/no-empty-lifecycle-method': 'off', - '@angular-eslint/component-class-suffix': 'off', - '@angular-eslint/template/eqeqeq': 'off', - '@angular-eslint/template/interactive-supports-focus': 'off', - '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', + '@typescript-eslint/no-namespace': [ + 'error', + { + allowDeclarations: true, + }, + ], + '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions', 'constructors'] }], + //Turned off to allow ! in the code '@typescript-eslint/no-non-null-assertion': 'off', - '@stylistic/no-extra-parens': 'off', - '@typescript-eslint/no-confusing-non-null-assertion': 'off', - //Delete these rules after fixing all the issues and enabling the actual rules - '@stylistic/quotes': 'off', - '@stylistic/function-call-argument-newline': 'off', + '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', - //Actual formatting rules - // '@stylistic/function-call-argument-newline': ['error', 'never'], - // '@stylistic/quotes': ['error', 'double'], + '@typescript-eslint/no-confusing-non-null-assertion': 'error', + + //Stylistic eslint rules + '@stylistic/no-extra-parens': 'error', + '@stylistic/function-call-argument-newline': ['error', 'never'], + '@stylistic/quotes': ['error', 'single'], '@stylistic/padded-blocks': ['error', 'never'], '@stylistic/dot-location': ['error', 'property'], '@stylistic/newline-per-chained-call': ['error', { ignoreChainWithDepth: 1 }], @@ -55,10 +106,13 @@ export default tsEslint.config( '@stylistic/object-curly-spacing': ['error', 'always'], '@stylistic/array-bracket-newline': ['error', { minItems: 4 }], '@stylistic/semi-style': ['error'], - 'space-before-function-paren': ['error', 'never'], '@stylistic/function-paren-newline': ['error', { minItems: 4 }], '@stylistic/space-before-function-paren': ['error', 'never'], - + // Disabled because it's an unnecessary rule in our case + '@stylistic/lines-around-comment': 'off', + //Angular eslint rules + '@angular-eslint/no-empty-lifecycle-method': 'error', + '@angular-eslint/component-class-suffix': 'error', '@angular-eslint/directive-selector': [ 'error', { @@ -80,10 +134,14 @@ export default tsEslint.config( }, { files: ['**/*.spec.ts'], + extends: [...tsEslint.configs.recommended], rules: { + //Rules removed for Test files because they are unnecessary for tests '@typescript-eslint/no-explicit-any': 'off', 'prefer-rest-params': 'off', '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', }, }, @@ -96,11 +154,13 @@ export default tsEslint.config( }, rules: { ...html.configs['flat/recommended'].rules, + //Html eslint rules // Must be defined. If not, all recommended rules will be lost - '@html-eslint/indent': ['error', 2], '@html-eslint/require-img-alt': 'off', - '@html-eslint/element-newline': 'off', + '@html-eslint/indent': ['error', 2], '@html-eslint/require-closing-tags': ['error', { selfClosing: 'always' }], + //Doesn't work with Angular 17+ + '@html-eslint/element-newline': 'off', }, }, { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3b7dd96e24..050060cc9a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,6 +30,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^18.2.9", + "@angular-eslint/eslint-plugin-template": "^19.0.2", "@angular/cli": "~18.2.9", "@angular/compiler-cli": "^18.2.8", "@cypress/schematic": "^2.5.2", @@ -47,6 +48,7 @@ "cypress": "^13.15.0", "cypress-real-events": "^1.13.0", "eslint": "^9.16.0", + "eslint-plugin-i18n-json": "^4.0.0", "eslint-plugin-unused-imports": "^4.1.4", "jest": "^29.7.0", "jest-preset-angular": "^14.2.4", @@ -3620,6 +3622,62 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.1.tgz", + "integrity": "sha512-Ip9uV+/MpLXWRk03U/GzeJMuPeOXpJBSB5V1tjA6kJhvqssye5J5LoYLc7Z5IAHb7nR62sRoguzrFiVCP/hnzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.5", + "@formatjs/intl-localematcher": "0.5.9", + "decimal.js": "10", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.5.tgz", + "integrity": "sha512-6PoewUMrrcqxSoBXAOJDiW1m+AmkrAj0RiXnOMD59GRaswjXhm3MDhgepXPBgonc09oSirAJTsAggzAGQf6A6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.7.tgz", + "integrity": "sha512-cuEHyRM5VqLQobANOjtjlgU7+qmk9Q3fDQuBiRRJ3+Wp3ZoZhpUPtUfuimZXsir6SaI2TaAJ+SLo9vLnV5QcbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "@formatjs/icu-skeleton-parser": "1.8.11", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.11.tgz", + "integrity": "sha512-8LlHHE/yL/zVJZHAX3pbKaCjZKmBIO6aJY1mkVh4RMSEu/2WRZ4Ysvv3kKXJ9M8RJLBHdnk1/dUQFdod1Dt7Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.1", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.9.tgz", + "integrity": "sha512-8zkGu/sv5euxbjfZ/xmklqLyDGQSxsLqg8XOq88JW3cmJtzhCP8EtSJXlaKZnVO4beEaoiT9wj4eIoCQ9smwxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@html-eslint/eslint-plugin": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@html-eslint/eslint-plugin/-/eslint-plugin-0.27.0.tgz", @@ -6577,6 +6635,16 @@ "node": ">=8" } }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -8947,6 +9015,16 @@ "dev": true, "license": "MIT" }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -9029,9 +9107,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.0.tgz", + "integrity": "sha512-pmjBRsZD5Fz3XR8NlM1zTBDlHjB5OvLjMkGMGHrLabzoECJNPPOCiCNEgSSLQn2pYIOO4Ytfia7zIehRiit+sQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9506,6 +9584,142 @@ } } }, + "node_modules/eslint-plugin-i18n-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-json/-/eslint-plugin-i18n-json-4.0.1.tgz", + "integrity": "sha512-LNQS2XeEy1fdCOn/n3Aeh7RWPVbwKL0tH4Q1c2Y/M1TN6Jo3uN6C3cTvtjzJEms7ul0rmCAPoGqM4IsVCOIxnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^2.0.18", + "chalk": "^2.3.2", + "indent-string": "^3.2.0", + "jest-diff": "^22.0.3", + "lodash": "^4.17.21", + "log-symbols": "^2.2.0", + "parse-json": "^5.2.0", + "plur": "^2.1.2", + "pretty-format": "^22.0.3" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=4.0.0" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-i18n-json/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/pretty-format": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", + "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + }, + "node_modules/eslint-plugin-i18n-json/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/eslint-plugin-unused-imports": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", @@ -11146,13 +11360,13 @@ } }, "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/inflight": { @@ -11208,6 +11422,16 @@ "node": ">= 10" } }, + "node_modules/irregular-plurals": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", + "integrity": "sha512-kniTIJmaZYiwa17eTtWIfm0K342seyugl6vuC8DiiyiRAJWAVlLkqGCI0Im0neo0TkXw+pRcKaBPRdcKHnQJ6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -11894,19 +12118,122 @@ } }, "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-22.4.3.tgz", + "integrity": "sha512-/QqGvCDP5oZOF6PebDuLwrB2BMD8ffJv6TAGAdEVuDx1+uEgrHpSFrfrOiMRx2eJ1hgNjlQrOQEHetVwij90KA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "chalk": "^2.0.1", + "diff": "^3.2.0", + "jest-get-type": "^22.4.3", + "pretty-format": "^22.4.3" + } + }, + "node_modules/jest-diff/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=4" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-diff/node_modules/jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz", + "integrity": "sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/jest-docblock": { @@ -12051,6 +12378,22 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-matcher-utils/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -12328,6 +12671,22 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -14987,6 +15346,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/plur": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", + "integrity": "sha512-WhcHk576xg9y/iv6RWOuroZgsqvCbJN+XGvAypCJwLAYs2iWDp5LUmvaCdV6JR2O0SMBf8l6p7A94AyLCFVMlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "irregular-plurals": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss": { "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3e183315ea..fb2e149a98 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,6 +53,8 @@ "@angular/compiler-cli": "^18.2.8", "@cypress/schematic": "^2.5.2", "@cypress/skip-test": "^2.6.1", + "eslint-plugin-i18n-json": "^4.0.0", + "@angular-eslint/eslint-plugin-template": "^19.0.2", "@stylistic/eslint-plugin": "^2.11.0", "@typescript-eslint/eslint-plugin": "^8.15.0", "@eslint/js": "^9.15.0", diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts index da41e72992..59730f25ce 100644 --- a/frontend/src/app/app.component.spec.ts +++ b/frontend/src/app/app.component.spec.ts @@ -3,51 +3,32 @@ import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { TranslateTestingModule } from 'ngx-translate-testing'; -import { AuthConfig, OAuthModule, OAuthService } from 'angular-oauth2-oidc'; +import { OAuthModule, OAuthService } from 'angular-oauth2-oidc'; import { HttpClientTestingModule } from '@angular/common/http/testing'; // @ts-ignore import * as de from '../assets/i18n/de.json'; -import { HarnessLoader } from '@angular/cdk/testing'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSidenavModule } from '@angular/material/sidenav'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { NavigationEnd, Routes } from '@angular/router'; -import { of } from 'rxjs'; +import { Routes } from '@angular/router'; import { OverviewComponent } from './components/overview/overview.component'; import { ObjectiveDetailComponent } from './components/objective-detail/objective-detail.component'; import { CommonModule } from '@angular/common'; -const oAuthServiceMock = { - configure(environment: AuthConfig): void {}, - initCodeFlow(): void {}, - setupAutomaticSilentRefresh(): void {}, - hasValidAccessToken(): boolean { - return true; - }, - loadDiscoveryDocumentAndTryLogin(): Promise { - this.initCodeFlow(); - return Promise.resolve(); - } -}; - -const routerMock = { - root: jest.fn(), - // Router - events: of(new NavigationEnd(0, 'http://localhost:4200/objective/2', 'http://localhost:4200/objective/2')) -}; +const oAuthServiceMock = {}; const routes: Routes = [{ path: '', component: OverviewComponent, - children: [{ path: 'objective/:id', + children: [{ + path: 'objective/:id', component: ObjectiveDetailComponent, - pathMatch: 'full' }] + pathMatch: 'full' + }] }]; describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture; - let loader: HarnessLoader; beforeEach(async() => { await TestBed.configureTestingModule({ @@ -62,8 +43,10 @@ describe('AppComponent', () => { NoopAnimationsModule, CommonModule ], - providers: [{ provide: OAuthService, - useValue: oAuthServiceMock }], + providers: [{ + provide: OAuthService, + useValue: oAuthServiceMock + }], declarations: [AppComponent, OverviewComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA] @@ -74,7 +57,6 @@ describe('AppComponent', () => { component = fixture.componentInstance; fixture.detectChanges(); - loader = TestbedHarnessEnvironment.loader(fixture); fixture.detectChanges(); }); }); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 8b2861a876..33a11643d9 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -13,9 +13,7 @@ export class AppComponent { constructor(private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer) { - this.matIconRegistry.addSvgIcon('pz-search', - this.domSanitizer.bypassSecurityTrustResourceUrl(this.PATH_PREFIX + 'search-icon.svg')); - this.matIconRegistry.addSvgIcon('pz-menu-icon', - this.domSanitizer.bypassSecurityTrustResourceUrl(this.PATH_PREFIX + 'three-dot-menu-icon.svg')); + this.matIconRegistry.addSvgIcon('pz-search', this.domSanitizer.bypassSecurityTrustResourceUrl(this.PATH_PREFIX + 'search-icon.svg')); + this.matIconRegistry.addSvgIcon('pz-menu-icon', this.domSanitizer.bypassSecurityTrustResourceUrl(this.PATH_PREFIX + 'three-dot-menu-icon.svg')); } } diff --git a/frontend/src/app/components/action-plan/action-plan.component.spec.ts b/frontend/src/app/components/action-plan/action-plan.component.spec.ts index 943111a5fb..a898fe8db3 100644 --- a/frontend/src/app/components/action-plan/action-plan.component.spec.ts +++ b/frontend/src/app/components/action-plan/action-plan.component.spec.ts @@ -116,7 +116,7 @@ describe('ActionPlanComponent', () => { action3]); component.handleKeyDown(keyEvent, 2); - expect((component.activeItem = 1)); + expect(component.activeItem = 1); expect(component.control.getValue()!.toString()) .toBe([action1, action3, @@ -136,7 +136,7 @@ describe('ActionPlanComponent', () => { ]); component.handleKeyDown(keyEvent, 2); - expect((component.activeItem = 3)); + expect(component.activeItem = 3); expect(component.control.getValue()!.toString()) .toBe([ action1, diff --git a/frontend/src/app/components/action-plan/action-plan.component.ts b/frontend/src/app/components/action-plan/action-plan.component.ts index 623055e1d8..674ddcf58d 100644 --- a/frontend/src/app/components/action-plan/action-plan.component.ts +++ b/frontend/src/app/components/action-plan/action-plan.component.ts @@ -22,7 +22,8 @@ export class ActionPlanComponent { listItems!: QueryList; constructor(private actionService: ActionService, - public dialogService: DialogService) {} + public dialogService: DialogService) { + } handleKeyDown(event: Event, currentIndex: number) { let newIndex = currentIndex; @@ -44,18 +45,18 @@ export class ActionPlanComponent { const currentActionPlan: Action[] = this.control.getValue()!; this.updateActionTexts(currentActionPlan); moveItemInArray(currentActionPlan, currentIndex, newIndex); - currentActionPlan.forEach((action: Action, index: number) => (action.priority = index)); + currentActionPlan.forEach((action: Action, index: number) => action.priority = index); this.control.next(currentActionPlan); } updateActionTexts(currentActionPlan: Action[]) { const texts = Array.from(this.listItems) .map((input: any) => input.nativeElement.value); - currentActionPlan.forEach((action: Action, index: number) => (action.action = texts[index])); + currentActionPlan.forEach((action: Action, index: number) => action.action = texts[index]); } increaseActiveItemWithTab() { - if (this.activeItem <= this.control.value!.length - 2) { + if (this.activeItem <= this.control.getValue()!.length - 2) { this.activeItem++; } } @@ -70,8 +71,10 @@ export class ActionPlanComponent { const value: string = (event.container.element.nativeElement.children[event.previousIndex].children[1] as HTMLInputElement).value; const actions: Action[] = this.control.getValue()!; if (actions[event.previousIndex].action == '' && value != '') { - actions[event.previousIndex] = { ...actions[event.previousIndex], - action: value }; + actions[event.previousIndex] = { + ...actions[event.previousIndex], + action: value + }; this.control.next(actions); } if (event.previousContainer === event.container) { @@ -122,9 +125,11 @@ export class ActionPlanComponent { addNewAction() { const actions: Action[] = this.control.getValue()!; - actions.push({ action: '', + actions.push({ + action: '', priority: actions.length, - keyResultId: this.keyResultId } as Action); + keyResultId: this.keyResultId + } as Action); this.control.next(actions); this.activeItem = actions.length - 1; } diff --git a/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.spec.ts b/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.spec.ts index 4ef1d3d017..c4a93de78e 100644 --- a/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.spec.ts +++ b/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.spec.ts @@ -12,7 +12,7 @@ describe('MetricCheckInDirective', () => { 200], ['200HelloWorld', 200], - ["200'000", + ['200\'000', 200000], ['1050&%ç*', 1050], diff --git a/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.ts b/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.ts index 98f4638545..55375f93c5 100644 --- a/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.ts +++ b/frontend/src/app/components/checkin/check-in-form-metric/metric-check-in-directive.ts @@ -1,4 +1,4 @@ -import { Directive, HostListener, forwardRef } from '@angular/core'; +import { Directive, forwardRef, HostListener } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Directive({ @@ -10,20 +10,21 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; }] }) export class MetricCheckInDirective implements ControlValueAccessor { - private onChange: (value: number | null) => void = () => {}; + private onChange: (value: number | null) => void = () => { + }; protected readonly CHAR_REGEX = /[^0-9.]/g; - writeValue(value: any): void { - // does not need to be implemented because the display value does not need to be modified + writeValue(): void { + // does not need to be implemented because the display value does not need to be modified comment is here to make linter happy } registerOnChange(fn: (value: number | null) => void): void { this.onChange = fn; } - registerOnTouched(fn: () => void): void { - // does not need to be implemented + registerOnTouched(): void { + // does not need to be implemented comment is here to make linter happy } @HostListener('input', ['$event.target.value']) diff --git a/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html b/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html index a15793ca9b..99eec7cbc4 100644 --- a/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html +++ b/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html @@ -12,7 +12,8 @@ class="ordinal-zone" >
- Fail:  Commit / Target / Stretch noch nicht erreicht + Fail:  + Commit / Target / Stretch noch nicht erreicht
@@ -22,7 +23,10 @@ class="ordinal-zone" >
- Commit: {{ keyResult.commitZone }} + + Commit:  + {{ keyResult.commitZone }} +
@@ -32,7 +36,10 @@ class="ordinal-zone" >
- Target: {{ keyResult.targetZone }} + + Target:  + {{ keyResult.targetZone }} +
@@ -42,7 +49,10 @@ class="ordinal-zone" >
- Stretch: {{ keyResult.stretchZone }} + + Stretch:  + {{ keyResult.stretchZone }} +
diff --git a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts index 3298e393e1..6711b0ba98 100644 --- a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts +++ b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts @@ -72,6 +72,7 @@ export class CheckInFormComponent implements OnInit { this.dialogForm.controls.initiatives.setValue(this.checkIn.initiatives); return; } + /* If KeyResult has lastCheckIn set checkIn to this value */ if ((this.keyResult as KeyResultMetric | KeyResultOrdinal).lastCheckIn != null) { this.checkIn = { @@ -81,6 +82,7 @@ export class CheckInFormComponent implements OnInit { this.dialogForm.controls.confidence.setValue(this.checkIn.confidence); return; } + /* If Check-in is null set as object with confidence 5 default value */ this.checkIn = { confidence: 5 } as CheckInMin; } @@ -136,8 +138,10 @@ export class CheckInFormComponent implements OnInit { changeIsChecked(event: any, index: number) { const actions = this.dialogForm.value.actionList!; - actions[index] = { ...actions[index], - isChecked: event.checked }; + actions[index] = { + ...actions[index], + isChecked: event.checked + }; this.dialogForm.patchValue({ actionList: actions }); } diff --git a/frontend/src/app/components/confidence/confidence.component.spec.ts b/frontend/src/app/components/confidence/confidence.component.spec.ts index 6c6e4b623f..a628972571 100644 --- a/frontend/src/app/components/confidence/confidence.component.spec.ts +++ b/frontend/src/app/components/confidence/confidence.component.spec.ts @@ -1,8 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ConfidenceComponent } from './confidence.component'; -import { HarnessLoader } from '@angular/cdk/testing'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { checkInMetric } from '../../shared/testData'; import { MatSliderModule } from '@angular/material/slider'; import { CheckInMin } from '../../shared/types/model/CheckInMin'; @@ -13,7 +11,6 @@ import { By } from '@angular/platform-browser'; describe('ConfidenceComponent', () => { let component: ConfidenceComponent; let fixture: ComponentFixture; - let loader: HarnessLoader; beforeEach(() => { TestBed.configureTestingModule({ @@ -25,7 +22,6 @@ describe('ConfidenceComponent', () => { fixture = TestBed.createComponent(ConfidenceComponent); component = fixture.componentInstance; - loader = TestbedHarnessEnvironment.loader(fixture); component.checkIn = checkInMetric; component.isEdit = true; }); @@ -45,7 +41,7 @@ describe('ConfidenceComponent', () => { }); fixture.detectChanges(); await fixture.whenStable(); - const textField = fixture.debugElement.query(By.css("[data-testId='confidence']")); + const textField = fixture.debugElement.query(By.css('[data-testId=\'confidence\']')); const expectedLabel = expected + '/' + component.max; const sliderInputField = fixture.debugElement.query(By.css('mat-slider > input ')); diff --git a/frontend/src/app/components/key-result-detail/key-result-detail.component.html b/frontend/src/app/components/key-result-detail/key-result-detail.component.html index 6be9fbb3ef..fee2d36990 100644 --- a/frontend/src/app/components/key-result-detail/key-result-detail.component.html +++ b/frontend/src/app/components/key-result-detail/key-result-detail.component.html @@ -52,7 +52,7 @@

Confidence

- +

Baseline: {{ keyResultMetric.baseline | unitTransformation: keyResultMetric.unit }} @@ -76,7 +76,7 @@

Confidence

- +

Commit

@@ -106,7 +106,7 @@

Beschrieb

{{ keyResult.description || "-" }}

-
+

Action Plan

@@ -120,7 +120,10 @@

Action Plan

-
- {{ action.action }}
+
+ - + {{ action.action }} +
diff --git a/frontend/src/app/components/key-result-form/key-result-form.component.ts b/frontend/src/app/components/key-result-form/key-result-form.component.ts index 7fd70a3585..6f88e55fef 100644 --- a/frontend/src/app/components/key-result-form/key-result-form.component.ts +++ b/frontend/src/app/components/key-result-form/key-result-form.component.ts @@ -40,9 +40,7 @@ export class KeyResultFormComponent implements OnInit, OnDestroy { ngOnInit(): void { this.users$ = this.userService.getUsers(); - this.filteredUsers$ = this.keyResultForm.get('owner')?.valueChanges.pipe(startWith(''), - filter((value) => typeof value === 'string'), - switchMap((value) => this.filter(value as string))); + this.filteredUsers$ = this.keyResultForm.get('owner')?.valueChanges.pipe(startWith(''), filter((value) => typeof value === 'string'), switchMap((value) => this.filter(value as string))); if (this.keyResult) { this.keyResultForm.patchValue({ actionList: this.keyResult.actionList }); this.keyResultForm.controls['title'].setValue(this.keyResult.title); @@ -145,7 +143,9 @@ export class KeyResultFormComponent implements OnInit, OnDestroy { return this.keyResult ? this.keyResult.id : null; } - updateFormValidity() {} + updateFormValidity() { + // Implemented because of interface this comment is to satisfy the linter + } getFullNameOfLoggedInUser() { return this.getFullNameOfUser(this.userService.getCurrentUser()); diff --git a/frontend/src/app/components/key-result-type/key-result-type.component.ts b/frontend/src/app/components/key-result-type/key-result-type.component.ts index 3c143d8a71..6015b9b135 100644 --- a/frontend/src/app/components/key-result-type/key-result-type.component.ts +++ b/frontend/src/app/components/key-result-type/key-result-type.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { KeyResult } from '../../shared/types/model/KeyResult'; import { FormGroup, Validators } from '@angular/forms'; import { KeyResultMetric } from '../../shared/types/model/KeyResultMetric'; @@ -17,7 +17,6 @@ export class KeyResultTypeComponent implements OnInit { @Input() keyResult!: KeyResult | null; - @Output() formValidityEmitter = new EventEmitter(); isMetric = true; @@ -29,7 +28,8 @@ export class KeyResultTypeComponent implements OnInit { protected readonly hasFormFieldErrors = hasFormFieldErrors; - constructor(private translate: TranslateService) {} + constructor(private translate: TranslateService) { + } ngOnInit(): void { if (this.keyResult) { @@ -65,7 +65,6 @@ export class KeyResultTypeComponent implements OnInit { async updateFormValidity() { await new Promise((r) => setTimeout(r, 100)); - this.formValidityEmitter.emit(this.keyResultForm.invalid); } setValidatorsMetric() { @@ -98,7 +97,7 @@ export class KeyResultTypeComponent implements OnInit { } switchKeyResultType(type: string) { - if (((type == 'metric' && !this.isMetric) || (type == 'ordinal' && this.isMetric)) && this.typeChangeAllowed) { + if ((type == 'metric' && !this.isMetric || type == 'ordinal' && this.isMetric) && this.typeChangeAllowed) { this.isMetric = !this.isMetric; const keyResultType = this.isMetric ? 'metric' : 'ordinal'; this.keyResultForm.controls['keyResultType'].setValue(keyResultType); diff --git a/frontend/src/app/components/objective-detail/objective-detail.component.html b/frontend/src/app/components/objective-detail/objective-detail.component.html index 9e9b5094c8..55ac578e19 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.html +++ b/frontend/src/app/components/objective-detail/objective-detail.component.html @@ -19,7 +19,9 @@

{{ objective.title }}

Beschrieb

-

-

+
+

-

+
Beschrieb

{{ objective.description }}

-
+
+ >Abbrechen +
-
+