Browse Source

Add new buttons. (#1124)

* Add new buttons.

* Harden tests

* Make screenshots.

* Enable screenshots.

* Fix close?
pull/1127/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
a746d23487
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      backend/i18n/frontend_en.json
  2. 8
      backend/i18n/frontend_fr.json
  3. 8
      backend/i18n/frontend_it.json
  4. 8
      backend/i18n/frontend_nl.json
  5. 8
      backend/i18n/frontend_pt.json
  6. 8
      backend/i18n/frontend_zh.json
  7. 14
      backend/i18n/source/frontend_en.json
  8. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs
  9. 8
      backend/src/Squidex.Infrastructure/Json/System/PolymorphicConverter.cs
  10. 4
      backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs
  11. 2
      backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs
  12. 5
      backend/src/Squidex.Infrastructure/ThrowHelper.cs
  13. 1
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  14. 9
      frontend/src/app/features/apps/pages/app.component.html
  15. 9
      frontend/src/app/features/apps/pages/team.component.html
  16. 68
      frontend/src/app/features/content/pages/content/content-page.component.html
  17. 88
      frontend/src/app/features/content/pages/content/content-page.component.ts
  18. 3
      frontend/src/app/features/content/shared/references/reference-item.component.html
  19. 2
      frontend/src/app/features/rules/pages/rules/rule.component.html
  20. 68
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html
  21. 6
      frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.ts
  22. 2
      frontend/src/app/framework/angular/modals/dialog-renderer.component.html
  23. 20
      frontend/src/app/framework/angular/pager.component.html
  24. 13
      frontend/src/app/framework/services/localizer.service.ts
  25. 8
      frontend/src/app/shared/components/assets/asset-folder.component.html
  26. 1
      frontend/src/environments/environment.prod.ts
  27. 1
      frontend/src/environments/environment.ts
  28. 4
      frontend/src/main.ts
  29. 3
      tools/e2e/playwright.config.ts
  30. 3
      tools/e2e/tests/given-app/rules.spec.ts
  31. 2
      tools/e2e/tests/given-app/schemas.spec.ts
  32. 2
      tools/e2e/tests/given-schema/_setup.ts
  33. 104
      tools/e2e/tests/given-schema/contents.spec.ts

14
backend/i18n/frontend_en.json

@ -310,6 +310,7 @@
"common.more": "More",
"common.name": "Name",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "No",
"common.nothingChanged": "Nothing has been changed.",
"common.notSupported": "Not Supported",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
"common.pageSize": "Page Size",
"common.password": "Password",
"common.passwordConfirm": "Confirm Password",
"common.pattern": "Pattern",
@ -328,6 +330,7 @@
"common.permissions": "Permissions",
"common.prev": "Back",
"common.preview": "Preview",
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.queryOperators.contains": "contains",
@ -359,6 +362,9 @@
"common.rules": "Rules",
"common.sampleCodeLabel": "Sample Code at",
"common.save": "Save",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Schema",
"common.schemas": "Schemas",
"common.search": "Search",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "Do you really want to remove the content?",
"contents.removeConfirmTitle": "Remove content",
"contents.saveAndPublish": "Save and Publish",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "at",
"contents.scheduledBy": "by",
"contents.scheduledTo": "to",
@ -785,9 +793,9 @@
"rules.updateFailed": "Failed to update rule. Please reload.",
"rules.when": "When",
"schemas.addField": "Add Field",
"schemas.addFieldAndClose": "Create and close",
"schemas.addFieldAndCreate": "Create and add field",
"schemas.addFieldAndEdit": "Create and edit field",
"schemas.addFieldAndClose": "Create",
"schemas.addFieldAndCreate": "Create & add another",
"schemas.addFieldAndEdit": "Create & edit properties",
"schemas.addFieldButton": "Add Field",
"schemas.addFieldFailed": "Failed to add field. Please reload.",
"schemas.addNestedField": "Add Nested Field",

8
backend/i18n/frontend_fr.json

@ -310,6 +310,7 @@
"common.more": "Plus",
"common.name": "Nom",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "Non",
"common.nothingChanged": "Rien n'a été changé.",
"common.notSupported": "Non supporté",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} Du total?",
"common.pagerReload": "Cliquez pour recharger la vue et obtenir le nombre total d'articles",
"common.pageSize": "Page Size",
"common.password": "Mot de passe",
"common.passwordConfirm": "Confirmez le mot de passe",
"common.pattern": "Modèle",
@ -328,6 +330,7 @@
"common.permissions": "Autorisations",
"common.prev": "Back",
"common.preview": "Aperçu",
"common.prevPage": "Previous Page",
"common.product": "CMS sans tête Squidex",
"common.project": "Projet",
"common.queryOperators.contains": "contient",
@ -359,6 +362,9 @@
"common.rules": "Règles",
"common.sampleCodeLabel": "Exemple de code à",
"common.save": "Sauvegarder",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Schéma",
"common.schemas": "Schémas",
"common.search": "Recherche",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "Voulez-vous vraiment supprimer le contenu\u00A0?",
"contents.removeConfirmTitle": "Supprimer le contenu",
"contents.saveAndPublish": "Enregistrer et publier",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "à",
"contents.scheduledBy": "par",
"contents.scheduledTo": "pour",

8
backend/i18n/frontend_it.json

