From 3621cf4833088a99f7f89ee8b57f55dd4e77c2ea Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 6 Sep 2022 16:42:05 +0200 Subject: [PATCH 1/2] Translation status column. (#918) --- backend/i18n/frontend_en.json | 24 +++++---- backend/i18n/frontend_it.json | 24 +++++---- backend/i18n/frontend_nl.json | 24 +++++---- backend/i18n/frontend_zh.json | 24 +++++---- backend/i18n/source/frontend_en.json | 24 +++++---- backend/i18n/source/frontend_it.json | 21 ++++---- backend/i18n/source/frontend_nl.json | 21 ++++---- backend/i18n/source/frontend_zh.json | 21 ++++---- .../Squidex.Translator/Processes/Helper.cs | 19 +++---- .../content-references.component.html | 5 +- .../contents/contents-page.component.html | 2 + .../shared/list/content.component.html | 8 +-- .../content/shared/list/content.component.ts | 10 +++- .../references/reference-item.component.html | 16 +++++- .../references/reference-item.component.ts | 9 ++-- .../references-editor.component.html | 1 + .../pages/plans/plans-page.component.html | 2 +- .../content-list-field.component.html | 16 ++++++ .../contents/content-list-field.component.ts | 10 +++- .../translation-status.component.html | 1 + .../translation-status.component.scss | 0 .../contents/translation-status.component.ts | 54 +++++++++++++++++++ .../content-selector-item.component.html | 24 +++++++-- .../content-selector-item.component.ts | 5 +- .../content-selector.component.html | 1 + frontend/src/app/shared/declarations.ts | 1 + frontend/src/app/shared/module.ts | 4 +- .../app/shared/services/contents.service.ts | 2 +- .../app/shared/services/schemas.service.ts | 30 +++++++---- 29 files changed, 274 insertions(+), 129 deletions(-) create mode 100644 frontend/src/app/shared/components/contents/translation-status.component.html create mode 100644 frontend/src/app/shared/components/contents/translation-status.component.scss create mode 100644 frontend/src/app/shared/components/contents/translation-status.component.ts diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 4546beb5c..1278def16 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -85,7 +85,6 @@ "assets.fileTooBig": "Asset is too big.", "assets.folderName": "Folder Name", "assets.folderNameHint": "The folder name is used as a display name and might not be unique.", - "assets.insertAssets": "Insert Assets", "assets.linkSelected": "Link selected assets ({count})", "assets.listPageTitle": "Assets", "assets.loadFailed": "Failed to load assets. Please reload.", @@ -265,6 +264,7 @@ "common.edit": "Edit", "common.editing": "Editing", "common.email": "Email", + "common.empty": "No results.", "common.enable": "Enable", "common.enabled": "Enabled", "common.error": "Error", @@ -484,16 +484,6 @@ "contents.statusQueries": "Status Queries", "contents.stockPhotoSearch": "Search for Photos by Unsplash", "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", - "contents.tableHeaders.created": "Created", - "contents.tableHeaders.createdBy": "Created By", - "contents.tableHeaders.createdByShort": "By", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "Updated", - "contents.tableHeaders.lastModifiedBy": "Updated By", - "contents.tableHeaders.lastModifiedByShort": "By", - "contents.tableHeaders.nextStatus": "Next Status", - "contents.tableHeaders.status": "Status", - "contents.tableHeaders.version": "Version", "contents.unpublishReferrerConfirmText": "The content is referenced by another published content item.\n\nDo you really want to unpublish this content?", "contents.unpublishReferrerConfirmTitle": "Unpublish content", "contents.unsavedChangesText": "You have unsaved changes. Do you want to load them now?", @@ -941,6 +931,18 @@ "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", "schemas.tabFields": "Fields", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "Created", + "schemas.tableHeaders.createdBy": "Created By", + "schemas.tableHeaders.createdByShort": "By", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "Updated", + "schemas.tableHeaders.lastModifiedBy": "Updated By", + "schemas.tableHeaders.lastModifiedByShort": "By", + "schemas.tableHeaders.nextStatus": "Next Status", + "schemas.tableHeaders.status": "Status", + "schemas.tableHeaders.translationStatus": "Translation Status", + "schemas.tableHeaders.translationStatusAverage": "Average Translation Status", + "schemas.tableHeaders.version": "Version", "schemas.tabMore": "More", "schemas.tabScripts": "Scripts", "schemas.tabUI": "UI", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 7905dde29..d392c3fb0 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -85,7 +85,6 @@ "assets.fileTooBig": "La risorsa è troppo grande.", "assets.folderName": "Nome della cartella", "assets.folderNameHint": "Il nome della cartella viene usato solo per la visualizzazione e può non essere univoco.", - "assets.insertAssets": "Inserisci le risorse", "assets.linkSelected": "Collega le risorse selezionate ({count})", "assets.listPageTitle": "Risorse", "assets.loadFailed": "Non è stato possibile caricare le risorse. Per favore ricarica.", @@ -265,6 +264,7 @@ "common.edit": "Modifica", "common.editing": "Editing", "common.email": "Email", + "common.empty": "No results.", "common.enable": "Enable", "common.enabled": "Enabled", "common.error": "Errore", @@ -484,16 +484,6 @@ "contents.statusQueries": "Stato Query", "contents.stockPhotoSearch": "Cerca foto su Unsplash", "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", - "contents.tableHeaders.created": "Creato", - "contents.tableHeaders.createdBy": "Creato da", - "contents.tableHeaders.createdByShort": "Da", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "Modificato", - "contents.tableHeaders.lastModifiedBy": "Modificato da", - "contents.tableHeaders.lastModifiedByShort": "Da", - "contents.tableHeaders.nextStatus": "Stato successivo", - "contents.tableHeaders.status": "Stato", - "contents.tableHeaders.version": "Versione", "contents.unpublishReferrerConfirmText": "Il contenuto è inserito come collegamento da un altro contenuto pubblicato.\n\nSei sicuro di volerlo rimuovere dalla pubblicazione?", "contents.unpublishReferrerConfirmTitle": "Rimuovi dalla pubblicazione il contenuto", "contents.unsavedChangesText": "Non hai salvato le modifiche. Vuoi salvarle adesso?", @@ -941,6 +931,18 @@ "schemas.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.", "schemas.tabFields": "Campi", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "Creato", + "schemas.tableHeaders.createdBy": "Creato da", + "schemas.tableHeaders.createdByShort": "Da", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "Modificato", + "schemas.tableHeaders.lastModifiedBy": "Modificato da", + "schemas.tableHeaders.lastModifiedByShort": "Da", + "schemas.tableHeaders.nextStatus": "Stato successivo", + "schemas.tableHeaders.status": "Stato", + "schemas.tableHeaders.translationStatus": "Translation Status", + "schemas.tableHeaders.translationStatusAverage": "Average Translation Status", + "schemas.tableHeaders.version": "Versione", "schemas.tabMore": "Altro", "schemas.tabScripts": "Script", "schemas.tabUI": "UI", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 1461da72c..90b01cdcf 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -85,7 +85,6 @@ "assets.fileTooBig": "Asset is te groot.", "assets.folderName": "Mapnaam", "assets.folderNameHint": "De mapnaam wordt gebruikt als weergavenaam en mag niet uniek zijn.", - "assets.insertAssets": "Bestanden invoegen", "assets.linkSelected": "Link geselecteerde items ({count})", "assets.listPageTitle": "Bestanden", "assets.loadFailed": "Laden van bestanden is mislukt. Laad opnieuw.", @@ -265,6 +264,7 @@ "common.edit": "Bewerken", "common.editing": "Bewerken", "common.email": "E-mail", + "common.empty": "No results.", "common.enable": "Aanzetten", "common.enabled": "Aangezet", "common.error": "Fout", @@ -484,16 +484,6 @@ "contents.statusQueries": "Statusquery's", "contents.stockPhotoSearch": "Zoeken naar foto's op Unsplash", "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", - "contents.tableHeaders.created": "Gemaakt", - "contents.tableHeaders.createdBy": "Gemaakt door", - "contents.tableHeaders.createdByShort": "Door", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "Bijgewerkt", - "contents.tableHeaders.lastModifiedBy": "Bijgewerkt door", - "contents.tableHeaders.lastModifiedByShort": "Door", - "contents.tableHeaders.nextStatus": "Volgende status", - "contents.tableHeaders.status": "Status", - "contents.tableHeaders.version": "Versie", "contents.unpublishReferrerConfirmText": "Er wordt naar de inhoud verwezen door een ander gepubliceerd inhoudsitem.\n\nWilt u de publicatie van deze inhoud echt ongedaan maken?", "contents.unpublishReferrerConfirmTitle": "Publicatie van inhoud ongedaan maken", "contents.unsavedChangesText": "Je hebt niet-opgeslagen wijzigingen. Wil je ze nu laden?", @@ -941,6 +931,18 @@ "schemas.synchronizeFailed": "Synchroniseren van schema is mislukt. Laad opnieuw.", "schemas.tabFields": "Velden", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "Gemaakt", + "schemas.tableHeaders.createdBy": "Gemaakt door", + "schemas.tableHeaders.createdByShort": "Door", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "Bijgewerkt", + "schemas.tableHeaders.lastModifiedBy": "Bijgewerkt door", + "schemas.tableHeaders.lastModifiedByShort": "Door", + "schemas.tableHeaders.nextStatus": "Volgende status", + "schemas.tableHeaders.status": "Status", + "schemas.tableHeaders.translationStatus": "Translation Status", + "schemas.tableHeaders.translationStatusAverage": "Average Translation Status", + "schemas.tableHeaders.version": "Versie", "schemas.tabMore": "Meer", "schemas.tabScripts": "Scripts", "schemas.tabUI": "UI", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index a153054ff..8f5b761ed 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -85,7 +85,6 @@ "assets.fileTooBig": "资源太大。", "assets.folderName": "文件夹名称", "assets.folderNameHint": "文件夹名称用作显示名称,不能唯一。", - "assets.insertAssets": "插入资源", "assets.linkSelected": "链接选定的资源 ({count})", "assets.listPageTitle": "资源", "assets.loadFailed": "资源加载失败,请重新加载。", @@ -265,6 +264,7 @@ "common.edit": "编辑", "common.editing": "Editing", "common.email": "电子邮件", + "common.empty": "No results.", "common.enable": "Enable", "common.enabled": "已启用", "common.error": "错误", @@ -484,16 +484,6 @@ "contents.statusQueries": "状态查询", "contents.stockPhotoSearch": "通过 Unsplash 搜索照片", "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", - "contents.tableHeaders.created": "创建", - "contents.tableHeaders.createdBy": "创建者", - "contents.tableHeaders.createdByShort": "By", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "更新", - "contents.tableHeaders.lastModifiedBy": "更新者", - "contents.tableHeaders.lastModifiedByShort": "By", - "contents.tableHeaders.nextStatus": "下一个状态", - "contents.tableHeaders.status": "状态", - "contents.tableHeaders.version": "版本", "contents.unpublishReferrerConfirmText": "该内容被另一个已发布的内容项引用。\n\n您真的要取消发布此内容吗?", "contents.unpublishReferrerConfirmTitle": "取消发布内容", "contents.unsavedChangesText": "您有未保存的更改。要立即加载吗?", @@ -941,6 +931,18 @@ "schemas.synchronizeFailed": "同步Schemas失败。请重新加载。", "schemas.tabFields": "字段", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "创建", + "schemas.tableHeaders.createdBy": "创建者", + "schemas.tableHeaders.createdByShort": "By", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "更新", + "schemas.tableHeaders.lastModifiedBy": "更新者", + "schemas.tableHeaders.lastModifiedByShort": "By", + "schemas.tableHeaders.nextStatus": "下一个状态", + "schemas.tableHeaders.status": "状态", + "schemas.tableHeaders.translationStatus": "Translation Status", + "schemas.tableHeaders.translationStatusAverage": "Average Translation Status", + "schemas.tableHeaders.version": "版本", "schemas.tabMore": "More", "schemas.tabScripts": "Scripts", "schemas.tabUI": "UI", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 4546beb5c..1278def16 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -85,7 +85,6 @@ "assets.fileTooBig": "Asset is too big.", "assets.folderName": "Folder Name", "assets.folderNameHint": "The folder name is used as a display name and might not be unique.", - "assets.insertAssets": "Insert Assets", "assets.linkSelected": "Link selected assets ({count})", "assets.listPageTitle": "Assets", "assets.loadFailed": "Failed to load assets. Please reload.", @@ -265,6 +264,7 @@ "common.edit": "Edit", "common.editing": "Editing", "common.email": "Email", + "common.empty": "No results.", "common.enable": "Enable", "common.enabled": "Enabled", "common.error": "Error", @@ -484,16 +484,6 @@ "contents.statusQueries": "Status Queries", "contents.stockPhotoSearch": "Search for Photos by Unsplash", "contents.stockPhotoSearchEmpty": "Use the search bar above to find photos.", - "contents.tableHeaders.created": "Created", - "contents.tableHeaders.createdBy": "Created By", - "contents.tableHeaders.createdByShort": "By", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "Updated", - "contents.tableHeaders.lastModifiedBy": "Updated By", - "contents.tableHeaders.lastModifiedByShort": "By", - "contents.tableHeaders.nextStatus": "Next Status", - "contents.tableHeaders.status": "Status", - "contents.tableHeaders.version": "Version", "contents.unpublishReferrerConfirmText": "The content is referenced by another published content item.\n\nDo you really want to unpublish this content?", "contents.unpublishReferrerConfirmTitle": "Unpublish content", "contents.unsavedChangesText": "You have unsaved changes. Do you want to load them now?", @@ -941,6 +931,18 @@ "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", "schemas.tabFields": "Fields", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "Created", + "schemas.tableHeaders.createdBy": "Created By", + "schemas.tableHeaders.createdByShort": "By", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "Updated", + "schemas.tableHeaders.lastModifiedBy": "Updated By", + "schemas.tableHeaders.lastModifiedByShort": "By", + "schemas.tableHeaders.nextStatus": "Next Status", + "schemas.tableHeaders.status": "Status", + "schemas.tableHeaders.translationStatus": "Translation Status", + "schemas.tableHeaders.translationStatusAverage": "Average Translation Status", + "schemas.tableHeaders.version": "Version", "schemas.tabMore": "More", "schemas.tabScripts": "Scripts", "schemas.tabUI": "UI", diff --git a/backend/i18n/source/frontend_it.json b/backend/i18n/source/frontend_it.json index 08c3852eb..298a02ee0 100644 --- a/backend/i18n/source/frontend_it.json +++ b/backend/i18n/source/frontend_it.json @@ -64,7 +64,6 @@ "assets.fileTooBig": "La risorsa è troppo grande.", "assets.folderName": "Nome della cartella", "assets.folderNameHint": "Il nome della cartella viene usato solo per la visualizzazione e può non essere univoco.", - "assets.insertAssets": "Inserisci le risorse", "assets.linkSelected": "Collega le risorse selezionate ({count})", "assets.listPageTitle": "Risorse", "assets.loadFailed": "Non è stato possibile caricare le risorse. Per favore ricarica.", @@ -401,16 +400,6 @@ "contents.selectionCount": "{count} elementi selezionati", "contents.statusQueries": "Stato Query", "contents.stockPhotoSearch": "Cerca foto su Unsplash", - "contents.tableHeaders.created": "Creato", - "contents.tableHeaders.createdBy": "Creato da", - "contents.tableHeaders.createdByShort": "Da", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "Modificato", - "contents.tableHeaders.lastModifiedBy": "Modificato da", - "contents.tableHeaders.lastModifiedByShort": "Da", - "contents.tableHeaders.nextStatus": "Stato successivo", - "contents.tableHeaders.status": "Stato", - "contents.tableHeaders.version": "Versione", "contents.unpublishReferrerConfirmText": "Il contenuto è inserito come collegamento da un altro contenuto pubblicato.\n\nSei sicuro di volerlo rimuovere dalla pubblicazione?", "contents.unpublishReferrerConfirmTitle": "Rimuovi dalla pubblicazione il contenuto", "contents.unsavedChangesText": "Non hai salvato le modifiche. Vuoi salvarle adesso?", @@ -779,6 +768,16 @@ "schemas.synchronizeFailed": "Non è stato possibile sincronizzare lo schema. Per favore ricarica.", "schemas.tabFields": "Campi", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "Creato", + "schemas.tableHeaders.createdBy": "Creato da", + "schemas.tableHeaders.createdByShort": "Da", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "Modificato", + "schemas.tableHeaders.lastModifiedBy": "Modificato da", + "schemas.tableHeaders.lastModifiedByShort": "Da", + "schemas.tableHeaders.nextStatus": "Stato successivo", + "schemas.tableHeaders.status": "Stato", + "schemas.tableHeaders.version": "Versione", "schemas.tabMore": "Altro", "schemas.tabScripts": "Script", "schemas.tabUI": "UI", diff --git a/backend/i18n/source/frontend_nl.json b/backend/i18n/source/frontend_nl.json index 81a7baf12..72cdb9eec 100644 --- a/backend/i18n/source/frontend_nl.json +++ b/backend/i18n/source/frontend_nl.json @@ -82,7 +82,6 @@ "assets.fileTooBig": "Asset is te groot.", "assets.folderName": "Mapnaam", "assets.folderNameHint": "De mapnaam wordt gebruikt als weergavenaam en mag niet uniek zijn.", - "assets.insertAssets": "Bestanden invoegen", "assets.linkSelected": "Link geselecteerde items ({count})", "assets.listPageTitle": "Bestanden", "assets.loadFailed": "Laden van bestanden is mislukt. Laad opnieuw.", @@ -469,16 +468,6 @@ "contents.selectionCount": "{count} items geselecteerd", "contents.statusQueries": "Statusquery's", "contents.stockPhotoSearch": "Zoeken naar foto's op Unsplash", - "contents.tableHeaders.created": "Gemaakt", - "contents.tableHeaders.createdBy": "Gemaakt door", - "contents.tableHeaders.createdByShort": "Door", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "Bijgewerkt", - "contents.tableHeaders.lastModifiedBy": "Bijgewerkt door", - "contents.tableHeaders.lastModifiedByShort": "Door", - "contents.tableHeaders.nextStatus": "Volgende status", - "contents.tableHeaders.status": "Status", - "contents.tableHeaders.version": "Versie", "contents.unpublishReferrerConfirmText": "Er wordt naar de inhoud verwezen door een ander gepubliceerd inhoudsitem.\n\nWilt u de publicatie van deze inhoud echt ongedaan maken?", "contents.unpublishReferrerConfirmTitle": "Publicatie van inhoud ongedaan maken", "contents.unsavedChangesText": "Je hebt niet-opgeslagen wijzigingen. Wil je ze nu laden?", @@ -901,6 +890,16 @@ "schemas.synchronizeFailed": "Synchroniseren van schema is mislukt. Laad opnieuw.", "schemas.tabFields": "Velden", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "Gemaakt", + "schemas.tableHeaders.createdBy": "Gemaakt door", + "schemas.tableHeaders.createdByShort": "Door", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "Bijgewerkt", + "schemas.tableHeaders.lastModifiedBy": "Bijgewerkt door", + "schemas.tableHeaders.lastModifiedByShort": "Door", + "schemas.tableHeaders.nextStatus": "Volgende status", + "schemas.tableHeaders.status": "Status", + "schemas.tableHeaders.version": "Versie", "schemas.tabMore": "Meer", "schemas.tabScripts": "Scripts", "schemas.tabUI": "UI", diff --git a/backend/i18n/source/frontend_zh.json b/backend/i18n/source/frontend_zh.json index 940015eed..9452bd36d 100644 --- a/backend/i18n/source/frontend_zh.json +++ b/backend/i18n/source/frontend_zh.json @@ -74,7 +74,6 @@ "assets.fileTooBig": "资源太大。", "assets.folderName": "文件夹名称", "assets.folderNameHint": "文件夹名称用作显示名称,不能唯一。", - "assets.insertAssets": "插入资源", "assets.linkSelected": "链接选定的资源 ({count})", "assets.listPageTitle": "资源", "assets.loadFailed": "资源加载失败,请重新加载。", @@ -423,16 +422,6 @@ "contents.selectionCount": "{count} 个选定的项目", "contents.statusQueries": "状态查询", "contents.stockPhotoSearch": "通过 Unsplash 搜索照片", - "contents.tableHeaders.created": "创建", - "contents.tableHeaders.createdBy": "创建者", - "contents.tableHeaders.createdByShort": "By", - "contents.tableHeaders.id": "Id", - "contents.tableHeaders.lastModified": "更新", - "contents.tableHeaders.lastModifiedBy": "更新者", - "contents.tableHeaders.lastModifiedByShort": "By", - "contents.tableHeaders.nextStatus": "下一个状态", - "contents.tableHeaders.status": "状态", - "contents.tableHeaders.version": "版本", "contents.unpublishReferrerConfirmText": "该内容被另一个已发布的内容项引用。\n\n您真的要取消发布此内容吗?", "contents.unpublishReferrerConfirmTitle": "取消发布内容", "contents.unsavedChangesText": "您有未保存的更改。要立即加载吗?", @@ -794,6 +783,16 @@ "schemas.synchronizeFailed": "同步Schemas失败。请重新加载。", "schemas.tabFields": "字段", "schemas.tabJson": "Json", + "schemas.tableHeaders.created": "创建", + "schemas.tableHeaders.createdBy": "创建者", + "schemas.tableHeaders.createdByShort": "By", + "schemas.tableHeaders.id": "Id", + "schemas.tableHeaders.lastModified": "更新", + "schemas.tableHeaders.lastModifiedBy": "更新者", + "schemas.tableHeaders.lastModifiedByShort": "By", + "schemas.tableHeaders.nextStatus": "下一个状态", + "schemas.tableHeaders.status": "状态", + "schemas.tableHeaders.version": "版本", "schemas.ui": "指定的字段", "schemas.ui.unassignedFields": "未分配的字段", "schemas.unpublishFailed": "无法取消发布Schemas。请重新加载。", diff --git a/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs b/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs index 3c3719ac5..1f8b1401f 100644 --- a/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs +++ b/backend/i18n/translator/Squidex.Translator/Processes/Helper.cs @@ -26,9 +26,9 @@ namespace Squidex.Translator.Processes Console.WriteLine("----- CHECKING <{0}> -----", locale); var notTranslated = mainTranslations.Keys.Except(texts.Keys).ToList(); - var notUsed = texts.Keys.Except(mainTranslations.Keys).ToList(); + var notUsing = texts.Keys.Except(mainTranslations.Keys).ToList(); - if (notTranslated.Count > 0 || notUsed.Count > 0) + if (notTranslated.Count > 0 || notUsing.Count > 0) { if (notTranslated.Count > 0) { @@ -42,12 +42,12 @@ namespace Squidex.Translator.Processes } } - if (notUsed.Count > 0) + if (notUsing.Count > 0) { Console.WriteLine(); Console.WriteLine("Translations not used:"); - foreach (var key in notUsed.OrderBy(x => x)) + foreach (var key in notUsing.OrderBy(x => x)) { Console.Write(" * "); Console.WriteLine(key); @@ -90,24 +90,25 @@ namespace Squidex.Translator.Processes public static void CheckUnused(TranslationService service, HashSet translations) { - var notUsed = new SortedSet(); + var notUsing = new SortedSet(); foreach (var key in service.MainTranslations.Keys) { if (!translations.Contains(key) && !key.StartsWith("common.", StringComparison.OrdinalIgnoreCase) && !key.StartsWith("dotnet_", StringComparison.OrdinalIgnoreCase) && - !key.StartsWith("validation.", StringComparison.OrdinalIgnoreCase)) + !key.StartsWith("validation.", StringComparison.OrdinalIgnoreCase) && + !key.StartsWith("rules.simulation.error", StringComparison.OrdinalIgnoreCase)) { - notUsed.Add(key); + notUsing.Add(key); } } - if (notUsed.Count > 0) + if (notUsing.Count > 0) { Console.WriteLine("Translations not used:"); - foreach (var key in notUsed) + foreach (var key in notUsing) { Console.Write(" * "); Console.WriteLine(key); diff --git a/frontend/src/app/features/content/pages/content/references/content-references.component.html b/frontend/src/app/features/content/pages/content/references/content-references.component.html index 967844aff..669290689 100644 --- a/frontend/src/app/features/content/pages/content/references/content-references.component.html +++ b/frontend/src/app/features/content/pages/content/references/content-references.component.html @@ -7,9 +7,10 @@ [columns]="contents | sqxContentsColumns" [isCompact]="false" [isDisabled]="false" + [language]="language" + [languages]="languages" [validations]="(contentsState.validationResults | async)!" - [validityVisible]="true" - [language]="language"> + [validityVisible]="true"> diff --git a/frontend/src/app/features/content/pages/contents/contents-page.component.html b/frontend/src/app/features/content/pages/contents/contents-page.component.html index 341119f37..acbd6b441 100644 --- a/frontend/src/app/features/content/pages/contents/contents-page.component.html +++ b/frontend/src/app/features/content/pages/contents/contents-page.component.html @@ -123,7 +123,9 @@ [cloneable]="contentsState.snapshot.canCreate" (delete)="delete(content)" [language]="language" + [languages]="languages" [link]="[content.id, 'history']" + [schema]="schema" [selected]="isItemSelected(content)" (selectedChange)="selectItem(content, $event)" (statusChange)="changeStatus(content, $event)" diff --git a/frontend/src/app/features/content/shared/list/content.component.html b/frontend/src/app/features/content/shared/list/content.component.html index e217c136f..fc549263a 100644 --- a/frontend/src/app/features/content/shared/list/content.component.html +++ b/frontend/src/app/features/content/shared/list/content.component.html @@ -61,12 +61,14 @@ [fields]="tableSettings" [sqxStopClick]="shouldStop(field)"> + [patchForm]="patchForm?.form" + [schema]="schema"> diff --git a/frontend/src/app/features/content/shared/list/content.component.ts b/frontend/src/app/features/content/shared/list/content.component.ts index a2f75e384..c1a2e72ef 100644 --- a/frontend/src/app/features/content/shared/list/content.component.ts +++ b/frontend/src/app/features/content/shared/list/content.component.ts @@ -6,12 +6,12 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; -import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, TableField, TableSettings } from '@app/shared'; +import { AppLanguageDto, ContentDto, ContentListFieldComponent, ContentsState, ModalModel, PatchContentForm, SchemaDto, TableField, TableSettings } from '@app/shared'; /* tslint:disable: component-selector */ @Component({ - selector: '[sqxContent][language][tableFields][tableSettings]', + selector: '[sqxContent][language][languages][tableFields][schema][tableSettings]', styleUrls: ['./content.component.scss'], templateUrl: './content.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -35,6 +35,12 @@ export class ContentComponent implements OnChanges { @Input() public language!: AppLanguageDto; + @Input() + public languages!: ReadonlyArray; + + @Input() + public schema?: SchemaDto; + @Input() public tableFields!: ReadonlyArray; diff --git a/frontend/src/app/features/content/shared/references/reference-item.component.html b/frontend/src/app/features/content/shared/references/reference-item.component.html index 8adc687aa..32c63be56 100644 --- a/frontend/src/app/features/content/shared/references/reference-item.component.html +++ b/frontend/src/app/features/content/shared/references/reference-item.component.html @@ -4,7 +4,13 @@ - + + @@ -17,7 +23,13 @@ - + + diff --git a/frontend/src/app/features/content/shared/references/reference-item.component.ts b/frontend/src/app/features/content/shared/references/reference-item.component.ts index f913619bd..534202e0d 100644 --- a/frontend/src/app/features/content/shared/references/reference-item.component.ts +++ b/frontend/src/app/features/content/shared/references/reference-item.component.ts @@ -8,10 +8,10 @@ /* tslint:disable: component-selector */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { AppLanguageDto, ContentDto, getContentValue, META_FIELDS } from '@app/shared'; +import { ContentDto, getContentValue, LanguageDto, META_FIELDS } from '@app/shared'; @Component({ - selector: '[sqxReferenceItem][language]', + selector: '[sqxReferenceItem][language][languages]', styleUrls: ['./reference-item.component.scss'], templateUrl: './reference-item.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -26,7 +26,10 @@ export class ReferenceItemComponent implements OnChanges { public clone = new EventEmitter(); @Input() - public language!: AppLanguageDto; + public language!: LanguageDto; + + @Input() + public languages!: ReadonlyArray; @Input() public canRemove?: boolean | null = true; diff --git a/frontend/src/app/features/content/shared/references/references-editor.component.html b/frontend/src/app/features/content/shared/references/references-editor.component.html index b3b56846e..9fa285aeb 100644 --- a/frontend/src/app/features/content/shared/references/references-editor.component.html +++ b/frontend/src/app/features/content/shared/references/references-editor.component.html @@ -28,6 +28,7 @@ [isCompact]="snapshot.isCompact" [isDisabled]="snapshot.isDisabled" [language]="language" + [languages]="languages" (delete)="remove(content)"> diff --git a/frontend/src/app/features/settings/pages/plans/plans-page.component.html b/frontend/src/app/features/settings/pages/plans/plans-page.component.html index c4a210411..1cfcce805 100644 --- a/frontend/src/app/features/settings/pages/plans/plans-page.component.html +++ b/frontend/src/app/features/settings/pages/plans/plans-page.component.html @@ -13,7 +13,7 @@
- {{ 'plans.notPlanOwner' | sqxTranslate }} {{ 'plans.planOwner' |sqxTranslate }}: {{plansState.planOwner | async | sqxUserName}} + {{ 'plans.notPlanOwner' | sqxTranslate }} {{ 'plans.planOwner' | sqxTranslate }}: {{plansState.planOwner | async | sqxUserName}}
diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.html b/frontend/src/app/shared/components/contents/content-list-field.component.html index 1edfbb306..a98362708 100644 --- a/frontend/src/app/shared/components/contents/content-list-field.component.html +++ b/frontend/src/app/shared/components/contents/content-list-field.component.html @@ -20,6 +20,22 @@ {{content.lastModifiedBy | sqxUserNameRef}} + + + + + + + +
diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.ts b/frontend/src/app/shared/components/contents/content-list-field.component.ts index 14ab66b36..a93734cd6 100644 --- a/frontend/src/app/shared/components/contents/content-list-field.component.ts +++ b/frontend/src/app/shared/components/contents/content-list-field.component.ts @@ -7,7 +7,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { ContentDto, FieldValue, getContentValue, LanguageDto, META_FIELDS, StatefulComponent, TableField, TableSettings } from '@app/shared/internal'; +import { ContentDto, FieldValue, getContentValue, LanguageDto, META_FIELDS, SchemaDto, StatefulComponent, TableField, TableSettings } from '@app/shared/internal'; interface State { // The formatted value. @@ -15,7 +15,7 @@ interface State { } @Component({ - selector: 'sqx-content-list-field[content][field][language]', + selector: 'sqx-content-list-field[content][field][language][languages][schema]', styleUrls: ['./content-list-field.component.scss'], templateUrl: './content-list-field.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -38,9 +38,15 @@ export class ContentListFieldComponent extends StatefulComponent implemen @Input() public patchForm?: FormGroup | null; + @Input() + public schema?: SchemaDto; + @Input() public language!: LanguageDto; + @Input() + public languages!: ReadonlyArray; + public get isInlineEditable() { return this.field.rootField?.isInlineEditable === true; } diff --git a/frontend/src/app/shared/components/contents/translation-status.component.html b/frontend/src/app/shared/components/contents/translation-status.component.html new file mode 100644 index 000000000..bcc835519 --- /dev/null +++ b/frontend/src/app/shared/components/contents/translation-status.component.html @@ -0,0 +1 @@ +{{text}} \ No newline at end of file diff --git a/frontend/src/app/shared/components/contents/translation-status.component.scss b/frontend/src/app/shared/components/contents/translation-status.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/contents/translation-status.component.ts b/frontend/src/app/shared/components/contents/translation-status.component.ts new file mode 100644 index 000000000..0f33f4653 --- /dev/null +++ b/frontend/src/app/shared/components/contents/translation-status.component.ts @@ -0,0 +1,54 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { contentTranslationStatus, LanguageDto, SchemaDto } from '@app/shared/internal'; + +@Component({ + selector: 'sqx-translation-status[data][languages][schema]', + styleUrls: ['./translation-status.component.scss'], + templateUrl: './translation-status.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TranslationStatusComponent { + @Input() + public data!: any; + + @Input() + public language?: LanguageDto | null; + + @Input() + public languages!: ReadonlyArray; + + @Input() + public schema?: SchemaDto; + + public text = 'N/A'; + + public ngOnChanges() { + if (!this.schema) { + this.text = 'N/A'; + return; + } + + const status = contentTranslationStatus(this.data, this.schema, this.languages); + + let progress = 0; + + if (this.language) { + progress = status[this.language.iso2Code]; + } else { + for (const value of Object.values(status)) { + progress += value; + } + + progress = Math.round(progress / this.languages.length); + } + + this.text = `${progress || 0} %`; + } +} diff --git a/frontend/src/app/shared/components/references/content-selector-item.component.html b/frontend/src/app/shared/components/references/content-selector-item.component.html index 9d125ef19..051c30103 100644 --- a/frontend/src/app/shared/components/references/content-selector-item.component.html +++ b/frontend/src/app/shared/components/references/content-selector-item.component.html @@ -8,15 +8,33 @@ - + + - + + - + + \ No newline at end of file diff --git a/frontend/src/app/shared/components/references/content-selector-item.component.ts b/frontend/src/app/shared/components/references/content-selector-item.component.ts index 0ecd36a9e..fe15f8f76 100644 --- a/frontend/src/app/shared/components/references/content-selector-item.component.ts +++ b/frontend/src/app/shared/components/references/content-selector-item.component.ts @@ -11,7 +11,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from import { ContentDto, LanguageDto, META_FIELDS, SchemaDto } from '@app/shared/internal'; @Component({ - selector: '[sqxContentSelectorItem][language][schema]', + selector: '[sqxContentSelectorItem][language][languages][schema]', styleUrls: ['./content-selector-item.component.scss'], templateUrl: './content-selector-item.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,6 +31,9 @@ export class ContentSelectorItemComponent { @Input() public language!: LanguageDto; + @Input() + public languages!: ReadonlyArray; + @Input() public schema!: SchemaDto; diff --git a/frontend/src/app/shared/components/references/content-selector.component.html b/frontend/src/app/shared/components/references/content-selector.component.html index d3018fbd9..617971a4e 100644 --- a/frontend/src/app/shared/components/references/content-selector.component.html +++ b/frontend/src/app/shared/components/references/content-selector.component.html @@ -74,6 +74,7 @@ Date: Tue, 6 Sep 2022 18:40:57 +0200 Subject: [PATCH 2/2] Redirect login. (#919) --- frontend/src/app/framework/configurations.ts | 2 +- .../must-be-authenticated.guard.spec.ts | 23 ++++++++------ .../guards/must-be-authenticated.guard.ts | 18 ++++++----- .../must-be-not-authenticated.guard.spec.ts | 23 ++++++++------ .../guards/must-be-not-authenticated.guard.ts | 19 +++++++----- .../interceptors/auth.interceptor.spec.ts | 12 ++++++-- .../shared/interceptors/auth.interceptor.ts | 6 ++-- .../src/app/shared/services/auth.service.ts | 30 +++++++++---------- .../shell/pages/home/home-page.component.ts | 23 +++++++++----- .../pages/internal/profile-menu.component.ts | 2 +- .../shell/pages/login/login-page.component.ts | 6 ++-- .../pages/logout/logout-page.component.ts | 6 ++-- 12 files changed, 105 insertions(+), 65 deletions(-) diff --git a/frontend/src/app/framework/configurations.ts b/frontend/src/app/framework/configurations.ts index 5d39ffc7a..4b1fd7fd3 100644 --- a/frontend/src/app/framework/configurations.ts +++ b/frontend/src/app/framework/configurations.ts @@ -7,7 +7,7 @@ export class UIOptions { constructor( - private readonly value: any, + public readonly value: any, ) { } diff --git a/frontend/src/app/shared/guards/must-be-authenticated.guard.spec.ts b/frontend/src/app/shared/guards/must-be-authenticated.guard.spec.ts index 8d851cc8a..9d80beb4e 100644 --- a/frontend/src/app/shared/guards/must-be-authenticated.guard.spec.ts +++ b/frontend/src/app/shared/guards/must-be-authenticated.guard.spec.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +import { Location } from '@angular/common'; import { Router } from '@angular/router'; import { firstValueFrom, of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; @@ -13,19 +14,25 @@ import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard'; describe('MustBeAuthenticatedGuard', () => { let router: IMock; + let location: IMock; let authService: IMock; - const uiOptions = new UIOptions({ map: { type: 'OSM' } }); - const uiOptionsRedirect = new UIOptions({ map: { type: 'OSM' }, redirectToLogin: true }); + let authGuard: MustBeAuthenticatedGuard; + + const uiOptions = new UIOptions({}); beforeEach(() => { + location = Mock.ofType(); + + location.setup(x => x.path(true)) + .returns(() => '/my-path'); + router = Mock.ofType(); authService = Mock.ofType(); + authGuard = new MustBeAuthenticatedGuard(authService.object, location.object, router.object, uiOptions); }); it('should navigate to default page if not authenticated', async () => { - const authGuard = new MustBeAuthenticatedGuard(uiOptions, authService.object, router.object); - authService.setup(x => x.userChanges) .returns(() => of(null)); @@ -33,12 +40,10 @@ describe('MustBeAuthenticatedGuard', () => { expect(result).toBeFalsy(); - router.verify(x => x.navigate(['']), Times.once()); + router.verify(x => x.navigate([''], { queryParams: { redirectPath: '/my-path' } }), Times.once()); }); it('should return true if authenticated', async () => { - const authGuard = new MustBeAuthenticatedGuard(uiOptions, authService.object, router.object); - authService.setup(x => x.userChanges) .returns(() => of({})); @@ -50,7 +55,7 @@ describe('MustBeAuthenticatedGuard', () => { }); it('should login redirect if redirect enabled', async () => { - const authGuard = new MustBeAuthenticatedGuard(uiOptionsRedirect, authService.object, router.object); + uiOptions.value.redirectToLogin = true; authService.setup(x => x.userChanges) .returns(() => of(null)); @@ -59,6 +64,6 @@ describe('MustBeAuthenticatedGuard', () => { expect(result!).toBeFalsy(); - authService.verify(x => x.loginRedirect(), Times.once()); + authService.verify(x => x.loginRedirect('/my-path'), Times.once()); }); }); diff --git a/frontend/src/app/shared/guards/must-be-authenticated.guard.ts b/frontend/src/app/shared/guards/must-be-authenticated.guard.ts index 2e35129d1..73c3f20b4 100644 --- a/frontend/src/app/shared/guards/must-be-authenticated.guard.ts +++ b/frontend/src/app/shared/guards/must-be-authenticated.guard.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +import { Location } from '@angular/common'; import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { Observable } from 'rxjs'; @@ -14,24 +15,27 @@ import { AuthService } from './../services/auth.service'; @Injectable() export class MustBeAuthenticatedGuard implements CanActivate { - private readonly redirect: boolean; - - constructor(uiOptions: UIOptions, + constructor( private readonly authService: AuthService, + private readonly location: Location, private readonly router: Router, + private readonly uiOptions: UIOptions, ) { - this.redirect = uiOptions.get('redirectToLogin'); } public canActivate(): Observable { + const redirect = this.uiOptions.get('redirectToLogin'); + return this.authService.userChanges.pipe( take(1), tap(user => { if (!user) { - if (this.redirect) { - this.authService.loginRedirect(); + const redirectPath = this.location.path(true); + + if (redirect) { + this.authService.loginRedirect(redirectPath); } else { - this.router.navigate(['']); + this.router.navigate([''], { queryParams: { redirectPath } }); } } }), diff --git a/frontend/src/app/shared/guards/must-be-not-authenticated.guard.spec.ts b/frontend/src/app/shared/guards/must-be-not-authenticated.guard.spec.ts index 2e9af64e2..1bfa95923 100644 --- a/frontend/src/app/shared/guards/must-be-not-authenticated.guard.spec.ts +++ b/frontend/src/app/shared/guards/must-be-not-authenticated.guard.spec.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +import { Location } from '@angular/common'; import { Router } from '@angular/router'; import { firstValueFrom, of } from 'rxjs'; import { IMock, It, Mock, Times } from 'typemoq'; @@ -13,19 +14,25 @@ import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard'; describe('MustBeNotAuthenticatedGuard', () => { let router: IMock; + let location: IMock; let authService: IMock; - const uiOptions = new UIOptions({ map: { type: 'OSM' } }); - const uiOptionsRedirect = new UIOptions({ map: { type: 'OSM' }, redirectToLogin: true }); + let authGuard: MustBeNotAuthenticatedGuard; + + const uiOptions = new UIOptions({}); beforeEach(() => { + location = Mock.ofType(); + + location.setup(x => x.path(true)) + .returns(() => '/my-path'); + router = Mock.ofType(); authService = Mock.ofType(); + authGuard = new MustBeNotAuthenticatedGuard(authService.object, location.object, router.object, uiOptions); }); it('should navigate to app page if authenticated', async () => { - const authGuard = new MustBeNotAuthenticatedGuard(uiOptions, authService.object, router.object); - authService.setup(x => x.userChanges) .returns(() => of({})); @@ -33,12 +40,10 @@ describe('MustBeNotAuthenticatedGuard', () => { expect(result!).toBeFalsy(); - router.verify(x => x.navigate(['app']), Times.once()); + router.verify(x => x.navigate(['app'], { queryParams: { redirectPath: '/my-path' } }), Times.once()); }); it('should return true if not authenticated', async () => { - const authGuard = new MustBeNotAuthenticatedGuard(uiOptions, authService.object, router.object); - authService.setup(x => x.userChanges) .returns(() => of(null)); @@ -50,7 +55,7 @@ describe('MustBeNotAuthenticatedGuard', () => { }); it('should login redirect and return false if redirect enabled', async () => { - const authGuard = new MustBeNotAuthenticatedGuard(uiOptionsRedirect, authService.object, router.object); + uiOptions.value.redirectToLogin = true; authService.setup(x => x.userChanges) .returns(() => of(null)); @@ -59,6 +64,6 @@ describe('MustBeNotAuthenticatedGuard', () => { expect(result).toBeFalsy(); - authService.verify(x => x.loginRedirect(), Times.once()); + authService.verify(x => x.loginRedirect('/my-path'), Times.once()); }); }); diff --git a/frontend/src/app/shared/guards/must-be-not-authenticated.guard.ts b/frontend/src/app/shared/guards/must-be-not-authenticated.guard.ts index d7f54e255..da647c97f 100644 --- a/frontend/src/app/shared/guards/must-be-not-authenticated.guard.ts +++ b/frontend/src/app/shared/guards/must-be-not-authenticated.guard.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +import { Location } from '@angular/common'; import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { Observable } from 'rxjs'; @@ -14,25 +15,29 @@ import { AuthService } from './../services/auth.service'; @Injectable() export class MustBeNotAuthenticatedGuard implements CanActivate { - private readonly redirect: boolean; - constructor(uiOptions: UIOptions, + constructor( private readonly authService: AuthService, + private readonly location: Location, private readonly router: Router, + private readonly uiOptions: UIOptions, ) { - this.redirect = uiOptions.get('redirectToLogin'); } public canActivate(): Observable { + const redirect = this.uiOptions.get('redirectToLogin'); + return this.authService.userChanges.pipe( take(1), tap(user => { - if (this.redirect) { - this.authService.loginRedirect(); + const redirectPath = this.location.path(true); + + if (redirect) { + this.authService.loginRedirect(redirectPath); } else if (user) { - this.router.navigate(['app']); + this.router.navigate(['app'], { queryParams: { redirectPath } }); } }), - map(user => !user && !this.redirect)); + map(user => !user && !redirect)); } } diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.spec.ts b/frontend/src/app/shared/interceptors/auth.interceptor.spec.ts index 28dd2cdcb..7ce74c10b 100644 --- a/frontend/src/app/shared/interceptors/auth.interceptor.spec.ts +++ b/frontend/src/app/shared/interceptors/auth.interceptor.spec.ts @@ -7,6 +7,7 @@ /* eslint-disable deprecation/deprecation */ +import { Location } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; @@ -19,9 +20,15 @@ import { AuthInterceptor } from './auth.interceptor'; describe('AuthInterceptor', () => { let authService: IMock; + let location: IMock; let router: IMock; beforeEach(() => { + location = Mock.ofType(); + + location.setup(x => x.path()) + .returns(() => '/my-path'); + authService = Mock.ofType(AuthService); router = Mock.ofType(); @@ -32,6 +39,7 @@ describe('AuthInterceptor', () => { ], providers: [ { provide: Router, useFactory: () => router.object }, + { provide: Location, useFactory: () => location.object }, { provide: AuthService, useValue: authService.object }, { provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }, { @@ -100,7 +108,7 @@ describe('AuthInterceptor', () => { expect().nothing(); - authService.verify(x => x.logoutRedirect(), Times.once()); + authService.verify(x => x.logoutRedirect('/my-path'), Times.once()); })); const AUTH_ERRORS = [403]; @@ -137,7 +145,7 @@ describe('AuthInterceptor', () => { expect().nothing(); - authService.verify(x => x.logoutRedirect(), Times.never()); + authService.verify(x => x.logoutRedirect('/my-path'), Times.never()); })); }); }); diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.ts b/frontend/src/app/shared/interceptors/auth.interceptor.ts index 92c71d6cc..e7b52ed14 100644 --- a/frontend/src/app/shared/interceptors/auth.interceptor.ts +++ b/frontend/src/app/shared/interceptors/auth.interceptor.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +import { Location } from '@angular/common'; import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; @@ -19,6 +20,7 @@ export class AuthInterceptor implements HttpInterceptor { constructor(apiUrlConfig: ApiUrlConfig, private readonly authService: AuthService, + private readonly location: Location, private readonly router: Router, ) { this.baseUrl = apiUrlConfig.buildUrl(''); @@ -50,7 +52,7 @@ export class AuthInterceptor implements HttpInterceptor { if (error.status === 401 && renew) { return this.authService.loginSilent().pipe( catchError(() => { - this.authService.logoutRedirect(); + this.authService.logoutRedirect(this.location.path()); return EMPTY; }), @@ -58,7 +60,7 @@ export class AuthInterceptor implements HttpInterceptor { } else if (error.status === 401 || error.status === 403) { if (req.method === 'GET') { if (error.status === 401) { - this.authService.logoutRedirect(); + this.authService.logoutRedirect(this.location.path()); } else { this.router.navigate(['/forbidden'], { replaceUrl: true }); } diff --git a/frontend/src/app/shared/services/auth.service.ts b/frontend/src/app/shared/services/auth.service.ts index d9c23f52f..ce1998410 100644 --- a/frontend/src/app/shared/services/auth.service.ts +++ b/frontend/src/app/shared/services/auth.service.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { Log, User, UserManager, WebStorageStateStore } from 'oidc-client'; -import { concat, Observable, Observer, of, ReplaySubject, throwError, TimeoutError } from 'rxjs'; +import { concat, Observable, of, ReplaySubject, throwError, TimeoutError } from 'rxjs'; import { delay, mergeMap, retryWhen, take, timeout } from 'rxjs/operators'; import { ApiUrlConfig, Types } from '@app/framework'; @@ -124,19 +124,19 @@ export class AuthService { this.checkState(this.userManager.getUser()); } - public logoutRedirect() { - this.userManager.signoutRedirect(); + public logoutRedirect(redirectPath: string) { + this.userManager.signoutRedirect({ state: { redirectPath } }); } - public loginRedirect() { - this.userManager.signinRedirect(); + public loginRedirect(redirectPath: string) { + this.userManager.signinRedirect({ state: { redirectPath } }); } - public logoutRedirectComplete(): Observable { - return new Observable((observer: Observer) => { + public logoutRedirectComplete(): Observable { + return new Observable(observer => { this.userManager.signoutRedirectCallback() .then(x => { - observer.next(x); + observer.next(x.state?.redirectPath); observer.complete(); }, err => { observer.error(err); @@ -145,11 +145,11 @@ export class AuthService { }); } - public loginPopup(): Observable { - return new Observable(observer => { - this.userManager.signinPopup() + public loginPopup(redirectPath: string): Observable { + return new Observable(observer => { + this.userManager.signinPopup({ state: { redirectPath } }) .then(x => { - observer.next(AuthService.createProfile(x)); + observer.next(x.state?.redirectPath); observer.complete(); }, err => { observer.error(err); @@ -158,11 +158,11 @@ export class AuthService { }); } - public loginRedirectComplete(): Observable { - return new Observable(observer => { + public loginRedirectComplete(): Observable { + return new Observable(observer => { this.userManager.signinRedirectCallback() .then(x => { - observer.next(AuthService.createProfile(x)); + observer.next(x.state?.redirectPath); observer.complete(); }, err => { observer.error(err); diff --git a/frontend/src/app/shell/pages/home/home-page.component.ts b/frontend/src/app/shell/pages/home/home-page.component.ts index 0159e29c7..e86c6c7c0 100644 --- a/frontend/src/app/shell/pages/home/home-page.component.ts +++ b/frontend/src/app/shell/pages/home/home-page.component.ts @@ -5,8 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +import { Location } from '@angular/common'; import { Component } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { AuthService } from '@app/shared'; @Component({ @@ -19,18 +20,26 @@ export class HomePageComponent { constructor( private readonly authService: AuthService, + private readonly location: Location, + private readonly route: ActivatedRoute, private readonly router: Router, ) { } public login() { + const redirectPath = + this.route.snapshot.queryParams.redirectPath || + this.location.path(); + if (this.isIE()) { - this.authService.loginRedirect(); + this.authService.loginRedirect(redirectPath); } else { - this.authService.loginPopup() + this.authService.loginPopup(redirectPath) .subscribe({ - next: () => { - this.router.navigate(['/app']); + next: path => { + path ||= '/app'; + + this.router.navigateByUrl(path); }, error: () => { this.showLoginError = true; @@ -40,8 +49,6 @@ export class HomePageComponent { } public isIE() { - const isIE = !!navigator.userAgent.match(/Trident/g) || !!navigator.userAgent.match(/MSIE/g); - - return isIE; + return !!navigator.userAgent.match(/Trident/g) || !!navigator.userAgent.match(/MSIE/g); } } diff --git a/frontend/src/app/shell/pages/internal/profile-menu.component.ts b/frontend/src/app/shell/pages/internal/profile-menu.component.ts index e59708c79..bfd251809 100644 --- a/frontend/src/app/shell/pages/internal/profile-menu.component.ts +++ b/frontend/src/app/shell/pages/internal/profile-menu.component.ts @@ -93,6 +93,6 @@ export class ProfileMenuComponent extends StatefulComponent implements On } public logout() { - this.authService.logoutRedirect(); + this.authService.logoutRedirect('/'); } } diff --git a/frontend/src/app/shell/pages/login/login-page.component.ts b/frontend/src/app/shell/pages/login/login-page.component.ts index 81149e8e2..fbb00da25 100644 --- a/frontend/src/app/shell/pages/login/login-page.component.ts +++ b/frontend/src/app/shell/pages/login/login-page.component.ts @@ -23,8 +23,10 @@ export class LoginPageComponent implements OnInit { public ngOnInit() { this.authService.loginRedirectComplete() .subscribe({ - next: () => { - this.router.navigate(['/app'], { replaceUrl: true }); + next: path => { + path ||= '/app'; + + this.router.navigateByUrl(path, { replaceUrl: true }); }, error: () => { this.router.navigate(['/'], { replaceUrl: true }); diff --git a/frontend/src/app/shell/pages/logout/logout-page.component.ts b/frontend/src/app/shell/pages/logout/logout-page.component.ts index bdb7fa7c9..d4357f537 100644 --- a/frontend/src/app/shell/pages/logout/logout-page.component.ts +++ b/frontend/src/app/shell/pages/logout/logout-page.component.ts @@ -23,8 +23,10 @@ export class LogoutPageComponent implements OnInit { public ngOnInit() { this.authService.logoutRedirectComplete() .subscribe({ - next: () => { - this.router.navigate(['/'], { replaceUrl: true }); + next: path => { + path ||= '/'; + + this.router.navigateByUrl(path, { replaceUrl: true }); }, error: () => { this.router.navigate(['/'], { replaceUrl: true });