diff --git a/public/assets/locale/en-AU.json b/public/assets/locale/en-AU.json index 531cf1b6a..49fe9fd03 100644 --- a/public/assets/locale/en-AU.json +++ b/public/assets/locale/en-AU.json @@ -48,7 +48,21 @@ "NONE": "None", "CANCEL": "Cancel", "CLOSE": "Close", + "MENU": "Menu", + "CLEAR_VALUE": "Clear", + "REMOVE": "Remove", + "VIEW": "View", + "MENU_EXPAND": "Expand menu", + "MENU_COLLAPSE": "Collapse menu", + "MORE_ACTIONS": "More actions", + "FILTER": "Filter", + "INCREASE": "Increase", + "DECREASE": "Decrease", + "DISCARD": "Discard", + "DISCARD_CHANGES": "Discard changes?", + "DISCARD_CHANGES_MSG": "You have unsaved changes. Close and lose them?", "CONFIRM": "Confirm", + "DELETE": "Delete", "ITEM_REMOVE": "Remove {{ item }}", "FIELD_REQUIRED": "This field is required", "DATE_EMPTY": "No date selected", @@ -67,6 +81,7 @@ "COPY_LINK": "Copy Link", "RETRY": "Retry", "EDITOR_LOAD_ERROR": "Could not load the editor. Check your connection and retry.", + "LOAD_ERROR": "Failed to load data. Error: {{ error }}", "IMAGES": "Images", "NEW_ITEM": "New item", "ITEM_ADD": "Add {{ item }}", @@ -172,7 +187,7 @@ "GIT_PULL_LATEST": "Pull latest commit", "COMMIT_HASH_COPIED": "Commit hash copied to clipboard", "DESCRIPTION_EMPTY": "No description", - "INVALID_FIELDS": "Some fields are invalid. [{{ field_list }}]", + "INVALID_FIELDS": "Check these fields: {{ field_list }}", "BULK_ADD": "Bulk add {{ type }}", "BULK_DOWNLOAD": "Download Template", "BULK_DROP_MSG": "Select or drop TSV file to begin", @@ -244,7 +259,7 @@ "ADD": "Add system", "EDIT": "Edit system", "DELETE": "Delete system", - "DELETE_MSG": "

Are you sure you want delete this system({{ name }})?

Deleting this will immediately delete modules that are not in another system

", + "DELETE_MSG": "

Delete the system {{ name }}?

Modules that are not in another system are deleted immediately.

", "DELETE_LOADING": "Deleting system...", "DELETE_SUCCESS": "Successfully deleted system.", "DELETE_ERROR": "Failed to delete system. Error: {{ error }}", @@ -332,7 +347,7 @@ "SEARCH": "Search for modules...", "ADD": "Add module", "DELETE": "Delete module", - "DELETE_MSG": "

Are you sure you want delete this module?

Deleting this will module immediately remove it from any system associated with it

", + "DELETE_MSG": "

Delete the module {{ name }}?

It is removed immediately from every system that uses it.

", "DELETE_LOADING": "Deleting module...", "DELETE_SUCCESS": "Successfully deleted module.", "DELETE_ERROR": "Failed to delete module. Error: {{ error }}", @@ -389,7 +404,7 @@ "ADD": "Add zone", "EDIT": "Edit zone", "DELETE": "Delete zone", - "DELETE_MSG": "

Are you sure you want delete this zone?

Zones beneath it, its triggers, metadata and settings are removed immediately. Systems in this zone are kept unless you also remove associated resources.

", + "DELETE_MSG": "

Delete the zone {{ name }}?

Zones beneath it, its triggers, metadata and settings are removed immediately. Systems in this zone are kept unless you also remove associated resources.

", "DELETE_LOADING": "Deleting zone...", "DELETE_SUCCESS": "Successfully deleted zone.", "DELETE_ERROR": "Failed to delete zone. Error: {{ error }}", @@ -463,7 +478,7 @@ "ADD": "Add driver", "EDIT": "Edit driver", "DELETE": "Delete driver", - "DELETE_MSG": "

Are you sure you want delete this driver?

", + "DELETE_MSG": "

Delete the driver {{ name }}?

", "DELETE_LOADING": "Deleting driver...", "DELETE_SUCCESS": "Successfully deleted driver.", "DELETE_ERROR": "Failed to delete driver. Error: {{ error }}", @@ -476,6 +491,7 @@ "UPDATE": "Update Drivers", "UPDATE_COUNT": "{{ count }} updates available", "UPDATE_SELECTED": "Update selected drivers", + "UPDATING": "Updating drivers...", "NO_UPDATES": "No drivers require updating.", "TAB_ABOUT": "About", "TAB_DOCS": "Readme", @@ -518,7 +534,7 @@ "DELETE": "Delete repository", "REMOVE": "Remove repository", "NEW": "New repository", - "DELETE_MSG": "

Deleting this repository will immediately remove assoicated drivers and modules

", + "DELETE_MSG": "

Delete the repository {{ name }}?

Its drivers and their modules are removed immediately.

