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
111 changes: 111 additions & 0 deletions e2e/src/admin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Page } from '@playwright/test';
import { expect, seedSession, test } from './fixtures/test-fixtures';
import { AdminPage } from './pages';

Expand Down Expand Up @@ -55,6 +56,25 @@ const API_KEYS = [
},
];

const TENANT = {
id: 'tenant-e2e',
name: 'E2E Tenant',
domain: 'place.tech',
platform: 'google',
delegated: true,
booking_limits: {},
booking_range: {},
booking_period: { desk: 240, parking: 1470, locker: 45 },
early_checkin: 3600,
};

/** Tenant as saved by the mock staff API */
async function storedTenant(page: Page): Promise<typeof TENANT> {
return page.evaluate(
() => JSON.parse(sessionStorage.getItem('PLACEOS.mocks.tenants'))[0],
);
}

test.describe('Admin', () => {
let admin: AdminPage;

Expand Down Expand Up @@ -174,6 +194,97 @@ test.describe('Admin', () => {
const row = await admin.tableRow('Default Tenant', admin.section);
await expect(row.nth(1)).toHaveText(/google/);
});

test.describe('Maximum booking length', () => {
test.beforeEach(async ({ page }) => {
await seedSession(page, 'tenants', [TENANT]);
await admin.gotoSection('staff-api');
});

test('AC-ADM-009-2: shows lengths without empty units', async () => {
await expect(
await admin.tenantBookingPeriods('E2E Tenant'),
).toHaveText([
'desk: 4 hours',
'parking: 1 day 30 minutes',
'locker: 45 minutes',
]);
});

test('AC-ADM-009-3: edits lengths as days, hours and minutes', async ({
page,
}) => {
await admin.editTenant('E2E Tenant');

const rows = admin.bookingPeriodRows;
await expect(rows).toHaveCount(3);
await expect(
rows.nth(1).getByRole('spinbutton', { name: 'Days' }),
).toHaveValue('1');
await expect(
rows.nth(1).getByRole('combobox', { name: 'Hours' }),
).toContainText('0 hours');
await expect(
rows.nth(1).getByRole('combobox', { name: 'Minutes' }),
).toContainText('30 minutes');

// desk: 4 hours => 4 hours 30 minutes
await admin.setBookingPeriod(0, { minutes: 30 });
await expect(
admin.dialog.locator('[period-total]').first(),
).toHaveText('4 hours 30 minutes');

// Remove locker and add a two day room booking length
await rows
.nth(2)
.getByRole('button', { name: 'Delete' })
.click();
await admin.dialog
.locator('[booking-period]')
.getByRole('button', { name: 'New item' })
.click();
await admin.setBookingPeriod(2, {
type: 'room',
days: 2,
hours: 0,
});

await admin.saveButton.click();
await expect(admin.dialog).toBeHidden();

expect((await storedTenant(page)).booking_period).toEqual({
desk: 270,
parking: 1470,
room: 2880,
});
await expect(
await admin.tenantBookingPeriods('E2E Tenant'),
).toHaveText([
'desk: 4 hours 30 minutes',
'parking: 1 day 30 minutes',
'room: 2 days',
]);
});

test('AC-ADM-009-4: does not save a zero length', async ({
page,
}) => {
await admin.editTenant('E2E Tenant');

await admin.setBookingPeriod(2, { minutes: 0 });
await expect(
admin.dialog.getByText(
'Enter a unique booking type and a length greater than 0 for each row.',
),
).toBeVisible();

await admin.saveButton.click();
await expect(admin.dialog).toBeVisible();
expect((await storedTenant(page)).booking_period).toEqual(
TENANT.booking_period,
);
});
});
});

