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
5 changes: 4 additions & 1 deletion apps/signage-manager/USER_STORIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,16 @@ These stories cover the current app workflows:
**Acceptance Criteria:**

- The templates page shows a searchable template list and loads more templates as the user scrolls.
- The header shows the number of templates that match, including templates that are not loaded yet.
- If the list cannot load, the page shows an error with a retry button, not an empty list.
- A link to a template that is not in the loaded pages opens that template.
- Users with create or update permission can create templates and edit their layout items.
- Users with create permission can duplicate a template. The copy has the same settings and saved layouts. It is not approved and has no template mappings.
- Template rows show approval-required and awaiting-review states.
- Users with approval permission can review and approve a selected template.
- Users without approval permission can select an approver and request template approval with a message.
- The approval preview shows only changed layout items from the pending and approved templates, including the applicable X and Y values. It shows a no-older-version placeholder when no distinct approved version exists.
- Users with update permission can discard pending changes when an approved version exists.
- Users with update permission can discard pending changes when an approved version exists. They must confirm first.
- Users must confirm before they leave a template that has unsaved layout changes. Confirming discards the changes. The browser warns before a reload or tab close drops them.

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,15 @@ export class TemplateApproveModalComponent {
public readonly has_previous_version = () =>
this.template_versions().length > 1;

/** Discard the pending version. The service asks the user to confirm. */
public async undoChanges() {
const previous_version = this.template_versions()[1];
if (!previous_version) return;
this.loading.set(i18n('SIGNAGE_MANAGER.UNDOING_CHANGES'));
this._dialog_ref.disableClose = true;
try {
const undone = await this._template_service.undoTemplateChanges(
this._data.template.id,
previous_version,
);
if (undone) this._dialog_ref.close(true);
} finally {
this.loading.set('');
this._dialog_ref.disableClose = false;
}
const undone = await this._template_service.undoTemplateChanges(
this._data.template.id,
previous_version,
);
if (undone) this._dialog_ref.close(true);
}

/** Approve the pending version. Blocked when the versions failed to load. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,10 @@ export class TemplateEditModalComponent {
this._dialog_ref.disableClose = false;
this._dialog_ref.close(result);
notifySuccess(i18n('SIGNAGE_MANAGER.TEMPLATE_SAVED'));
} catch (e) {
} catch {
this._dialog_ref.disableClose = false;
this.loading.set(false);
notifyError(i18n('SIGNAGE_MANAGER.TEMPLATE_SAVE_ERROR'));
throw e;
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,12 @@ export class TemplateMappingModalComponent {
notifySuccess(
i18n('SIGNAGE_MANAGER.SVC_TEMPLATE_MAPPING_SAVED'),
);
} catch (error) {
} catch {
this._dialog_ref.disableClose = false;
this.loading.set(false);
notifyError(
i18n('SIGNAGE_MANAGER.SVC_TEMPLATE_MAPPING_SAVE_ERROR'),
);
throw error;
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, computed, inject, input, resource } from '@angular/core';
import { MatRippleModule } from '@angular/material/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { i18n } from '@placeos/common';
Expand Down Expand Up @@ -52,11 +53,16 @@ export type TemplateMappingTargetType = 'display' | 'zone';
'SIGNAGE_MANAGER.APPLY_TEMPLATE' | translate
"
(click)="editMapping()"
[disabled]="opening()"
[attr.aria-label]="
'SIGNAGE_MANAGER.APPLY_TEMPLATE' | translate
"
>
<icon>add</icon>
@if (opening()) {
<mat-spinner diameter="20" />
} @else {
<icon>add</icon>
}
</button>
}
</div>
Expand Down Expand Up @@ -320,6 +326,7 @@ export type TemplateMappingTargetType = 'display' | 'zone';
],
imports: [
MatRippleModule,
MatProgressSpinnerModule,
MatTooltipModule,
AuthenticatedImageDirective,
IconComponent,
Expand All @@ -335,6 +342,7 @@ export class TemplateMappingsComponent {
public readonly target_type = input<TemplateMappingTargetType>('display');
public readonly target_id = input('');
public readonly can_update = this._context.can_update;
public readonly opening = this._template_service.template_mapping_opening;

private readonly _mappings = resource({
params: () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,18 @@ export interface TemplateRequestApprovalModalResult {
}}</icon>
</button>
@if (show_preview()) {
<template-approval-preview
[versions]="template_versions()"
/>
@if (versions_error()) {
<p class="text-error p-8 text-center">
{{
'SIGNAGE_MANAGER.TEMPLATE_VERSIONS_LOAD_ERROR'
| translate
}}
</p>
} @else {
<template-approval-preview
[versions]="template_versions()"
/>
}
}
</main>
<footer
Expand Down Expand Up @@ -230,6 +239,8 @@ export class TemplateRequestApprovalModalComponent {
public readonly show_preview = signal(false);
public readonly loading = signal('');
public readonly template_versions = signal<SignageTemplate[]>([]);
/** Whether the versions for the preview failed to load */
public readonly versions_error = signal(false);
public readonly has_previous_version = () =>
this.template_versions().length > 1;
public readonly can_update = this._context.can_update_templates;
Expand All @@ -240,15 +251,20 @@ export class TemplateRequestApprovalModalComponent {
if (show_preview) void this._loadTemplateVersions();
}

/** Load the versions once. Sets `versions_error` and returns none on failure. */
private async _loadTemplateVersions() {
if (this.template_versions().length) return this.template_versions();
const template_id = this.data?.template?.id || '';
if (!template_id) return [];
this.loading.set(i18n('SIGNAGE_MANAGER.LOADING_VERSIONS'));
this.versions_error.set(false);
try {
const versions = await loadTemplateApprovalVersions(template_id);
this.template_versions.set(versions);
return versions;
} catch {
this.versions_error.set(true);
return [];
} finally {
this.loading.set('');
}
Expand All @@ -261,20 +277,14 @@ export class TemplateRequestApprovalModalComponent {
});
}

/** Discard the pending version. The service asks the user to confirm. */
public async undoChanges() {
const [, previous_version] = await this._loadTemplateVersions();
if (!previous_version) return;
this.loading.set(i18n('SIGNAGE_MANAGER.UNDOING_CHANGES'));
this._dialog_ref.disableClose = true;
try {
const undone = await this._template_service.undoTemplateChanges(
this.data.template.id,
previous_version,
);
if (undone) this._dialog_ref.close();
} finally {
this.loading.set('');
this._dialog_ref.disableClose = false;
}
const undone = await this._template_service.undoTemplateChanges(
this.data.template.id,
previous_version,
);
if (undone) this._dialog_ref.close();
}
}
7 changes: 6 additions & 1 deletion apps/signage-manager/src/app/signage-plugin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,16 @@ export class SignagePluginService {
});
public readonly widgets = computed(() => this._widgets.value() || []);

/** Find a plugin by ID, from every plugin and not only the group's */
/**
* Find a plugin by ID, from every plugin and not only the group's.
* Uses the loaded plugins first and only queries on a miss.
*/
public async resolvePlugin(
plugin_id: string,
): Promise<SignagePlugin | undefined> {
if (!plugin_id) return undefined;
const loaded = this.all_plugins().find(({ id }) => id === plugin_id);
if (loaded) return loaded;
const result = await querySignagePlugins({
limit: 500,
plugin_type: 'plugin',
Expand Down
Loading
Loading