", "DELETE_LOADING": "Deleting repository...", "DELETE_SUCCESS": "Successfully deleted repository.", "DELETE_ERROR": "Failed to delete repository. Error: {{ error }}", @@ -567,7 +583,7 @@ "DELETE": "Delete trigger", "DELETE_INSTANCE": "Delete instance", "REMOVE": "Remove trigger", - "DELETE_MSG": "

Are you sure you want delete this trigger?

Deleting this trigger will immediately remove it from all associated systems and zones

", + "DELETE_MSG": "

Delete the trigger {{ name }}?

It is removed immediately from all systems and zones that use it.

", "DELETE_LOADING": "Deleting trigger...", "DELETE_SUCCESS": "Successfully deleted trigger.", "DELETE_ERROR": "Failed to delete trigger. Error: {{ error }}", @@ -684,7 +700,7 @@ "BULK": "Bulk add users", "REMOVE": "Remove user", "DELETE": "Delete user", - "DELETE_MSG": "

Are you sure you want delete this user?

The user will be removed from the system within 24 hours

", + "DELETE_MSG": "

Delete the user {{ name }}?

The user is removed within 24 hours.

", "DELETE_LOADING": "Deleting user...", "DELETE_SUCCESS": "Successfully deleted user.", "DELETE_ERROR": "Failed to delete user. Error: {{ error }}", @@ -748,7 +764,7 @@ "NEW": "New group", "REMOVE": "Remove group", "DELETE": "Delete group", - "DELETE_MSG": "

Are you sure you want delete this group?

The group will be deleted immediately.

", + "DELETE_MSG": "

Delete the group {{ name }}?

The group is deleted immediately.

", "DELETE_LOADING": "Deleting group...", "DELETE_SUCCESS": "Successfully deleted group.", "DELETE_ERROR": "Failed to delete group. Error: {{ error }}", @@ -842,7 +858,7 @@ "BULK": "Bulk add domains", "REMOVE": "Remove domain", "DELETE": "Delete domain", - "DELETE_MSG": "

Are you sure you want delete this domain?

Its users, auth sources and groups are removed immediately.

", + "DELETE_MSG": "

Delete the domain {{ name }}?

Its users, auth sources and groups are removed immediately.

", "DELETE_LOADING": "Deleting domain...", "DELETE_SUCCESS": "Successfully deleted domain.", "DELETE_ERROR": "Failed to delete domain. Error: {{ error }}", diff --git a/public/assets/locale/en-GB.json b/public/assets/locale/en-GB.json index 0d0cca2ad..54f758772 100644 --- a/public/assets/locale/en-GB.json +++ b/public/assets/locale/en-GB.json @@ -80,7 +80,7 @@ "METADATA_REMOVE": "Remove Metadata block", "METADATA_EMPTY": "No Metadata blocks found", "METADATA_SAVE": "Successfully saved metadata", - "TOTAL_ITEMS": "{{ count }} items(s)", + "TOTAL_ITEMS": "{{ count }} item(s)", "ITEM_ERROR": "Failed to save item. Error: {{ error }}", "ITEM_SAVE": "Successfully saved item", "UPDATE_AVAILABLE": "Update Available", @@ -152,7 +152,7 @@ "GIT_COMMIT": "Commit hash", "GIT_PULL_LATEST": "Pull latest commit", "DESCRIPTION_EMPTY": "No description", - "INVALID_FIELDS": "Some fields are invalid. [{{ field_list }}]", + "INVALID_FIELDS": "Check these fields: {{ field_list }}", "BULK_ADD": "Bulk add {{ type }}", "BULK_DOWNLOAD": "Download Template", "BULK_DROP_MSG": "Select or drop TSV file to begin", @@ -213,7 +213,7 @@ "ADD": "Add system", "EDIT": "Edit system", "DELETE": "Delete system", - "DELETE_MSG": "

Are you sure you want delete this system({{ name }})?

Deleting this will immediately delete modules that are not in another system

", + "DELETE_MSG": "

Delete the system {{ name }}?

Modules that are not in another system are deleted immediately.

", "DELETE_LOADING": "Deleting system...", "DELETE_SUCCESS": "Successfully deleted system.", "DELETE_ERROR": "Failed to delete system. Error: {{ error }}", @@ -292,7 +292,7 @@ "SEARCH": "Search for modules...", "ADD": "Add module", "DELETE": "Delete module", - "DELETE_MSG": "

Are you sure you want delete this module?

Deleting this will module immediately remove it from any system associated with it

", + "DELETE_MSG": "

Delete the module {{ name }}?

It is removed immediately from every system that uses it.

", "DELETE_LOADING": "Deleting module...", "DELETE_SUCCESS": "Successfully deleted module.", "DELETE_ERROR": "Failed to delete module. Error: {{ error }}", @@ -348,7 +348,7 @@ "ADD": "Add zone", "EDIT": "Edit zone", "DELETE": "Delete zone", - "DELETE_MSG": "

Are you sure you want delete this zone?

Zones beneath it, its triggers, metadata and settings are removed immediately. Systems in this zone are kept unless you also remove associated resources.

", + "DELETE_MSG": "

Delete the zone {{ name }}?

