Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions docs/features/project-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

The Project Brief feature displays team project information to users on the home page. When a user's team has a project brief configured, a "Project Brief" button appears next to the experience name. Clicking this button opens a modal that displays structured project details.
The Project Brief feature displays team project information to users on the home page. When a user's team has a project brief configured, a "Project Brief" button appears next to the experience name. Clicking this button opens a modal that displays structured project details. When the Project Hub feature is enabled, a visible "Go to Project Hub" button opens the external Project Hub application.

## Data Flow

Expand All @@ -17,7 +17,7 @@ HomePage.updateDashboard()
↓ reads from storage
this.projectBrief = this.storageService.getUser().projectBrief
Template: *ngIf="projectBrief" shows button
Template: *ngIf="projectBrief" shows Project Brief button; *ngIf="showProjectHub" shows Project Hub button
showProjectBrief() → opens ProjectBriefModalComponent
```
Expand All @@ -34,10 +34,11 @@ showProjectBrief() → opens ProjectBriefModalComponent
- `project-brief-modal.component.scss` - Component-specific styles
- `project-brief-modal.component.spec.ts` - Unit tests

**Input:**
**Modal property:**
```typescript
@Input() projectBrief: ProjectBrief = {};
projectBrief: ProjectBrief = {};
```
Ionic sets this public property through `componentProps` when the modal is created.

**Interface:**
```typescript
Expand All @@ -62,6 +63,10 @@ interface ProjectBrief {
- Professional Skills (as chips)
- Deliverables

**Color treatment:**
- Modal section header icons and chips use the primary brand color.
- Do not use the secondary brand color for Technical Skills accents; customer secondary colors can be too light to remain visible on the light modal background.

**Empty Field Handling:**
- All sections show "None specified" when the field is empty or undefined
- Uses `hasValue()` for string fields and `hasItems()` for array fields
Expand Down Expand Up @@ -124,17 +129,28 @@ Button placement - next to experience name:
```html
<div class="exp-header">
<h2 class="headline-2" [innerHTML]="experience.name"></h2>
<ion-button *ngIf="projectBrief"
fill="clear"
size="small"
class="project-brief-btn"
(click)="showProjectBrief()"
(keydown.enter)="showProjectBrief()"
(keydown.space)="showProjectBrief(); $event.preventDefault()"
aria-label="View project brief" i18n-aria-label>
<ion-icon name="document-text-outline" slot="start" aria-hidden="true"></ion-icon>
<span i18n>Project Brief</span>
</ion-button>
<div class="button-group-no-gap" *ngIf="!isExpert && (projectBrief || showProjectHub)">
<ion-button *ngIf="projectBrief?.id"
fill="clear"
size="small"
class="project-brief-btn"
(click)="showProjectBrief()"
(keydown.enter)="showProjectBrief()"
(keydown.space)="showProjectBrief(); $event.preventDefault()"
aria-label="View project brief" i18n-aria-label>
<ion-icon name="document-text-outline" slot="start" aria-hidden="true"></ion-icon>
<span i18n>Project Brief</span>
</ion-button>
<ion-button *ngIf="showProjectHub"
fill="clear"
size="small"
class="project-brief-btn"
title="Go to Project Hub"
aria-label="Go to Project Hub" i18n-aria-label>
<ion-icon name="open-outline" slot="start" size="small" aria-hidden="true"></ion-icon>
<span i18n>Go to Project Hub</span>
</ion-button>
</div>
</div>
```

Expand Down Expand Up @@ -177,11 +193,11 @@ Button placement - next to experience name:

## Accessibility

- Button includes `aria-label="View project brief"` with i18n support
- Buttons include descriptive aria labels with i18n support
- Keyboard navigation with `(keydown.enter)` and `(keydown.space)` handlers
- Modal has proper semantic structure with `<main>`, `<section>`, and heading hierarchy
- Close button includes `aria-label="Close project brief"`
- Ion-chips for industry/skills are visually distinct with color coding
- Ion-chips for industry/skills use the primary brand color so labels and outlines remain visible when secondary branding is faint

## Sample Data

Expand Down Expand Up @@ -219,6 +235,7 @@ After parsing:
- Template renders title when provided
- Template shows "None specified" for empty fields
- Template renders chips for industry and skills
- Template keeps section accents on the primary brand color

**HomePage tests (additions needed):**
- Button visible when `projectBrief` is set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ <h1 id="project-brief-title" class="headline-3">{{ projectBrief.title }}</h1>

<ion-accordion value="technical-skills">
<ion-item slot="header" color="light">
<ion-icon name="code-slash-outline" slot="start" color="secondary" aria-hidden="true"></ion-icon>
<ion-icon name="code-slash-outline" slot="start" color="primary" aria-hidden="true"></ion-icon>
<ion-label class="accordion-header" i18n>Technical Skills</ion-label>
</ion-item>
<div slot="content" class="accordion-content">
<div class="chip-container" *ngIf="hasItems(projectBrief?.technicalSkills); else noTechnicalSkills">
<ion-chip *ngFor="let skill of projectBrief.technicalSkills"
color="secondary"
color="primary"
outline="true">
<ion-label>{{ skill }}</ion-label>
</ion-chip>
Expand All @@ -78,13 +78,13 @@ <h1 id="project-brief-title" class="headline-3">{{ projectBrief.title }}</h1>

<ion-accordion value="professional-skills">
<ion-item slot="header" color="light">
<ion-icon name="people-outline" slot="start" color="tertiary" aria-hidden="true"></ion-icon>
<ion-icon name="people-outline" slot="start" color="primary" aria-hidden="true"></ion-icon>
<ion-label class="accordion-header" i18n>Professional Skills</ion-label>
</ion-item>
<div slot="content" class="accordion-content">
<div class="chip-container" *ngIf="hasItems(projectBrief?.professionalSkills); else noProfessionalSkills">
<ion-chip *ngFor="let skill of projectBrief.professionalSkills"
color="tertiary"
color="primary"
outline="true">
<ion-label>{{ skill }}</ion-label>
</ion-chip>
Expand All @@ -97,7 +97,7 @@ <h1 id="project-brief-title" class="headline-3">{{ projectBrief.title }}</h1>

<ion-accordion value="deliverables">
<ion-item slot="header" color="light">
<ion-icon name="checkbox-outline" slot="start" color="success" aria-hidden="true"></ion-icon>
<ion-icon name="checkbox-outline" slot="start" color="primary" aria-hidden="true"></ion-icon>
<ion-label class="accordion-header" i18n>Deliverables</ion-label>
</ion-item>
<div slot="content" class="accordion-content">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,30 @@ describe('ProjectBriefModalComponent', () => {
const chips = fixture.nativeElement.querySelectorAll('ion-chip');
expect(chips.length).toBe(4);
});

it('should use the primary brand color for section accents', () => {
component.projectBrief = {
industry: ['Health'],
technicalSkills: ['Python'],
professionalSkills: ['Leadership'],
deliverables: 'Prototype'
};
fixture.detectChanges();

const accentSelectors = [
'ion-icon[name="document-text-outline"]',
'ion-icon[name="business-outline"]',
'ion-icon[name="code-slash-outline"]',
'ion-icon[name="people-outline"]',
'ion-icon[name="checkbox-outline"]',
'ion-chip'
];

accentSelectors.forEach((selector) => {
fixture.nativeElement.querySelectorAll(selector).forEach((element: Element) => {
expect(element.getAttribute('color')).toBe('primary');
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h2 id="experiences-heading" class="for-accessibility" i18n>Experiences</h2>
}

<div class="exp-grid" role="list" aria-label="Available experiences" i18n-aria-label
[class.exp-grid--center]="experiences.length === 1">
[class.exp-grid--center]="isCompactLayout(experiences.length)">
@for (experience of experiences; track experience) {
<div class="exp-card-wrap" role="listitem">
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ describe('ExperiencesPage', () => {
});
});

describe('layout helpers', () => {
it('should mark compact layout only for one or two experiences', () => {
expect(component.isCompactLayout(1)).toBeTrue();
expect(component.isCompactLayout(2)).toBeTrue();
expect(component.isCompactLayout(3)).toBeFalse();
});
});

describe('switchProgram()', () => {
let presentLoading: any;
let dismissLoading: any;
Expand Down
5 changes: 5 additions & 0 deletions projects/v3/src/app/pages/experiences/experiences.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export class ExperiencesPage implements OnInit, OnDestroy {
return this.experienceService.getProgresses([projectId]).toPromise();
}

// display experience tile in compact UI
isCompactLayout(experienceCount: number): boolean {
return experienceCount <= 2;
}

get instituteLogo() {
return this.storage.getConfig().logo || this.storage.getUser()?.institutionLogo;
}
Expand Down
13 changes: 7 additions & 6 deletions projects/v3/src/app/pages/home/home.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,19 @@
</ion-button>
<ion-button fill="clear"
size="small"
class="project-brief-btn"
*ngIf="showProjectHub"
(click)="openProjectBriefExternal()"
(keydown.enter)="openProjectBriefExternal()"
(keydown.space)="openProjectBriefExternal(); $event.preventDefault()"
title="Go to project hub"
aria-label="Go to project hub" i18n-aria-label>
title="Go to Project Hub"
aria-label="Go to Project Hub" i18n-aria-label>
<ion-icon name="open-outline"
[slot]="projectBrief ? 'icon-only' : 'start'"
size="small"
aria-hidden="true"
slot="start"
size="small"
aria-hidden="true"
></ion-icon>
<span i18n *ngIf="!projectBrief">Project-Hub</span>
<span i18n>Go to Project Hub</span>
</ion-button>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion projects/v3/src/app/pages/home/home.page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ ion-content.scrollable-desktop {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
flex-shrink: 0;
}
}

Expand Down
19 changes: 19 additions & 0 deletions projects/v3/src/app/pages/home/home.page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ describe('HomePage', () => {
expect(component).toBeTruthy();
});

it('should show a visible Project Hub label when project brief and project hub are available', () => {
component.experience = { id: 1, name: 'Test Experience', cardUrl: 'test-card-url' } as any;
component.isExpert = false;
component.projectBrief = { id: 'brief-1', title: 'Project Brief' };
component.showProjectHub = true;

fixture.detectChanges();

const buttons = Array.from(
fixture.nativeElement.querySelectorAll('ion-button')
) as HTMLElement[];
const projectHubButton = buttons.find(button =>
button.getAttribute('aria-label') === 'Go to Project Hub'
);

expect(projectHubButton).toBeTruthy();
expect(projectHubButton?.textContent).toContain('Go to Project Hub');
});

describe('updateDashboard', () => {
beforeEach(() => {
sharedService.refreshJWT.and.returnValue(Promise.resolve());
Expand Down
Loading