@ -310,6 +310,7 @@
"common.more": "More",
"common.name": "Nome",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "No",
"common.nothingChanged": "Non è stato cambiato niente.",
"common.notSupported": "Not Supported",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
"common.pageSize": "Page Size",
"common.password": "Password",
"common.passwordConfirm": "Conferma Password",
"common.pattern": "Pattern",
@ -328,6 +330,7 @@
"common.permissions": "Permessi",
"common.prev": "Back",
"common.preview": "Anteprima",
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Progetto",
"common.queryOperators.contains": "contiene",
@ -359,6 +362,9 @@
"common.rules": "Regole",
"common.sampleCodeLabel": "Esempio di codice per",
"common.save": "Salva",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Schema",
"common.schemas": "Schemi",
"common.search": "Search",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "Sei sicuro di voler rimuovere il contenuto?",
"contents.removeConfirmTitle": "Cancella il contenuto",
"contents.saveAndPublish": "Salva e pubblica",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "alle",
"contents.scheduledBy": "by",
"contents.scheduledTo": "a",

8
backend/i18n/frontend_nl.json

@ -310,6 +310,7 @@
"common.more": "Meer",
"common.name": "Naam",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "Nee",
"common.nothingChanged": "Er is niets veranderd.",
"common.notSupported": "Not Supported",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst} - {itemLast} van {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
"common.pageSize": "Page Size",
"common.password": "Wachtwoord",
"common.passwordConfirm": "Bevestig wachtwoord",
"common.pattern": "Patroon",
@ -328,6 +330,7 @@
"common.permissions": "Rechten",
"common.prev": "Back",
"common.preview": "Preview",
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.queryOperators.contains": "bevat",
@ -359,6 +362,9 @@
"common.rules": "Regels",
"common.sampleCodeLabel": "Voorbeeldcode bij",
"common.save": "Opslaan",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Schema",
"common.schemas": "Schema's",
"common.search": "Zoeken",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "Wil je de inhoud echt verwijderen?",
"contents.removeConfirmTitle": "Verwijder inhoud",
"contents.saveAndPublish": "Opslaan en publiceren",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "bij",
"contents.scheduledBy": "door",
"contents.scheduledTo": "naar",

8
backend/i18n/frontend_pt.json

@ -310,6 +310,7 @@
"common.more": "Mais",
"common.name": "Nome",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "Não",
"common.nothingChanged": "Nada foi mudado.",
"common.notSupported": "Não suportado",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst}-{itemLast} de {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} do total?",
"common.pagerReload": "Clique para recarregar a vista e obter o número total de itens",
"common.pageSize": "Page Size",
"common.password": "Password",
"common.passwordConfirm": "Confirmar Password",
"common.pattern": "Padrão",
@ -328,6 +330,7 @@
"common.permissions": "Permissões",
"common.prev": "Back",
"common.preview": "Previsualizar",
"common.prevPage": "Previous Page",
"common.product": "CMS Headless Squidex",
"common.project": "Projeto",
"common.queryOperators.contains": "contém",
@ -359,6 +362,9 @@
"common.rules": "Regras",
"common.sampleCodeLabel": "Código de amostra em",
"common.save": "Salvar",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Esquema",
"common.schemas": "Esquemas",
"common.search": "Pesquisar",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "Quer mesmo remover o conteúdo?",
"contents.removeConfirmTitle": "Remover conteúdo",
"contents.saveAndPublish": "Salvar e Publicar",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "em",
"contents.scheduledBy": "por",
"contents.scheduledTo": "Para",

8
backend/i18n/frontend_zh.json