Zones beneath it, its triggers, metadata and settings are removed immediately. Systems in this zone are kept unless you also remove associated resources.

", "DELETE_LOADING": "Deleting zone...", "DELETE_SUCCESS": "Successfully deleted zone.", "DELETE_ERROR": "Failed to delete zone. Error: {{ error }}", @@ -403,7 +403,7 @@ "ADD": "Add driver", "EDIT": "Edit driver", "DELETE": "Delete driver", - "DELETE_MSG": "

Are you sure you want delete this driver?

", + "DELETE_MSG": "

Delete the driver {{ name }}?

", "DELETE_LOADING": "Deleting driver...", "DELETE_SUCCESS": "Successfully deleted driver.", "DELETE_ERROR": "Failed to delete driver. Error: {{ error }}", @@ -457,7 +457,7 @@ "DELETE": "Delete repository", "REMOVE": "Remove repository", "NEW": "New repository", - "DELETE_MSG": "

Deleting this repository will immediately remove assoicated drivers and modules

", + "DELETE_MSG": "

Delete the repository {{ name }}?

Its drivers and their modules are removed immediately.

", "DELETE_LOADING": "Deleting repository...", "DELETE_SUCCESS": "Successfully deleted repository.", "DELETE_ERROR": "Failed to delete repository. Error: {{ error }}", @@ -500,7 +500,7 @@ "DELETE": "Delete trigger", "DELETE_INSTANCE": "Delete instance", "REMOVE": "Remove trigger", - "DELETE_MSG": "

Are you sure you want delete this trigger?

Deleting this trigger will immediately remove it from all associated systems and zones

", + "DELETE_MSG": "

Delete the trigger {{ name }}?

It is removed immediately from all systems and zones that use it.

", "DELETE_LOADING": "Deleting trigger...", "DELETE_SUCCESS": "Successfully deleted trigger.", "DELETE_ERROR": "Failed to delete trigger. Error: {{ error }}", @@ -607,7 +607,7 @@ "NEW": "New user", "BULK": "Bulk add users", "REMOVE": "Remove user", - "DELETE_MSG": "

Are you sure you want delete this user?

The user will be removed from the system within 24 hours

", + "DELETE_MSG": "

Delete the user {{ name }}?

The user is removed within 24 hours.

", "DELETE_LOADING": "Deleting user...", "DELETE_SUCCESS": "Successfully deleted user.", "DELETE_ERROR": "Failed to delete user. Error: {{ error }}", @@ -651,7 +651,7 @@ "BULK": "Bulk add domains", "REMOVE": "Remove domain", "DELETE": "Delete domain", - "DELETE_MSG": "

Are you sure you want delete this domain?

Its users, auth sources and groups are removed immediately.

", + "DELETE_MSG": "

Delete the domain {{ name }}?

Its users, auth sources and groups are removed immediately.

", "DELETE_LOADING": "Deleting domain...", "DELETE_SUCCESS": "Successfully deleted domain.", "DELETE_ERROR": "Failed to delete domain. Error: {{ error }}", diff --git a/public/assets/locale/en-US.json b/public/assets/locale/en-US.json index 74f37f815..fc048ed1c 100644 --- a/public/assets/locale/en-US.json +++ b/public/assets/locale/en-US.json @@ -76,7 +76,7 @@ "METADATA_REMOVE": "Remove Metadata block", "METADATA_EMPTY": "No Metadata blocks found", "METADATA_SAVE": "Successfully saved metadata", - "TOTAL_ITEMS": "{{ count }} items(s)", + "TOTAL_ITEMS": "{{ count }} item(s)", "ITEM_ERROR": "Failed to save item. Error: {{ error }}", "ITEM_SAVE": "Successfully saved item", "UPDATE_AVAILABLE": "Update Available", @@ -149,7 +149,7 @@ "GIT_COMMIT": "Commit hash", "GIT_PULL_LATEST": "Pull latest commit", "DESCRIPTION_EMPTY": "No description", - "INVALID_FIELDS": "Some fields are invalid. [{{ field_list }}]", + "INVALID_FIELDS": "Check these fields: {{ field_list }}", "BULK_ADD": "Bulk add {{ type }}", "BULK_DOWNLOAD": "Download Template", "BULK_DROP_MSG": "Select or drop TSV file to begin", @@ -210,7 +210,7 @@ "ADD": "Add system", "EDIT": "Edit system", "DELETE": "Delete system", - "DELETE_MSG": "

Are you sure you want delete this system({{ name }})?

Deleting this will immediately delete modules that are not in another system

", + "DELETE_MSG": "

Delete the system {{ name }}?

Modules that are not in another system are deleted immediately.

", "DELETE_LOADING": "Deleting system...", "DELETE_SUCCESS": "Successfully deleted system.", "DELETE_ERROR": "Failed to delete system. Error: {{ error }}", @@ -289,7 +289,7 @@ "SEARCH": "Search for modules...", "ADD": "Add module", "DELETE": "Delete module", - "DELETE_MSG": "

Are you sure you want delete this module?

Deleting this will module immediately remove it from any system associated with it

", + "DELETE_MSG": "

Delete the module {{ name }}?

It is removed immediately from every system that uses it.

