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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export class MyComponent extends AsyncHandler {
- Supports multiple locales (see `app.locales` in settings)
- Service worker support for PWA functionality (production builds)
- Custom localization support via `public/assets/locale`, **TranslatePipe** and **LocaleService**. `en-AU.json` is the fallback locale
- Do not add Angular Material. Use the controls in `src/app/ui/controls` (form field, select, autocomplete, chip list, tabs, tooltip, spinner, progress bar, dialog close, toasts) and the Angular CDK (dialog, menu, overlay). Their styles are in `src/styles.css`
- Form errors: put `<div error>` in an `app-form-field`. It shows when its `[formField]` or `ngModel` control is touched and invalid
- Add a `ripple` attribute to an element for a press ripple. Use native `<input type="checkbox">` for checkboxes, with `role="switch"` for a switch
- Use **IconComponent** for icons. The default icon font is a subset of the icons in the source. After you add icons, run `bun config/icon-subset.ts` (needs `pip install fonttools brotli`). An icon that is not in the subset still shows, but it loads the full 420 KB font
- `src/index.html` has a splash that shows until the app loads. Lighthouse measures its title as the largest contentful paint, so keep the title larger than the content of the first page
- Use `feat/*`, `fix/*`, `perf/*`, or `refactor/*` for branch names
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The `Demo Site` workflow moves `backoffice-demo` to `develop` once per day (17:0

## Stack

- Angular 22 and Angular Material
- Angular 22 with the Angular CDK and the app's own controls (`src/app/ui/controls`)
- Nx 22 with the Angular application builder
- Vitest and Playwright
- Tailwind CSS 4
Expand Down
3 changes: 0 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions e2e/src/domains.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ test.describe('Domains', () => {
await domains.saveButton.click();

await expect(domains.dialog).toBeVisible();
await expect(domains.dialog.locator('mat-error')).toHaveCount(2);
await expect(
domains.dialog.locator('app-form-field [error]:visible'),
).toHaveCount(2);
});

test('AC-DOM-003-4: saves the domain and opens it', async () => {
Expand Down Expand Up @@ -193,7 +195,7 @@ test.describe('Domains', () => {

// The list is empty while it loads, so wait for the load to end
await expect(
domains.tabContent.locator('mat-progress-bar'),
domains.tabContent.locator('app-progress-bar'),
).toBeHidden();
await expect(domains.tabContent).toContainText(
'No users associated with this domain',
Expand Down
2 changes: 1 addition & 1 deletion e2e/src/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ test.describe('Navigation', () => {
await expect(zones.itemName).toHaveText('Level 1');
await expect(
page.locator('item-tablist a[href*="/systems"]'),
).toHaveAttribute('aria-selected', 'true');
).toHaveAttribute('aria-current', 'page');
});

test('browser back returns to the previous section', async ({ page }) => {
Expand Down
8 changes: 4 additions & 4 deletions e2e/src/pages/admin.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ export class AdminPage extends BasePage {
*/
async selectApiKeyDomain(domain = 'PlaceOS Global'): Promise<void> {
await this.page
.locator('admin-api-keys admin-domain-select mat-select')
.locator('admin-api-keys admin-domain-select app-select')
.click();
await this.page
.locator('mat-option')
.locator('app-option')
.filter({ hasText: domain })
.first()
.click();
Expand All @@ -78,7 +78,7 @@ export class AdminPage extends BasePage {
/** User option in the API key user autocomplete panel */
apiKeyUserOption(name: string): Locator {
return this.page
.locator('mat-option')
.locator('app-option')
.filter({ hasText: name })
.first();
}
Expand All @@ -100,7 +100,7 @@ export class AdminPage extends BasePage {
const label = permission.charAt(0).toUpperCase() + permission.slice(1);
await this.apiKeyPermissionsSelect.click();
await this.page
.locator('mat-option')
.locator('app-option')
.filter({ hasText: label })
.first()
.click();
Expand Down
18 changes: 8 additions & 10 deletions e2e/src/pages/base.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export abstract class BasePage {

/** Spinner of the metadata tab */
get metadataLoadingSpinner(): Locator {
return this.page.locator('metadata-display mat-spinner').first();
return this.page.locator('metadata-display app-spinner').first();
}

/**
Expand All @@ -127,20 +127,20 @@ export abstract class BasePage {
.locator('item-details button:has(icon:has-text("more_vert"))')
.first()
.click();
await expect(this.page.locator('.mat-mdc-menu-panel')).toBeVisible();
await expect(this.page.locator('.cdk-menu')).toBeVisible();
}

/** Edit entry of the action menu */
get editButton(): Locator {
return this.page
.locator('button[mat-menu-item]:has(icon:has-text("edit"))')
.locator('button[role="menuitem"]:has(icon:has-text("edit"))')
.first();
}

/** Delete entry of the action menu */
get deleteButton(): Locator {
return this.page
.locator('button[mat-menu-item]:has(icon:has-text("delete"))')
.locator('button[role="menuitem"]:has(icon:has-text("delete"))')
.first();
}

Expand All @@ -149,7 +149,7 @@ export abstract class BasePage {
/** The open modal */
get dialog(): Locator {
return this.page
.locator('fullscreen-modal-shell, mat-dialog-container')
.locator('fullscreen-modal-shell, cdk-dialog-container')
.first();
}

Expand All @@ -176,7 +176,7 @@ export abstract class BasePage {
this.page.getByRole('heading', { name: 'Discard changes?' }),
).toBeVisible();
await this.page.getByRole('button', { name: 'Discard' }).click();
await expect(this.page.locator('mat-dialog-container')).toHaveCount(0);
await expect(this.page.locator('cdk-dialog-container')).toHaveCount(0);
}

/**
Expand Down Expand Up @@ -262,11 +262,9 @@ export abstract class BasePage {

// Shared widgets

/** Snack bar notification with `text`. An old one can still be closing. */
/** Toast notification with `text`. An old one can still be closing. */
notification(text: string): Locator {
return this.page
.locator('mat-snack-bar-container')
.filter({ hasText: text });
return this.page.locator('.toast').filter({ hasText: text });
}

/**
Expand Down
4 changes: 2 additions & 2 deletions e2e/src/pages/systems.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class SystemsPage extends BasePage {
* Get the module selector used by execute method controls
*/
get executeModuleSelect(): Locator {
return this.page.locator('select-system-module mat-select').first();
return this.page.locator('select-system-module app-select').first();
}

/**
Expand All @@ -49,7 +49,7 @@ export class SystemsPage extends BasePage {
.first();
await zone_input.fill(zone_name);
await this.page
.locator('mat-option')
.locator('app-option')
.filter({ hasText: zone_name })
.first()
.click();
Expand Down
4 changes: 3 additions & 1 deletion e2e/src/repositories.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ test.describe('Repositories', () => {
await repositories.saveButton.click();

await expect(repositories.dialog).toBeVisible();
await expect(repositories.dialog.locator('mat-error')).toHaveText([
await expect(
repositories.dialog.locator('app-form-field [error]:visible'),
).toHaveText([
'A repository name is required',
'A folder name is required',
'A URI is required',
Expand Down
10 changes: 6 additions & 4 deletions e2e/src/systems.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ test.describe('Systems', () => {

await expect(systems.dialog).toBeVisible();
await expect(
systems.dialog.locator('mat-error').first(),
systems.dialog
.locator('app-form-field [error]:visible')
.first(),
).toBeVisible();
});

Expand Down Expand Up @@ -130,7 +132,7 @@ test.describe('Systems', () => {
name: 'Edit https://cam.example.com/old.jpg',
})
.click();
const edit_input = systems.dialog.locator('.mat-chip-edit-input');
const edit_input = systems.dialog.locator('.chip-edit');
await expect(edit_input).toBeFocused();
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.type('https://cam.example.com/new.jpg');
Expand All @@ -140,7 +142,7 @@ test.describe('Systems', () => {

await systems.openEditModal();
const chips = systems.dialog.locator(
'[aria-label="Camera Snapshot URL List"] mat-chip-row',
'[aria-label="Camera Snapshot URL List"] .chip',
);
await expect(chips).toHaveCount(1);
await expect(chips).toContainText(
Expand Down Expand Up @@ -211,7 +213,7 @@ test.describe('Systems', () => {
await systems.searchExecuteModules('Lighting');

const options = page
.locator('.cdk-overlay-pane mat-option')
.locator('.cdk-overlay-pane app-option')
.filter({ hasText: /\S/ });
await expect(
options.filter({ hasText: /Lighting/i }).first(),
Expand Down
4 changes: 3 additions & 1 deletion e2e/src/triggers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ test.describe('Triggers', () => {
await triggers.saveButton.click();

await expect(triggers.dialog).toBeVisible();
await expect(triggers.dialog.locator('mat-error')).toBeVisible();
await expect(
triggers.dialog.locator('app-form-field [error]:visible'),
).toBeVisible();
});

test('AC-TRG-003-5: saves the trigger and opens it', async () => {
Expand Down
4 changes: 3 additions & 1 deletion e2e/src/users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ test.describe('Users', () => {
await users.saveButton.click();

await expect(users.dialog).toBeVisible();
await expect(users.dialog.locator('mat-error')).toHaveCount(4);
await expect(
users.dialog.locator('app-form-field [error]:visible'),
).toHaveCount(4);
});

test('AC-USR-003-6: saves the user and opens them', async () => {
Expand Down
4 changes: 3 additions & 1 deletion e2e/src/zones.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ test.describe('Zones', () => {
await zones.saveButton.click();

await expect(zones.dialog).toBeVisible();
await expect(zones.dialog.locator('mat-error')).toBeVisible();
await expect(
zones.dialog.locator('app-form-field [error]:visible'),
).toBeVisible();
});

test('AC-ZON-003-5: saves the zone and opens it', async () => {
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"@angular/cli": "22.2.3",
"@angular/compiler-cli": "22.2.3",
"@angular/language-service": "22.2.3",
"@angular/material": "22.2.4",
"@eslint/js": "^9.8.0",
"@fontsource-variable/fira-code": "^5.3.0",
"@fontsource-variable/roboto": "^5.3.0",
Expand Down
5 changes: 3 additions & 2 deletions project.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@
"./node_modules/@fontsource-variable/roboto/wght-italic.css",
"./src/fonts/material-symbols.css",
"./node_modules/@fontsource-variable/fira-code/index.css",
"./src/styles.css",
"./src/material-theme.scss"
"./node_modules/@angular/cdk/overlay-prebuilt.css",
"./node_modules/@angular/cdk/a11y-prebuilt.css",
"./src/styles.css"
]
},
"configurations": {
Expand Down
27 changes: 17 additions & 10 deletions src/app/admin/admin-domain-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ import {
output,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { PlaceDomain } from '@placeos/ts-client';
import { FormFieldComponent } from '../ui/controls/form-field.component';
import { OptionComponent } from '../ui/controls/option.component';
import { SelectComponent } from '../ui/controls/select.component';
import { TranslatePipe } from '../ui/translate.pipe';
import { AdminDataService } from './admin-data.service';

/** Domain picker for admin pages. AdminDataService keeps the choice per `key`. */
@Component({
selector: 'admin-domain-select',
template: `
<mat-form-field appearance="outline" [class]="field_class()">
<mat-select
<app-form-field [class]="field_class()">
<app-select
name="domain"
[ngModel]="domain()"
(ngModelChange)="select($event)"
Expand All @@ -28,17 +29,23 @@ import { AdminDataService } from './admin-data.service';
"
>
@if (all()) {
<mat-option [value]="{}">
<app-option [value]="{}">
{{ 'ADMIN.ALL_DOMAINS' | translate }}
</mat-option>
</app-option>
}
@for (domain of domain_list(); track domain.id) {
<mat-option [value]="domain">{{ domain.name }}</mat-option>
<app-option [value]="domain">{{ domain.name }}</app-option>
}
</mat-select>
</mat-form-field>
</app-select>
</app-form-field>
`,
imports: [FormsModule, MatFormFieldModule, MatSelectModule, TranslatePipe],
imports: [
OptionComponent,
SelectComponent,
FormFieldComponent,
FormsModule,
TranslatePipe,
],
})
export class AdminDomainSelectComponent {
private _data = inject(AdminDataService);
Expand Down
6 changes: 2 additions & 4 deletions src/app/admin/admin.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, OnInit, computed, inject } from '@angular/core';

import { MatRippleModule } from '@angular/material/core';
import { RouterModule } from '@angular/router';
import { extensionsForItem } from '../common/api';
import { PlaceDebugService } from '../common/debug.service';
Expand Down Expand Up @@ -29,7 +28,7 @@ import { TranslatePipe } from '../ui/translate.pipe';
@for (item of tab_list(); track item.id) {
<a
btn
matRipple
ripple
class="clear hover:bg-base-200 w-auto min-w-full text-left"
[routerLink]="'/admin/' + item.id"
[queryParams]="item.query || {}"
Expand All @@ -54,7 +53,7 @@ import { TranslatePipe } from '../ui/translate.pipe';
</main>
<button
icon
matRipple
ripple
class="absolute top-2 left-4 z-40 mr-2 sm:hidden"
(click)="open_menu = true"
[attr.aria-label]="'COMMON.MENU' | translate"
Expand Down Expand Up @@ -85,7 +84,6 @@ import { TranslatePipe } from '../ui/translate.pipe';
TranslatePipe,
DebugOutputComponent,
IconComponent,
MatRippleModule,
SidebarMenuComponent,
RouterModule,
],
Expand Down
Loading
Loading