test.describe('US-ADM-011: Manage API Keys', () => {
Expand Down
55 changes: 55 additions & 0 deletions e2e/src/pages/admin.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,59 @@ export class AdminPage extends BasePage {
.first()
.click();
}

/** Maximum booking lengths of a tenant in the tenants table */
async tenantBookingPeriods(name: string): Promise<Locator> {
const row = await this.tableRow(name, this.section);
return row.locator('[booking-period] > div');
}

/** Open the edit modal of a tenant */
async editTenant(name: string): Promise<void> {
const row = await this.tableRow(name, this.section);
await row.getByRole('button', { name: 'Edit Tenant' }).click();
await expect(this.dialog).toBeVisible();
}

/** Rows of the maximum booking length editor in the tenant modal */
get bookingPeriodRows(): Locator {
return this.dialog.locator('[period-row]');
}

/** Set the maximum booking length of a row in the tenant modal */
async setBookingPeriod(
index: number,
period: {
type?: string;
days?: number;
hours?: number;
minutes?: number;
},
): Promise<void> {
const row = this.bookingPeriodRows.nth(index);
if (period.type != null) {
await row
.getByRole('textbox', { name: 'Booking type' })
.fill(period.type);
}
if (period.days != null) {
await row
.getByRole('spinbutton', { name: 'Days' })
.fill(`${period.days}`);
}
const selects: [string, number | undefined, string][] = [
['Hours', period.hours, 'hour'],
['Minutes', period.minutes, 'minute'],
];
for (const [name, value, unit] of selects) {
if (value == null) continue;
await row.getByRole('combobox', { name }).click();
await this.page
.getByRole('option', {
name: `${value} ${unit}${value === 1 ? '' : 's'}`,
exact: true,
})
.click();
}
}
}
14 changes: 14 additions & 0 deletions public/assets/locale/en-AU.json
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,20 @@
"TENANTS_MINUTES": "{{ minute }} minutes",
"TENANTS_HOURS": "{{ hour }} hours",
"TENANTS_MINUTES_HOURS": "{{ hour }} hours {{ minute }} minutes",
"TENANTS_BOOKING_PERIOD": "Maximum Booking Length",
"TENANTS_BOOKING_PERIOD_COLUMN": "Max Length",
"TENANTS_BOOKING_PERIOD_HINT": "Longest booking allowed for each booking type, such as desk. Remove a row to allow bookings of any length for that type.",
"TENANTS_BOOKING_PERIOD_ERROR": "Enter a unique booking type and a length greater than 0 for each row.",
"TENANTS_BOOKING_TYPE": "Booking type",
"TENANTS_DURATION_DAYS": "{{ count }} days",
"TENANTS_DURATION_DAYS_1": "{{ count }} day",
"TENANTS_DURATION_HOURS": "{{ count }} hours",
"TENANTS_DURATION_HOURS_1": "{{ count }} hour",
"TENANTS_DURATION_MINUTES": "{{ count }} minutes",
"TENANTS_DURATION_MINUTES_1": "{{ count }} minute",
"TENANTS_DAYS": "Days",
"TENANTS_HOURS_LABEL": "Hours",
"TENANTS_MINUTES_LABEL": "Minutes",
"TENANTS_SAVE_ERROR": "Failed to update tenant.",
"TENANTS_SAVE_SUCCESS": "Successfully updated tenant.",
"RESOURCE_IMPORTS_HEADER": "Resource Imports",
Expand Down
14 changes: 14 additions & 0 deletions public/assets/locale/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,20 @@
"TENANTS_MINUTES": "{{ minute }} minutes",
"TENANTS_HOURS": "{{ hour }} hours",
"TENANTS_MINUTES_HOURS": "{{ hour }} hours {{ minute }} minutes",
"TENANTS_BOOKING_PERIOD": "Maximum Booking Length",
"TENANTS_BOOKING_PERIOD_COLUMN": "Max Length",
"TENANTS_BOOKING_PERIOD_HINT": "Longest booking allowed for each booking type, such as desk. Remove a row to allow bookings of any length for that type.",
"TENANTS_BOOKING_PERIOD_ERROR": "Enter a unique booking type and a length greater than 0 for each row.",
"TENANTS_BOOKING_TYPE": "Booking type",
"TENANTS_DURATION_DAYS": "{{ count }} days",
"TENANTS_DURATION_DAYS_1": "{{ count }} day",
"TENANTS_DURATION_HOURS": "{{ count }} hours",
"TENANTS_DURATION_HOURS_1": "{{ count }} hour",
"TENANTS_DURATION_MINUTES": "{{ count }} minutes",
"TENANTS_DURATION_MINUTES_1": "{{ count }} minute",
"TENANTS_DAYS": "Days",
"TENANTS_HOURS_LABEL": "Hours",
"TENANTS_MINUTES_LABEL": "Minutes",
"TENANTS_SAVE_ERROR": "Failed to update tenant.",
"TENANTS_SAVE_SUCCESS": "Successfully updated tenant.",
"RESOURCE_IMPORTS_HEADER": "Resource Imports",
Expand Down
14 changes: 14 additions & 0 deletions public/assets/locale/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,20 @@
"TENANTS_MINUTES": "{{ minute }} minutes",
"TENANTS_HOURS": "{{ hour }} hours",
"TENANTS_MINUTES_HOURS": "{{ hour }} hours {{ minute }} minutes",
"TENANTS_BOOKING_PERIOD": "Maximum Booking Length",
"TENANTS_BOOKING_PERIOD_COLUMN": "Max Length",
"TENANTS_BOOKING_PERIOD_HINT": "Longest booking allowed for each booking type, such as desk. Remove a row to allow bookings of any length for that type.",
"TENANTS_BOOKING_PERIOD_ERROR": "Enter a unique booking type and a length greater than 0 for each row.",
"TENANTS_BOOKING_TYPE": "Booking type",
"TENANTS_DURATION_DAYS": "{{ count }} days",
"TENANTS_DURATION_DAYS_1": "{{ count }} day",
"TENANTS_DURATION_HOURS": "{{ count }} hours",
"TENANTS_DURATION_HOURS_1": "{{ count }} hour",
"TENANTS_DURATION_MINUTES": "{{ count }} minutes",
"TENANTS_DURATION_MINUTES_1": "{{ count }} minute",
"TENANTS_DAYS": "Days",
"TENANTS_HOURS_LABEL": "Hours",
"TENANTS_MINUTES_LABEL": "Minutes",
"TENANTS_SAVE_ERROR": "Failed to update tenant.",
"TENANTS_SAVE_SUCCESS": "Successfully updated tenant.",
"RESOURCE_IMPORTS_HEADER": "Resource Imports",
Expand Down
38 changes: 37 additions & 1 deletion src/app/admin/staff-api.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { SimpleTableComponent } from '../ui/simple-table.component';
import { TranslatePipe } from '../ui/translate.pipe';
import { AdminDataService } from './admin-data.service';
import { BookingLimitsModalComponent } from './booking-limits-modal.component';
import { isSecretExpired, isSecretExpiring } from './staff-api.utilities';
import {
formatMinutes,
isSecretExpired,
isSecretExpiring,
} from './staff-api.utilities';
import { StaffTenantModalComponent } from './staff-tenant-modal.component';

export interface PlaceTenant {
Expand All @@ -32,6 +36,8 @@ export interface PlaceTenant {
service_account?: string;
booking_limits: Record<string, number>;
booking_range?: Record<string, number>;
/** Booking type => maximum length of a booking in minutes */
booking_period?: Record<string, number>;
credentials?: HashMap<string>;
secret_expiry?: number;
early_checkin?: number;
Expand Down Expand Up @@ -91,6 +97,14 @@ export interface PlaceTenant {
key: 'platform',
name: 'ADMIN.TENANTS_PLATFORM' | translate,
},
{
key: 'booking_period',
name:
'ADMIN.TENANTS_BOOKING_PERIOD_COLUMN'
| translate,
content: period_template,
sortable: false,
},
{
key: 'secret_expiry',
name: 'ADMIN.TENANTS_SECRET_EXPIRY' | translate,
Expand Down Expand Up @@ -135,6 +149,16 @@ export interface PlaceTenant {
</div>
</div>
</ng-template>
<ng-template #period_template let-data="data">
<div class="flex flex-col px-2 py-1" booking-period>
@for (period of bookingPeriods(data); track period.type) {
<div>
<span class="opacity-60">{{ period.type }}:</span>
{{ period.length }}
</div>
}
</div>
</ng-template>
<ng-template #actions_template let-row="row">
<div class="flex items-center space-x-2 p-2">
<button
Expand All @@ -144,6 +168,9 @@ export interface PlaceTenant {
[matTooltip]="
'ADMIN.TENANTS_EDIT_BOOKING_LIMITS' | translate
"
[attr.aria-label]="
'ADMIN.TENANTS_EDIT_BOOKING_LIMITS' | translate
"
(click)="editLimits(row)"
>
<icon>app_registration</icon>
Expand All @@ -153,6 +180,7 @@ export interface PlaceTenant {
default
matRipple
[matTooltip]="'ADMIN.TENANTS_EDIT' | translate"
[attr.aria-label]="'ADMIN.TENANTS_EDIT' | translate"
(click)="editTenant(row)"
>
<icon>edit</icon>
Expand All @@ -163,6 +191,7 @@ export interface PlaceTenant {
error
matRipple
[matTooltip]="'ADMIN.TENANTS_REMOVE' | translate"
[attr.aria-label]="'ADMIN.TENANTS_REMOVE' | translate"
(click)="removeTenant(row)"
>
<icon>delete</icon>
Expand Down Expand Up @@ -208,6 +237,13 @@ export class PlaceStaffAPIComponent implements OnInit {
isSecretExpiring(tenant);
public readonly expired = (tenant: PlaceTenant) => isSecretExpired(tenant);

public bookingPeriods(periods?: Record<string, number>) {
return Object.entries(periods || {}).map(([type, minutes]) => ({
type,
length: formatMinutes(minutes),
}));
}

public async ngOnInit() {
this.loading.set('Loading domains...');
const domain = await this._admin_data.selectDefaultDomain('staff-api');
Expand Down
Loading
Loading