@ -310,6 +310,7 @@
"common.more": "More",
"common.name": "名称",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "不",
"common.nothingChanged": "什么都没有改变。",
"common.notSupported": "Not Supported",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst}-{itemLast} 的 {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
"common.pageSize": "Page Size",
"common.password": "密码",
"common.passwordConfirm": "确认密码",
"common.pattern": "模式",
@ -328,6 +330,7 @@
"common.permissions": "权限",
"common.prev": "Back",
"common.preview": "预览",
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "项目",
"common.queryOperators.contains": "包含",
@ -359,6 +362,9 @@
"common.rules": "规则",
"common.sampleCodeLabel": "示例代码在",
"common.save": "保存",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Schema",
"common.schemas": "Schemas",
"common.search": "搜索",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "您真的要删除内容吗?",
"contents.removeConfirmTitle": "删除内容",
"contents.saveAndPublish": "保存并发布",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "at",
"contents.scheduledBy": "by",
"contents.scheduledTo": "to",

14
backend/i18n/source/frontend_en.json

@ -310,6 +310,7 @@
"common.more": "More",
"common.name": "Name",
"common.next": "Continue",
"common.nextPage": "Next Page",
"common.no": "No",
"common.nothingChanged": "Nothing has been changed.",
"common.notSupported": "Not Supported",
@ -320,6 +321,7 @@
"common.pagerInfo": "{itemFirst}-{itemLast} of {numberOfItems}",
"common.pagerInfoNoTotal": "{itemFirst}-{itemLast} of total?",
"common.pagerReload": "Click to reload view and get total number of items",
"common.pageSize": "Page Size",
"common.password": "Password",
"common.passwordConfirm": "Confirm Password",
"common.pattern": "Pattern",
@ -328,6 +330,7 @@
"common.permissions": "Permissions",
"common.prev": "Back",
"common.preview": "Preview",
"common.prevPage": "Previous Page",
"common.product": "Squidex Headless CMS",
"common.project": "Project",
"common.queryOperators.contains": "contains",
@ -359,6 +362,9 @@
"common.rules": "Rules",
"common.sampleCodeLabel": "Sample Code at",
"common.save": "Save",
"common.saveAdd": "Save & add another",
"common.saveClose": "Save & close",
"common.saveEdit": "Save & editor",
"common.schema": "Schema",
"common.schemas": "Schemas",
"common.search": "Search",
@ -480,6 +486,8 @@
"contents.removeConfirmText": "Do you really want to remove the content?",
"contents.removeConfirmTitle": "Remove content",
"contents.saveAndPublish": "Save and Publish",
"contents.saveAndPublishAdd": "Save and Publish & add another",
"contents.saveAndPublishClose": "Save and Publish & close",
"contents.scheduledAt": "at",
"contents.scheduledBy": "by",
"contents.scheduledTo": "to",
@ -785,9 +793,9 @@
"rules.updateFailed": "Failed to update rule. Please reload.",
"rules.when": "When",
"schemas.addField": "Add Field",
"schemas.addFieldAndClose": "Create and close",
"schemas.addFieldAndCreate": "Create and add field",
"schemas.addFieldAndEdit": "Create and edit field",
"schemas.addFieldAndClose": "Create",
"schemas.addFieldAndCreate": "Create & add another",
"schemas.addFieldAndEdit": "Create & edit properties",
"schemas.addFieldButton": "Add Field",
"schemas.addFieldFailed": "Failed to add field. Please reload.",
"schemas.addNestedField": "Add Nested Field",

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs

@ -27,7 +27,8 @@ public sealed class ContentFieldDataConverter : JsonConverter<ContentFieldData>
if (!reader.Read())
{
throw new JsonException("Unexpected end when reading Object.");
ThrowHelper.JsonSystemException("Unexpected end when reading Object.");
return result;
}
var value = JsonSerializer.Deserialize<JsonValue>(ref reader, options)!;
@ -48,7 +49,8 @@ public sealed class ContentFieldDataConverter : JsonConverter<ContentFieldData>
}
}
throw new JsonException("Unexpected end when reading Object.");
ThrowHelper.JsonSystemException("Unexpected end when reading Object.");
return result;
}
public override void Write(Utf8JsonWriter writer, ContentFieldData value, JsonSerializerOptions options)

8
backend/src/Squidex.Infrastructure/Json/System/PolymorphicConverter.cs

@ -63,7 +63,7 @@ public sealed class PolymorphicConverter<T> : JsonConverter<T> where T : class
if (typeReader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
ThrowHelper.JsonSystemException($"Expected Object, got '{reader.TokenType}'");
}
while (typeReader.Read())
@ -75,7 +75,7 @@ public sealed class PolymorphicConverter<T> : JsonConverter<T> where T : class
if (typeReader.TokenType != JsonTokenType.String)
{
ThrowHelper.JsonException($"Expected string discriminator value, got '{reader.TokenType}'");
ThrowHelper.JsonSystemException($"Expected string discriminator value, got '{reader.TokenType}'");
return default!;
}
@ -94,7 +94,7 @@ public sealed class PolymorphicConverter<T> : JsonConverter<T> where T : class
}
}
ThrowHelper.JsonException($"Object has no discriminator '{discriminatorName}'.");
ThrowHelper.JsonSystemException($"Object has no discriminator '{discriminatorName}'.");
return default!;
}
@ -114,7 +114,7 @@ public sealed class PolymorphicConverter<T> : JsonConverter<T> where T : class
{
if (!typeRegistry.TryGetType<T>(name, out var type))
{
ThrowHelper.JsonException($"Object has invalid discriminator '{name}'.");
ThrowHelper.JsonSystemException($"Object has invalid discriminator '{name}'.");
return default!;
}

4
backend/src/Squidex.Infrastructure/Json/System/StringConverter.cs

@ -43,7 +43,7 @@ public sealed class StringConverter<T> : JsonConverter<T> where T : notnull
}
catch (Exception ex)
{
ThrowHelper.JsonException("Error while converting from string.", ex);
ThrowHelper.JsonSystemException("Error while converting from string.", ex);
return default;
}
@ -53,7 +53,7 @@ public sealed class StringConverter<T> : JsonConverter<T> where T : notnull
return JsonSerializer.Deserialize<T>(ref reader, optionsWithoutSelf);
default:
ThrowHelper.JsonException($"Expected string or object, got {reader.TokenType}.");
ThrowHelper.JsonSystemException($"Expected string or object, got {reader.TokenType}.");
return default;
}
}

2
backend/src/Squidex.Infrastructure/Queries/Json/JsonFilterSurrogate.cs

@ -50,7 +50,7 @@ public sealed class JsonFilterSurrogate : ISurrogate<FilterNode<JsonValue>>
return new CompareFilter<JsonValue>(Path, Op ?? CompareOperator.Equals, Value);
}
ThrowHelper.JsonException(Errors.InvalidJsonStructure());
ThrowHelper.JsonSystemException(Errors.InvalidJsonStructure());
return default!;
}
}