", "DELETE_LOADING": "Deleting module...", "DELETE_SUCCESS": "Successfully deleted module.", "DELETE_ERROR": "Failed to delete module. Error: {{ error }}", @@ -345,7 +345,7 @@ "ADD": "Add zone", "EDIT": "Edit zone", "DELETE": "Delete zone", - "DELETE_MSG": "

Are you sure you want delete this zone?

Zones beneath it, its triggers, metadata and settings are removed immediately. Systems in this zone are kept unless you also remove associated resources.

", + "DELETE_MSG": "

Delete the zone {{ name }}?

Zones beneath it, its triggers, metadata and settings are removed immediately. Systems in this zone are kept unless you also remove associated resources.

", "DELETE_LOADING": "Deleting zone...", "DELETE_SUCCESS": "Successfully deleted zone.", "DELETE_ERROR": "Failed to delete zone. Error: {{ error }}", @@ -400,7 +400,7 @@ "ADD": "Add driver", "EDIT": "Edit driver", "DELETE": "Delete driver", - "DELETE_MSG": "

Are you sure you want delete this driver?

", + "DELETE_MSG": "

Delete the driver {{ name }}?

", "DELETE_LOADING": "Deleting driver...", "DELETE_SUCCESS": "Successfully deleted driver.", "DELETE_ERROR": "Failed to delete driver. Error: {{ error }}", @@ -454,7 +454,7 @@ "DELETE": "Delete repository", "REMOVE": "Remove repository", "NEW": "New repository", - "DELETE_MSG": "

Deleting this repository will immediately remove assoicated drivers and modules

", + "DELETE_MSG": "

Delete the repository {{ name }}?

Its drivers and their modules are removed immediately.

", "DELETE_LOADING": "Deleting repository...", "DELETE_SUCCESS": "Successfully deleted repository.", "DELETE_ERROR": "Failed to delete repository. Error: {{ error }}", @@ -497,7 +497,7 @@ "DELETE": "Delete trigger", "DELETE_INSTANCE": "Delete instance", "REMOVE": "Remove trigger", - "DELETE_MSG": "

Are you sure you want delete this trigger?

Deleting this trigger will immediately remove it from all associated systems and zones

", + "DELETE_MSG": "

Delete the trigger {{ name }}?

It is removed immediately from all systems and zones that use it.

", "DELETE_LOADING": "Deleting trigger...", "DELETE_SUCCESS": "Successfully deleted trigger.", "DELETE_ERROR": "Failed to delete trigger. Error: {{ error }}", @@ -604,7 +604,7 @@ "NEW": "New user", "BULK": "Bulk add users", "REMOVE": "Remove user", - "DELETE_MSG": "

Are you sure you want delete this user?

The user will be removed from the system within 24 hours

", + "DELETE_MSG": "

Delete the user {{ name }}?

The user is removed within 24 hours.

", "DELETE_LOADING": "Deleting user...", "DELETE_SUCCESS": "Successfully deleted user.", "DELETE_ERROR": "Failed to delete user. Error: {{ error }}", @@ -648,7 +648,7 @@ "BULK": "Bulk add domains", "REMOVE": "Remove domain", "DELETE": "Delete domain", - "DELETE_MSG": "

Are you sure you want delete this domain?

Its users, auth sources and groups are removed immediately.

", + "DELETE_MSG": "

Delete the domain {{ name }}?

Its users, auth sources and groups are removed immediately.