5
backend/src/Squidex.Infrastructure/ThrowHelper.cs

@ -45,4 +45,9 @@ public static class ThrowHelper
{
throw new NotSupportedException(message);
}
public static void JsonSystemException(string? message = null, Exception? ex = null)
{
throw new System.Text.Json.JsonException(message, ex);
}
}

1
backend/src/Squidex.Web/ApiExceptionConverter.cs

@ -8,6 +8,7 @@
using System.Diagnostics;
using System.Security;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure;

9
frontend/src/app/features/apps/pages/app.component.html

@ -29,8 +29,13 @@
</div>
@if (app.canLeave) {
<button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="dropdown.toggle()" sqxStopClick type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<button
class="btn btn-sm btn-text-secondary"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="dropdown.toggle()"
sqxStopClick
type="button">
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">

9
frontend/src/app/features/apps/pages/team.component.html

@ -7,8 +7,13 @@
<a class="link" [routerLink]="['/app/teams', team.id]" sqxStopClick>{{ "common.edit" | sqxTranslate }}</a>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="dropdown.toggle()" sqxStopClick type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<button
class="btn btn-sm btn-text-secondary"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="dropdown.toggle()"
sqxStopClick
type="button">
<i class="icon-dots"></i>
</button>

68
frontend/src/app/features/content/pages/content/content-page.component.html

@ -1,6 +1,6 @@
<sqx-title [message]="schema.displayName" [url]="['..']"></sqx-title>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish('Edit')">
<sqx-layout [hideSidebar]="!content" layout="main">
<ng-container title>
<div class="d-flex align-items-center">
@ -76,8 +76,12 @@
[languages]="languages"
[percents]="contentForm.translationStatus | async"></sqx-language-selector>
@if (content.canDelete) {
<button class="btn btn-outline-secondary ms-2" #buttonOptions (click)="dropdown.toggle()" type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<button
class="btn btn-outline-secondary ms-2"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="dropdown.toggle()"
type="button">
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="dropdown; closeAlways: true">
@ -115,14 +119,60 @@
[percents]="contentForm.translationStatus | async"></sqx-language-selector>
<div sqxTourStep="saveContent">
@if (contentsState.canCreate | async) {
<button class="btn btn-primary ms-2" (click)="save()" type="button">
{{ "common.save" | sqxTranslate }}
</button>
<div class="btn-group ms-2" attr.aria-label="{{ 'common.save' | sqxTranslate }}" role="group">
<button class="btn btn-primary" (click)="saveAsDraft('Edit')" type="button">
{{ "common.save" | sqxTranslate }}
</button>
<button
class="btn btn-primary"
#buttonSave
attr.aria-label="{{ 'common.more' | sqxTranslate }}"
(click)="saveOnlyDropdown.toggle()"
type="button">
<i class="icon-angle-down"></i>
</button>
<sqx-dropdown-menu
position="bottom-end"
scrollY="true"
[sqxAnchoredTo]="buttonSave"
*sqxModal="saveOnlyDropdown; closeAlways: true">
<a class="dropdown-item" (click)="saveAsDraft('Add')">
{{ "common.saveAdd" | sqxTranslate }}
</a>
<a class="dropdown-item" (click)="saveAsDraft('Close')">
{{ "common.saveClose" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
}
@if (contentsState.canCreateAndPublish | async) {
<button class="btn btn-success ms-2" shortcut="CTRL + SHIFT + S" type="submit">
{{ "contents.saveAndPublish" | sqxTranslate }}
</button>
<div class="btn-group ms-2" attr.aria-label="{{ 'contents.saveAndPublish' | sqxTranslate }}" role="group">
<button class="btn btn-success" shortcut="CTRL + SHIFT + S" type="submit">
{{ "contents.saveAndPublish" | sqxTranslate }}
</button>
<button
class="btn btn-success"
#buttonSave
attr.aria-label="{{ 'common.more' | sqxTranslate }}"
(click)="savePublishDropdown.toggle()"
type="button">
<i class="icon-angle-down"></i>
</button>
<sqx-dropdown-menu
position="bottom-end"
scrollY="true"
[sqxAnchoredTo]="buttonSave"
*sqxModal="savePublishDropdown; closeAlways: true">
<a class="dropdown-item" (click)="saveAndPublish('Add')">
{{ "contents.saveAndPublishAdd" | sqxTranslate }}
</a>
<a class="dropdown-item" (click)="saveAndPublish('Close')">
{{ "contents.saveAndPublishClose" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
}
</div>
}

88
frontend/src/app/features/content/pages/content/content-page.component.ts

@ -18,6 +18,8 @@ import { ContentEditorComponent } from './editor/content-editor.component';
import { ContentInspectionComponent } from './inspecting/content-inspection.component';
import { ContentReferencesComponent } from './references/content-references.component';
type SaveNavigationMode = 'Close' | 'Add' | 'Edit';
@Component({
standalone: true,
selector: 'sqx-content-page',
@ -64,6 +66,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
private readonly subscriptions = new Subscriptions();
private readonly mutableContext: Record<string, any>;
private autoSaveKey!: AutoSaveKey;
private autoSaveIgnore = false;
public schema!: SchemaDto;
@ -75,6 +78,8 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
public contentVersion: Version | null = null;
public contentForm!: EditContentForm;
public contentFormCompare: EditContentForm | null = null;
public saveOnlyDropdown = new ModalModel();
public savePublishDropdown = new ModalModel();
public dropdown = new ModalModel();
@ -237,52 +242,61 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
);
}
public saveAndPublish() {
this.saveContent(true);
public saveAndPublish(navigationMode: SaveNavigationMode) {
this.saveContent(true, navigationMode);
}
public save() {
this.saveContent(false);
public saveAsDraft(navigationMode: SaveNavigationMode) {
this.saveContent(false, navigationMode);
}
private saveContent(publish: boolean) {
private saveContent(publish: boolean, navigationMode: SaveNavigationMode) {
const value = this.contentForm.submit();
if (value) {
if (this.content) {
if (!this.content.canUpdate) {
return;
}
if (!value) {
this.contentForm.submitFailed('i18n:contents.contentNotValid', false);
return;
}
this.contentsState.update(this.content, value)
.subscribe({
next: () => {
this.contentForm.submitCompleted({ noReset: true });
},
error: error => {
this.contentForm.submitFailed(error);
},
});
} else {
if (!this.canCreate(publish)) {
return;
}
if (this.content) {
if (!this.content.canUpdate) {
return;
}
this.contentsState.create(value, publish, this.contentId)
.subscribe({
next: content => {
this.contentsState.update(this.content, value)
.subscribe({
next: () => {
this.contentForm.submitCompleted({ noReset: true });
},
error: error => {
this.contentForm.submitFailed(error);
},
});
} else {
if (!this.canCreate(publish)) {
return;
}
this.contentsState.create(value, publish, this.contentId)
.subscribe({
next: content => {
if (navigationMode == 'Edit') {
this.contentForm.submitCompleted({ noReset: true });
this.contentForm.load(content.data, true);
this.router.navigate([content.id, 'history'], { relativeTo: this.route.parent! });
},
error: error => {
this.contentForm.submitFailed(error);
},
});
}
} else {
this.contentForm.submitFailed('i18n:contents.contentNotValid', false);
} else if (navigationMode === 'Close') {
this.autoSaveIgnore = true;
this.router.navigate(['./'], { relativeTo: this.route.parent! });
} else {
this.contentForm = new EditContentForm(this.languages, this.schema, this.schemasState.schemaMap, this.formContext);
}
},
error: error => {
this.contentForm.submitFailed(error);
},
});
}
}
@ -314,9 +328,9 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
public changeLanguage(language: AppLanguageDto) {
this.language = language;
this.updateContext();
this.localStore.set(this.languageKey(), language.iso2Code);
this.updateContext();
}
public checkPendingChangesBeforePreview() {
@ -332,7 +346,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
}
private checkPendingChanges(text: string) {
if (this.content && !this.content.canUpdate) {
if ((this.content && !this.content.canUpdate) || this.autoSaveIgnore) {
return of(true);
}

3
frontend/src/app/features/content/shared/references/reference-item.component.html

@ -48,8 +48,7 @@
<td class="cell-actions">
<div class="reference-edit">
<button class="btn btn-text-secondary" type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<button class="btn btn-text-secondary" attr.aria-label="{{ 'common.options' | sqxTranslate }}" type="button">
<i class="icon-dots"></i>
</button>

2
frontend/src/app/features/rules/pages/rules/rule.component.html

@ -14,8 +14,8 @@
<button
class="btn btn-text-secondary"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="dropdown.toggle()"
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
type="button">
<i class="icon-dots"></i>
</button>

68
frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.html

@ -88,26 +88,64 @@
@if (!editForm) {
<div>
<button class="btn btn-outline-success" (click)="addField(false)" type="button">
{{ "schemas.addFieldAndClose" | sqxTranslate }}
</button>
<button class="btn btn-success ms-2" (click)="addField(true)" type="button">
{{ "schemas.addFieldAndCreate" | sqxTranslate }}
</button>
<button class="btn btn-success ms-2" (click)="addField(false, true)" type="button">
{{ "schemas.addFieldAndEdit" | sqxTranslate }}
</button>
<div class="btn-group ms-2" attr.aria-label="{{ 'schemas.addFieldAndClose' | sqxTranslate }}" role="group">
<button class="btn btn-success" (click)="addField(false)" shortcut="CTRL + SHIFT + S">
{{ "schemas.addFieldAndClose" | sqxTranslate }}
</button>
<button
class="btn btn-success"
#buttonSave
attr.aria-label="{{ 'common.more' | sqxTranslate }}"
(click)="addFieldModal.toggle()"
type="button">
<i class="icon-angle-down"></i>
</button>
<sqx-dropdown-menu
position="top-end"
scrollY="true"
[sqxAnchoredTo]="buttonSave"
*sqxModal="addFieldModal; closeAlways: true">
<a class="dropdown-item" (click)="addField(true)">
{{ "schemas.addFieldAndCreate" | sqxTranslate }}
</a>
<a class="dropdown-item" (click)="addField(false, true)">
{{ "schemas.addFieldAndEdit" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
</div>
}
@if (editForm) {
<div>
<button class="btn btn-success" (click)="save(true)" type="button">
{{ "schemas.saveFieldAndNew" | sqxTranslate }}
</button>
<button class="btn btn-primary ms-2" (click)="save()" type="button">
{{ "schemas.saveFieldAndClose" | sqxTranslate }}
</button>
<div
class="btn-group ms-2"
attr.aria-label="{{ 'schemas.addFieldAndClose' | sqxTranslate }}"
position="top-end"
role="group">
<button class="btn btn-primary" (click)="save(true)" shortcut="CTRL + SHIFT + S">
{{ "schemas.saveFieldAndNew" | sqxTranslate }}
</button>
<button
class="btn btn-primary"
#buttonSave
attr.aria-label="{{ 'common.more' | sqxTranslate }}"
(click)="addFieldModal.toggle()"
type="button">
<i class="icon-angle-down"></i>
</button>
<sqx-dropdown-menu
position="bottom-end"
scrollY="true"
[sqxAnchoredTo]="buttonSave"
*sqxModal="addFieldModal; closeAlways: true">
<a class="dropdown-item" (click)="save()">
{{ "schemas.saveFieldAndClose" | sqxTranslate }}
</a>
</sqx-dropdown-menu>
</div>
</div>
}
</ng-container>

6
frontend/src/app/features/schemas/pages/schema/fields/field-wizard.component.ts

@ -8,7 +8,7 @@
import { AsyncPipe } from '@angular/common';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AddFieldForm, AppSettingsDto, ControlErrorsComponent, createProperties, EditFieldForm, FieldDto, fieldTypes, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, LanguagesState, ModalDialogComponent, RootFieldDto, SchemaDto, SchemasState, TooltipDirective, TranslatePipe, Types } from '@app/shared';
import { AddFieldForm, AppSettingsDto, ControlErrorsComponent, createProperties, DropdownMenuComponent, EditFieldForm, FieldDto, fieldTypes, FocusOnInitDirective, FormAlertComponent, FormErrorComponent, FormHintComponent, LanguagesState, ModalDialogComponent, ModalDirective, ModalModel, ModalPlacementDirective, RootFieldDto, SchemaDto, SchemasState, TooltipDirective, TranslatePipe, Types } from '@app/shared';
import { FieldFormComponent } from './forms/field-form.component';
const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createProperties('String') };
@ -21,6 +21,7 @@ const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createP
imports: [
AsyncPipe,
ControlErrorsComponent,
DropdownMenuComponent,
FieldFormComponent,
FocusOnInitDirective,
FormAlertComponent,
@ -28,6 +29,8 @@ const DEFAULT_FIELD = { name: '', partitioning: 'invariant', properties: createP
FormHintComponent,
FormsModule,
ModalDialogComponent,
ModalDirective,
ModalPlacementDirective,
ReactiveFormsModule,
TooltipDirective,
TranslatePipe,
@ -53,6 +56,7 @@ export class FieldWizardComponent implements OnInit {
public field!: FieldDto;
public addFieldForm = new AddFieldForm();
public addFieldModal = new ModalModel();
public editForm?: EditFieldForm;

2
frontend/src/app/framework/angular/modals/dialog-renderer.component.html

@ -31,7 +31,7 @@
<div class="notification-container notification-container-bottom-right">
@for (notification of snapshot.notifications; track notification) {
<div class="alert alert-dismissible alert-{{ notification.messageType }} shadowed" @fade (click)="close(notification)">
<div class="alert alert-dismissible alert-{{ notification.messageType }} shadowed" @fade (click)="close(notification)" role="alert">
<button class="btn-close" data-dismiss="alert" (dialogClose)="close(notification)" type="button"></button>
<div class="shadow"></div>
<div class="shadowed" [innerHTML]="notification.message | sqxTranslate | sqxMarkdown | sqxSafeHtml"></div>

20
frontend/src/app/framework/angular/pager.component.html

@ -2,7 +2,11 @@
<div class="d-flex align-items-center justify-content-end">
@if (paging) {
<div class="float-end pagination">
<select class="form-select form-select-sm" [ngModel]="paging.pageSize" (ngModelChange)="setPageSize($event)">
<select
class="form-select form-select-sm"
attr.aria-label="{{ 'common.pageSize' | sqxTranslate }}"
[ngModel]="paging.pageSize"
(ngModelChange)="setPageSize($event)">
@for (pageSize of pageSizes; track pageSize) {
<option [ngValue]="pageSize">{{ pageSize }}</option>
}
@ -20,10 +24,20 @@
{{ "common.pagerInfoNoTotal" | sqxTranslate: translationInfo }}
</button>
}
<button class="btn btn-sm btn-text-secondary ms-2" (click)="goPrev()" [disabled]="!canGoPrev" type="button">
<button
class="btn btn-sm btn-text-secondary ms-2"
attr.aria-label="{{ 'common.prevPage' | sqxTranslate }}"
(click)="goPrev()"
[disabled]="!canGoPrev"
type="button">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-sm btn-text-secondary ms-2" (click)="goNext()" [disabled]="!canGoNext" type="button">
<button
class="btn btn-sm btn-text-secondary ms-2"
attr.aria-label="{{ 'common.nextPage' | sqxTranslate }}"
(click)="goNext()"
[disabled]="!canGoNext"
type="button">
<i class="icon-angle-right"></i>
</button>
</span>

13
frontend/src/app/framework/services/localizer.service.ts

@ -10,19 +10,11 @@ import { compareStrings } from '../utils/array-helper';
@Injectable()
export class LocalizerService {
private shouldLog = false;
constructor(
private readonly translations: Object,
) {
}
public logMissingKeys(shouldLog = true) {
this.shouldLog = shouldLog;
return this;
}
public getOrKey(key: string | undefined, args?: any): string {
return this.get(key, args) || key || '';
}
@ -39,11 +31,6 @@ export class LocalizerService {
let text = (this.translations as any)[key];
if (!text) {
if (this.shouldLog && !key.includes(' ')) {
// eslint-disable-next-line no-console
console.warn(`Missing i18n key: ${key}`);
}
return null;
}

8
frontend/src/app/shared/components/assets/asset-folder.component.html

@ -9,8 +9,12 @@
</div>
<div class="col-auto">
@if ((canDelete || canUpdate) && !isDisabled) {
<button class="btn btn-sm btn-text-secondary" #buttonOptions (click)="editDropdown.toggle()" type="button">
<span class="hidden">{{ "common.options" | sqxTranslate }}</span>
<button
class="btn btn-sm btn-text-secondary"
#buttonOptions
attr.aria-label="{{ 'common.options' | sqxTranslate }}"
(click)="editDropdown.toggle()"
type="button">
<i class="icon-dots"></i>
</button>
<sqx-dropdown-menu scrollY="true" [sqxAnchoredTo]="buttonOptions" *sqxModal="editDropdown; closeAlways: true">

1
frontend/src/environments/environment.prod.ts

@ -1,6 +1,5 @@
export const environment = {
production: true,
textLogger: false,
textResolver: () => {
return (window as any)['texts'];
},

1
frontend/src/environments/environment.ts

@ -5,7 +5,6 @@ declare var require: any;
export const environment = {
production: false,
textLogger: true,
textResolver: () => {
const culture = (window as any)['options']?.culture || 'en';

4
frontend/src/main.ts

@ -11,12 +11,12 @@ import { enableProdMode, ErrorHandler } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { ActivatedRouteSnapshot, BaseRouteReuseStrategy, provideRouter, RouteReuseStrategy } from '@angular/router';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { TourService as BaseTourService } from 'ngx-ui-tour-core';
import { APP_ROUTES } from '@app/app.routes';
import { ApiUrlConfig, authInterceptor, buildTasks, cachingInterceptor, DateHelper, GlobalErrorHandler, loadingInterceptor, LocalizerService, TASK_CONFIGURATION, TitlesConfig, TourService, UIOptions } from '@app/shared';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
const options = (window as any)['options'] || {};
@ -77,7 +77,7 @@ function configTitles() {
}
function configLocalizerService() {
return new LocalizerService(environment.textResolver()).logMissingKeys(environment.textLogger);
return new LocalizerService(environment.textResolver());
}
export class AppRouteReuseStrategy extends BaseRouteReuseStrategy {

3
tools/e2e/playwright.config.ts

@ -29,6 +29,9 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Keep the test report small. See https://playwright.dev/docs/api/class-testoptions#test-options-screenshot */
screenshot: 'only-on-failure',
},
/* Configure projects for major browsers */

3
tools/e2e/tests/given-app/rules.spec.ts

@ -2,6 +2,9 @@ import { expect, Page } from '@playwright/test';
import { escapeRegex, getRandomId } from '../utils';
import { test } from './_fixture';
// We have no easy way to identity rules. Therefore run them sequentially.
test.describe.configure({ mode: 'serial' });
test.beforeEach(async ({ page, appName }) => {
await page.goto(`/app/${appName}/rules`);
});

2
tools/e2e/tests/given-app/schemas.spec.ts

@ -68,7 +68,7 @@ async function createRandomField(page: Page) {
await page.getByPlaceholder('Enter field name').fill(fieldName);
// Save field.
await page.getByRole('button', { name: 'Create and close' }).click();
await page.getByTestId('dialog').getByRole('button', { name: 'Create' }).click();
return fieldName;
}

2
tools/e2e/tests/given-schema/_setup.ts

@ -32,7 +32,7 @@ setup('prepare schema', async ({ page, appName }) => {
await page.getByPlaceholder('Enter field name').fill(field.name);
// Save field.
await page.getByRole('button', { name: 'Create and close' }).click();
await page.getByTestId('dialog').getByRole('button', { name: 'Create' }).click();
}
// Publish schema.

104
tools/e2e/tests/given-schema/contents.spec.ts

@ -11,24 +11,45 @@ import { expect, test } from './_fixture';
test.beforeEach(async ({ page, appName, schemaName }) => {
await page.goto(`/app/${appName}/content/${schemaName}`);
await page.getByRole('combobox').selectOption('3: 50');
});
test('create content', async ({ page }) => {
const contentText = await createRandomContent(page);
test('create content and close', async ({ page }) => {
const contentText = await createRandomContent(page, 'SaveAndClose');
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await expect(contentRow).toBeVisible();
});
test('create content as published', async ({ page }) => {
const contentText = await createRandomContent(page, true);
test('create content and edit', async ({ page }) => {
await createRandomContent(page, 'SaveAndEdit');
await expect(page.getByRole('button', { name: /Draft/ })).toBeVisible();
await expect(page.getByLabel('Identity')).toBeVisible();
});
test('create content and add another', async ({ page }) => {
await createRandomContent(page, 'SaveAndAdd');
await expect(page.locator('sqx-field-editor').getByRole('textbox')).toBeEmpty();
});
test('create content as published and edit', async ({ page }) => {
await createRandomContent(page, 'SavePublishAndEdit');
await expect(page.getByRole('button', { name: /Published/ })).toBeVisible();
await expect(page.getByLabel('Identity')).toBeVisible();
});
test('create content as published and close', async ({ page }) => {
const contentText = await createRandomContent(page, 'SavePublishAndClose');
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await expect(contentRow.getByLabel('Published')).toBeVisible();
});
test('update content', async ({ page }) => {
const contentText = await createRandomContent(page);
const contentText = await createRandomContent(page, 'SaveAndClose');
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await contentRow.click();
@ -37,7 +58,7 @@ test('update content', async ({ page }) => {
// Define content value.
await page.locator('sqx-field-editor').getByRole('textbox').fill(contentUpdate);
await saveContent(page, false);
await saveContent(page, 'Save');
await page.getByRole('button', { name: 'Save', exact: true }).click();
@ -67,8 +88,10 @@ const states = [{
}];
states.forEach(({ state, currentState, initialPublished }) => {
const mode: SaveMode = initialPublished ? 'SavePublishAndClose' : 'SaveAndClose';
test(`change content to ${state}`, async ({ dropdown, page }) => {
const contentText = await createRandomContent(page, initialPublished);
const contentText = await createRandomContent(page, mode);
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await contentRow.getByLabel('Options').click();
@ -79,7 +102,7 @@ states.forEach(({ state, currentState, initialPublished }) => {
});
test(`change content to ${state} by checkbox`, async ({ page }) => {
const contentText = await createRandomContent(page, initialPublished);
const contentText = await createRandomContent(page, mode);
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await contentRow.getByRole('checkbox').click();
@ -90,7 +113,7 @@ states.forEach(({ state, currentState, initialPublished }) => {
});
test(`change content to ${state} by detail page`, async ({ page }) => {
const contentText = await createRandomContent(page, initialPublished);
const contentText = await createRandomContent(page, mode);
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await contentRow.click();
@ -103,7 +126,16 @@ states.forEach(({ state, currentState, initialPublished }) => {
});
test('delete content', async ({ dropdown, page }) => {
const contentText = await createRandomContent(page);
await createRandomContent(page, 'SaveAndEdit');
await page.getByLabel('Options').click();
await dropdown.delete();
await expect(page.getByLabel('Identity')).not.toBeVisible();
});
test('delete content by options', async ({ dropdown, page }) => {
const contentText = await createRandomContent(page, 'SaveAndClose');
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await contentRow.getByLabel('Options').click();
@ -113,7 +145,7 @@ test('delete content', async ({ dropdown, page }) => {
});
test('delete content by checkbox', async ({ page }) => {
const contentText = await createRandomContent(page);
const contentText = await createRandomContent(page, 'SaveAndClose');
const contentRow = page.locator('tr', { hasText: escapeRegex(contentText) });
await contentRow.getByRole('checkbox').click();
@ -123,28 +155,56 @@ test('delete content by checkbox', async ({ page }) => {
await expect(contentRow).not.toBeVisible();
});
async function createRandomContent(page: Page, publish = false) {
async function createRandomContent(page: Page, mode: SaveMode) {
const contentText = `content-${getRandomId()}`;
await page.getByRole('button', { name: /New/ }).click();
// Define content value.
await page.locator('sqx-field-editor').getByRole('textbox').fill(contentText);
await saveContent(page, publish);
await saveContent(page, mode);
// Wait for the success alert.
await page.getByLabel('Identity').waitFor({ state: 'visible' });
// Go back
await page.getByLabel('Back').click();
await page.getByRole('alert').getByText('Content created successfully.').waitFor({ state: 'visible' });
return contentText;
}
async function saveContent(page: Page, publish: boolean) {
if (publish) {
await page.getByRole('button', { name: 'Save and Publish', exact: true }).click();
} else {
await page.getByRole('button', { name: 'Save', exact: true }).click();
type SaveMode =
'Save' |
'SaveAndAdd' |
'SaveAndClose' |
'SaveAndEdit' |
'SavePublishAndAdd' |
'SavePublishAndClose' |
'SavePublishAndEdit';
async function saveContent(page: Page, mode: SaveMode) {
switch (mode) {
case 'SaveAndAdd':
await page.getByLabel('Save', { exact: true }).getByLabel('More').click();
await page.getByText('Save & add another').click();
break;
case 'SaveAndClose':
await page.getByLabel('Save', { exact: true }).getByLabel('More').click();
await page.getByText('Save & close').click();
break;
case 'SaveAndEdit':
await page.getByRole('button', { name: 'Save', exact: true }).click();
break;
case 'SavePublishAndAdd':
await page.getByLabel('Save and Publish').getByLabel('More').click();
await page.getByText('Save and Publish & add another').click();
break;
case 'SavePublishAndClose':
await page.getByLabel('Save and Publish').getByLabel('More').click();
await page.getByText('Save and Publish & close').click();
break;
case 'SavePublishAndEdit':
await page.getByRole('button', { name: 'Save and Publish' }).click();
break;
case 'Save':
await page.getByRole('button', { name: 'Save', exact: true }).click();
break;
}
}

Loading…
Cancel
Save