", "DELETE_LOADING": "Deleting domain...", "DELETE_SUCCESS": "Successfully deleted domain.", "DELETE_ERROR": "Failed to delete domain. Error: {{ error }}", diff --git a/src/app/admin/admin.component.ts b/src/app/admin/admin.component.ts index 9e7357e96..c67a3ec25 100644 --- a/src/app/admin/admin.component.ts +++ b/src/app/admin/admin.component.ts @@ -12,6 +12,7 @@ import { DebugOutputComponent } from '../ui/debug-output.component'; import { IconComponent } from '../ui/icon.component'; import { ItemTab } from '../ui/item-tablist.component'; import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; +import { TranslatePipe } from '../ui/translate.pipe'; @Component({ selector: 'app-engine', @@ -51,6 +52,7 @@ import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; matRipple class="absolute top-2 left-4 z-40 mr-2 sm:hidden" (click)="open_menu = true" + [attr.aria-label]="'COMMON.MENU' | translate" > menu @@ -81,6 +83,7 @@ import { SidebarMenuComponent } from '../ui/sidebar-menu.component'; `, ], imports: [ + TranslatePipe, DebugOutputComponent, IconComponent, MatRippleModule, diff --git a/src/app/admin/api-keys/api-key-modal.component.ts b/src/app/admin/api-keys/api-key-modal.component.ts index 9f10a2b46..c6a3be488 100644 --- a/src/app/admin/api-keys/api-key-modal.component.ts +++ b/src/app/admin/api-keys/api-key-modal.component.ts @@ -112,7 +112,9 @@ export interface APIKeyModalData { [matChipInputSeparatorKeyCodes]="separators" [matChipInputAddOnBlur]="true" (matChipInputTokenEnd)="addScope($event)" - (input)="scope_search.set($any($event.target).value)" + (input)=" + scope_search.set($any($event.target).value) + " [matAutocomplete]="auto" /> @@ -178,6 +180,9 @@ export interface APIKeyModalData { icon type="button" (click)="clearUser()" + [attr.aria-label]=" + 'COMMON.CLEAR_VALUE' | translate + " > close @@ -278,6 +283,9 @@ export interface APIKeyModalData { matSuffix type="button" (click)="setExpiry('')" + [attr.aria-label]=" + 'COMMON.CLEAR_VALUE' | translate + " > close @@ -303,7 +311,10 @@ export interface APIKeyModalData { {{ 'ADMIN.APP_KEYS_TTL_NONE' | translate }} - @for (preset of ttl_presets; track preset.seconds) { + @for ( + preset of ttl_presets; + track preset.seconds + ) { {{ preset.label | translate }} @@ -406,7 +417,10 @@ export class APIKeyModalComponent extends AsyncHandler implements OnInit { (a.name || '').localeCompare(b.name || ''), ); // The selected user's label fills the input; don't filter it away. - if (!search || search === this._userLabel(this.formModel().user).toLowerCase()) { + if ( + !search || + search === this._userLabel(this.formModel().user).toLowerCase() + ) { return list; } return list.filter((user) => diff --git a/src/app/admin/api-keys/api-keys.component.ts b/src/app/admin/api-keys/api-keys.component.ts index d187636ce..fa3ae7f42 100644 --- a/src/app/admin/api-keys/api-keys.component.ts +++ b/src/app/admin/api-keys/api-keys.component.ts @@ -78,7 +78,7 @@ import { APIKeyService } from './api-keys.service';
- {{ last_key()?.name || 'Unanamed API Key' }} + {{ last_key()?.name || 'Unnamed API Key' }}
diff --git a/src/app/admin/resource-imports.component.ts b/src/app/admin/resource-imports.component.ts index 81ff49dd2..d7190d390 100644 --- a/src/app/admin/resource-imports.component.ts +++ b/src/app/admin/resource-imports.component.ts @@ -164,18 +164,27 @@ export interface ExternalResource { > publish - - visibility - + @if (row.system_id) { + + visibility + + } @else { + + } diff --git a/src/app/admin/signage-ai/signage-ai-provider-modal.component.ts b/src/app/admin/signage-ai/signage-ai-provider-modal.component.ts index 788d0108a..a332210fa 100644 --- a/src/app/admin/signage-ai/signage-ai-provider-modal.component.ts +++ b/src/app/admin/signage-ai/signage-ai-provider-modal.component.ts @@ -233,7 +233,8 @@ export class SignageAIProviderModalComponent { provider: this._data.item?.provider || 'OPENAI', endpoint: this._data.item?.endpoint || '', location: this._data.item?.location || '', - default_model: this._data.item?.default_model || 'gpt-image-2.5-sunburst', + default_model: + this._data.item?.default_model || 'gpt-image-2.5-sunburst', enabled: this._data.item?.enabled ?? true, is_default: this._data.item?.is_default ?? false, user_per_day: this._data.item?.quotas?.user_per_day ?? 60, @@ -277,7 +278,6 @@ export class SignageAIProviderModalComponent { } this.loading.set(i18n('ADMIN.AI_PROVIDER_SAVING')); - this._dialog_ref.disableClose = true; const body: Partial & { credentials?: Record; @@ -309,7 +309,6 @@ export class SignageAIProviderModalComponent { ); } finally { this.loading.set(''); - this._dialog_ref.disableClose = false; } notifySuccess(i18n('ADMIN.AI_PROVIDER_SAVE_SUCCESS')); this._dialog_ref.close(true); diff --git a/src/app/admin/signage-plugins/signage-plugin-import-modal.component.ts b/src/app/admin/signage-plugins/signage-plugin-import-modal.component.ts index e7fa54a7c..59bbd9b8a 100644 --- a/src/app/admin/signage-plugins/signage-plugin-import-modal.component.ts +++ b/src/app/admin/signage-plugins/signage-plugin-import-modal.component.ts @@ -56,7 +56,12 @@ interface PluginProbe {

{{ 'ADMIN.SIGNAGE_PLUGINS_IMPORT' | translate }}

- diff --git a/src/app/admin/signage-plugins/signage-plugin-modal.component.ts b/src/app/admin/signage-plugins/signage-plugin-modal.component.ts index 4926687da..74f4ca219 100644 --- a/src/app/admin/signage-plugins/signage-plugin-modal.component.ts +++ b/src/app/admin/signage-plugins/signage-plugin-modal.component.ts @@ -355,7 +355,6 @@ export class SignagePluginModalComponent } await submit(this.form, async () => { this.loading.set(i18n('ADMIN.SIGNAGE_PLUGINS_SAVING')); - this._dialog_ref.disableClose = true; const schema = this.schema(); const values = this.formModel(); const payload: Partial = { @@ -376,7 +375,6 @@ export class SignagePluginModalComponent }; try { const result = await this._data.save(payload); - this._dialog_ref.disableClose = false; this.event.emit({ reason: 'done', metadata: { item: result }, @@ -388,7 +386,6 @@ export class SignagePluginModalComponent }); } catch (err) { this.loading.set(null); - this._dialog_ref.disableClose = false; notifyError( i18n('ADMIN.SIGNAGE_PLUGINS_SAVE_ERROR', { error: await readError(err), diff --git a/src/app/admin/signage-plugins/signage-plugin-test-modal.component.ts b/src/app/admin/signage-plugins/signage-plugin-test-modal.component.ts index c72b0dd9f..fc5c235a3 100644 --- a/src/app/admin/signage-plugins/signage-plugin-test-modal.component.ts +++ b/src/app/admin/signage-plugins/signage-plugin-test-modal.component.ts @@ -188,7 +188,12 @@ export interface SignagePluginTestModalData { {{ 'ADMIN.SIGNAGE_PLUGINS_TEST_HEADING' | translate }} — {{ plugin().name }} - diff --git a/src/app/admin/staff-api.component.ts b/src/app/admin/staff-api.component.ts index 9747c9039..a7e8bf743 100644 --- a/src/app/admin/staff-api.component.ts +++ b/src/app/admin/staff-api.component.ts @@ -8,8 +8,9 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; import { del, get, PlaceDomain } from '@placeos/ts-client'; -import { escapeHtml } from '../common/general'; import { readError } from '../common/errors'; +import { escapeHtml } from '../common/general'; +import { reportLoadError } from '../common/load-error'; import { notifyError, notifySuccess } from '../common/notifications'; import { HashMap } from '../common/types'; import { openConfirmModal } from '../overlays/confirm-modal.component'; @@ -18,8 +19,8 @@ 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 { StaffTenantModalComponent } from './staff-tenant-modal.component'; import { isSecretExpired, isSecretExpiring } from './staff-api.utilities'; +import { StaffTenantModalComponent } from './staff-tenant-modal.component'; export interface PlaceTenant { id: string; @@ -278,7 +279,9 @@ export class PlaceStaffAPIComponent implements OnInit { return; } this.loading.set('Loading tenants for domain...'); - const tenants = await get('/api/staff/v1/tenants').catch(() => []); + const tenants = await get('/api/staff/v1/tenants').catch( + reportLoadError([]), + ); this.tenants.set( (tenants || []).filter((t) => t.domain === this.domain()?.domain), ); diff --git a/src/app/admin/staff-tenant-modal.component.ts b/src/app/admin/staff-tenant-modal.component.ts index 3e37fb6e0..5662c1446 100644 --- a/src/app/admin/staff-tenant-modal.component.ts +++ b/src/app/admin/staff-tenant-modal.component.ts @@ -525,7 +525,7 @@ export class StaffTenantModalComponent { const invalid_credentials = this.credential_fields() .filter((field) => field.required) .filter((field) => !this.formModel().credentials[field.key]) - .map((field) => field.key); + .map((field) => FIELD_NAME_MAPPING[field.key] || field.key); if (this.form().invalid() || invalid_credentials.length) { return notifyError( i18n('COMMON.INVALID_FIELDS', { @@ -536,7 +536,6 @@ export class StaffTenantModalComponent { }), ); } - this._dialog_ref.disableClose = true; this.loading.set('Saving staff API tenant...'); const model = this.formModel(); const limits = model.booking_limits || []; @@ -590,7 +589,6 @@ export class StaffTenantModalComponent { : post('/api/staff/v1/tenants', data); const tenant = await call.catch((__) => null); this.loading.set(''); - this._dialog_ref.disableClose = false; if (!tenant) return notifyError(i18n('ADMIN.TENANTS_SAVE_ERROR')); notifySuccess(i18n('ADMIN.TENANTS_SAVE_SUCCESS')); this._dialog_ref.close(); diff --git a/src/app/admin/storage/storage-provider-modal.component.ts b/src/app/admin/storage/storage-provider-modal.component.ts index c04441c72..70f4a070b 100644 --- a/src/app/admin/storage/storage-provider-modal.component.ts +++ b/src/app/admin/storage/storage-provider-modal.component.ts @@ -257,7 +257,6 @@ export class StorageProviderModalComponent { await submit(this.form, async () => undefined); if (this.form().invalid()) return; this.loading.set('Saving storage provider...'); - this._dialog_ref.disableClose = true; const details = { ...this.formModel() } as PlaceStorage; if (details.id && !details.access_secret) { delete (details as PlaceStorage & { access_secret?: unknown }) @@ -271,7 +270,6 @@ export class StorageProviderModalComponent { ); } finally { this.loading.set(''); - this._dialog_ref.disableClose = false; } notifySuccess(i18n('ADMIN.STORAGE_SAVE_SUCCESS')); this._dialog_ref.close(); diff --git a/src/app/admin/storage/storage.component.ts b/src/app/admin/storage/storage.component.ts index a383613be..ac54395ad 100644 --- a/src/app/admin/storage/storage.component.ts +++ b/src/app/admin/storage/storage.component.ts @@ -6,8 +6,9 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { escapeHtml } from '../../common/general'; import { describeError } from '../../common/errors'; +import { escapeHtml } from '../../common/general'; +import { reportLoadError } from '../../common/load-error'; import { i18n } from '../../common/locale.service'; import { notifyError } from '../../common/notifications'; import { openConfirmModal } from '../../overlays/confirm-modal.component'; @@ -248,7 +249,7 @@ export class StorageComponent implements OnInit { this.loading.set('Loading storage...'); const { data } = await queryStorage({ auth_id: this.domain()?.id, - }).catch(() => ({ data: [] })); + }).catch(reportLoadError({ data: [] })); this.store_list.set(data); this.loading.set(''); } diff --git a/src/app/admin/upload-library.component.ts b/src/app/admin/upload-library.component.ts index b482b42e3..a6c8f1543 100644 --- a/src/app/admin/upload-library.component.ts +++ b/src/app/admin/upload-library.component.ts @@ -500,7 +500,7 @@ export class UploadLibraryComponent extends AsyncHandler implements OnInit { if (failed) { notifyError('Failed to upload files.'); } else if (success && finished) { - notifySuccess('Succesfully uploaded files.'); + notifySuccess('Successfully uploaded files.'); } this.clearTimeout('upload_list'); this.loading.set(false); diff --git a/src/app/admin/view-upload-modal.component.ts b/src/app/admin/view-upload-modal.component.ts index 3c36bf892..9da5f5a4c 100644 --- a/src/app/admin/view-upload-modal.component.ts +++ b/src/app/admin/view-upload-modal.component.ts @@ -9,10 +9,10 @@ import { UploadInfo } from './upload-library.component'; selector: 'view-upload-modal', template: `

View Upload

-
diff --git a/src/app/admin/zone-tree-export-modal.component.ts b/src/app/admin/zone-tree-export-modal.component.ts index 43849e161..4bee61d05 100644 --- a/src/app/admin/zone-tree-export-modal.component.ts +++ b/src/app/admin/zone-tree-export-modal.component.ts @@ -16,7 +16,7 @@ import { IconComponent } from '../ui/icon.component'; template: `

Export Zone Tree

-
@@ -59,6 +59,7 @@ import { IconComponent } from '../ui/icon.component'; icon matRipple (click)="removeSelected(zone)" + aria-label="Close" > close diff --git a/src/app/common/forms.ts b/src/app/common/forms.ts index 2e333cc6d..7c2fa29d1 100644 --- a/src/app/common/forms.ts +++ b/src/app/common/forms.ts @@ -17,12 +17,21 @@ export function removeSignalChipItem(items: T[], item: T): T[] { return items.filter((existing) => existing !== item); } +/** Readable name for a form field key, e.g. `service_account` to "Service account" */ +export function fieldLabel(key: string): string { + const words = key.replace(/_/g, ' ').trim(); + return words.charAt(0).toUpperCase() + words.slice(1); +} + +/** Readable names of the invalid top-level fields, for error messages */ export function getInvalidSignalFields( form: FieldTree, ): string[] { const value = form().value() as object; - return Object.keys(value).filter((key) => { - const field = form[key as keyof typeof form]; - return typeof field === 'function' && field().invalid(); - }); + return Object.keys(value) + .filter((key) => { + const field = form[key as keyof typeof form]; + return typeof field === 'function' && field().invalid(); + }) + .map(fieldLabel); } diff --git a/src/app/common/hotkeys.service.ts b/src/app/common/hotkeys.service.ts index 82fc3b3de..24e6f87a8 100644 --- a/src/app/common/hotkeys.service.ts +++ b/src/app/common/hotkeys.service.ts @@ -20,6 +20,17 @@ const INVALID_STANDALONE_KEYS: string[] = [ /** Modifier keys whose held state is read from the key event flags */ const MODIFIER_KEYS: string[] = ['control', 'shift', 'alt', 'meta']; +/** Focused controls that handle letter and arrow keys themselves */ +const KEYBOARD_WIDGETS = [ + '[role="combobox"]', + '[role="listbox"]', + '[role="option"]', + '[role="menu"]', + '[role="menuitem"]', + '[role="radiogroup"]', + '[role="slider"]', + '[role="spinbutton"]', +].join(', '); @Service() export class HotkeysService { @@ -149,6 +160,11 @@ export class HotkeysService { return true; } + // Selects, menus and similar controls use letters and arrow keys + if (active.closest(KEYBOARD_WIDGETS)) { + return true; + } + return false; } diff --git a/src/app/common/item.service.ts b/src/app/common/item.service.ts index 30e3a878b..ac96e7f81 100644 --- a/src/app/common/item.service.ts +++ b/src/app/common/item.service.ts @@ -409,6 +409,7 @@ export class ActiveItemService extends AsyncHandler { ] : undefined, icon: { type: 'icon', content: 'delete' }, + destructive: true, }, }, ); diff --git a/src/app/common/load-error.ts b/src/app/common/load-error.ts new file mode 100644 index 000000000..e4206d511 --- /dev/null +++ b/src/app/common/load-error.ts @@ -0,0 +1,17 @@ +import { readError } from './errors'; +import { i18n } from './locale.service'; +import { notifyError } from './notifications'; + +/** + * Catch handler for list loads. Tells the user the load failed, then returns + * `empty` so the page still renders. Without it a failed load looks like a + * list with nothing in it. + */ +export function reportLoadError(empty: T) { + return async (error: unknown): Promise => { + notifyError( + i18n('COMMON.LOAD_ERROR', { error: await readError(error) }), + ); + return empty; + }; +} diff --git a/src/app/common/notifications.ts b/src/app/common/notifications.ts index b6e6709c4..1df181cd0 100644 --- a/src/app/common/notifications.ts +++ b/src/app/common/notifications.ts @@ -58,7 +58,8 @@ function openNotification( ) { const snackbar_ref = service.open(message, action, { panelClass: [type], - duration: 5000, + // Errors stay until dismissed so they can be read and acted on + duration: type === 'error' ? undefined : 5000, }); if (action) { on_action = on_action || (() => snackbar_ref.dismiss()); diff --git a/src/app/domains/application-form.component.ts b/src/app/domains/application-form.component.ts index b67eff3b5..ab1f1529b 100644 --- a/src/app/domains/application-form.component.ts +++ b/src/app/domains/application-form.component.ts @@ -299,7 +299,6 @@ export class ApplicationFormComponent extends AsyncHandler implements OnInit { await submit(this.form, async () => { const item = this._data.item as unknown as PlaceResource; this.loading.set(i18n(`${this._name}_SAVING`)); - this._dialog_ref.disableClose = true; const item_json = item.toJSON ? item.toJSON() : item; const form_item = ( item.id @@ -317,13 +316,11 @@ export class ApplicationFormComponent extends AsyncHandler implements OnInit { save_item as unknown as PlaceApplication, ) : addApplication(save_item as unknown as PlaceApplication)); - this._dialog_ref.disableClose = false; this.event.emit({ reason: 'done', metadata: { item: _item } }); notifySuccess(i18n(`${this._name}_SAVE_SUCCESS`)); this._dialog_ref.close(); } catch (err) { this.loading.set(null); - this._dialog_ref.disableClose = false; notifyError( i18n(`${this._name}_SAVE_ERROR`, { error: await readError(err), diff --git a/src/app/domains/domain-form.component.ts b/src/app/domains/domain-form.component.ts index c0e0cab14..4bb56f570 100644 --- a/src/app/domains/domain-form.component.ts +++ b/src/app/domains/domain-form.component.ts @@ -331,7 +331,6 @@ export class DomainFormComponent extends AsyncHandler implements OnInit { await submit(this.form, async () => { const item = this._data.item; this.loading.set(i18n(`${this._name}.SAVING`)); - this._dialog_ref.disableClose = true; const item_json = item.toJSON ? item.toJSON() : item; const form_item = ( item.id @@ -347,13 +346,11 @@ export class DomainFormComponent extends AsyncHandler implements OnInit { form_item as unknown as PlaceDomain, ) : addDomain(form_item as unknown as PlaceDomain)); - this._dialog_ref.disableClose = false; this.event.emit({ reason: 'done', metadata: { item: _item } }); notifySuccess(i18n(`${this._name}.SAVE_SUCCESS`)); this._dialog_ref.close(); } catch (err) { this.loading.set(null); - this._dialog_ref.disableClose = false; notifyError( i18n(`${this._name}.SAVE_ERROR`, { error: await readError(err), diff --git a/src/app/domains/domain-state.service.ts b/src/app/domains/domain-state.service.ts index 5bf3636d1..9776dc1d6 100644 --- a/src/app/domains/domain-state.service.ts +++ b/src/app/domains/domain-state.service.ts @@ -34,6 +34,7 @@ import { filter, map } from 'rxjs'; import { describeError } from '../common/errors'; import { escapeHtml } from '../common/general'; import { ActiveItemService } from '../common/item.service'; +import { reportLoadError } from '../common/load-error'; import { i18n } from '../common/locale.service'; import { notifyError, notifySuccess } from '../common/notifications'; import { waitForEvent } from '../common/signals'; @@ -116,7 +117,7 @@ export class DomainStateService { const response = await queryUsers({ authority_id: params.id, limit: params.full ? 1000 : 1, - }).catch(() => ({ data: [] as PlaceUser[], total: 0 })); + }).catch(reportLoadError({ data: [] as PlaceUser[], total: 0 })); return { ...response, data: [...response.data].sort((a, b) => @@ -179,7 +180,9 @@ export class DomainStateService { const response = await queryApplications({ authority_id: params.id, ...(params.full ? {} : { limit: 1 }), - }).catch(() => ({ data: [] as PlaceApplication[], total: 0 })); + }).catch( + reportLoadError({ data: [] as PlaceApplication[], total: 0 }), + ); return { ...response, data: [...response.data].sort((a, b) => diff --git a/src/app/domains/domains.component.ts b/src/app/domains/domains.component.ts index caad62351..e3819e064 100644 --- a/src/app/domains/domains.component.ts +++ b/src/app/domains/domains.component.ts @@ -24,13 +24,13 @@ import { DomainStateService } from './domain-state.service'; >