From 70c365be402fa6e00320b2d51b905a10696a00a0 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 3 Aug 2021 11:11:04 +0200 Subject: [PATCH] Feature/calendar (#742) * Calendar view for contents. * More UI fixes. * Another UI fix. * Localization. * More UI fixes. * Fix ID query. * Title for schema name. * Show only references that can be read. * Test fixed * Reference dropdown fix and asset fix. --- backend/i18n/frontend_en.json | 10 +- backend/i18n/frontend_it.json | 8 +- backend/i18n/frontend_nl.json | 8 +- backend/i18n/frontend_zh.json | 8 +- backend/i18n/source/backend_en.json | 1 + backend/i18n/source/frontend_en.json | 10 +- .../Contents/MongoContentCollection.cs | 10 + .../Contents/Operations/QueryScheduled.cs | 34 ++- .../Assets/Queries/AssetQueryParser.cs | 8 +- .../Contents/Queries/ContentQueryParser.cs | 8 +- backend/src/Squidex.Domain.Apps.Entities/Q.cs | 18 +- backend/src/Squidex.Shared/Texts.it.resx | 3 + backend/src/Squidex.Shared/Texts.nl.resx | 3 + backend/src/Squidex.Shared/Texts.resx | 3 + backend/src/Squidex.Shared/Texts.zh.resx | 3 + .../Contents/ContentsController.cs | 10 +- .../Contents/Models/AllContentsByGetDto.cs | 51 +++++ .../Contents/Models/AllContentsByPostDto.cs | 48 ++++ .../Contents/Models/ContentsIdsQueryDto.cs | 22 -- .../Contents/GraphQL/GraphQLQueriesTests.cs | 14 +- frontend/app-config/webpack.config.js | 3 + frontend/app/features/content/declarations.ts | 1 + frontend/app/features/content/module.ts | 6 + .../calendar/calendar-page.component.html | 114 ++++++++++ .../calendar/calendar-page.component.scss | 24 ++ .../pages/calendar/calendar-page.component.ts | 210 ++++++++++++++++++ .../pages/schemas/schemas-page.component.html | 8 + .../shared/forms/field-editor.component.html | 1 + .../content/shared/list/content.component.ts | 4 +- .../references/reference-item.component.html | 2 +- .../references/references-editor.component.ts | 2 +- .../pages/schema/fields/field.component.scss | 1 + .../pages/schemas/schemas-page.component.html | 4 +- .../clients/client-connect-form.component.ts | 2 +- .../editors/date-time-editor.component.ts | 13 +- .../framework/angular/layout.component.html | 4 +- .../angular/modals/modal.directive.ts | 2 +- .../assets/asset-dialog.component.html | 4 +- .../components/assets/asset.component.scss | 11 +- .../content-list-field.component.html | 2 +- .../references/content-selector.component.ts | 2 +- .../reference-dropdown.component.ts | 7 +- .../references/reference-input.component.ts | 9 +- .../references/references-tag-converter.ts | 5 +- .../components/schema-category.component.html | 26 +-- .../components/schema-category.component.scss | 19 +- .../shared/services/contents.service.spec.ts | 8 +- .../app/shared/services/contents.service.ts | 15 +- frontend/app/theme/_bootstrap.scss | 11 +- frontend/app/theme/_mixins.scss | 2 +- frontend/app/theme/_panels2.scss | 4 +- frontend/app/theme/_vars.scss | 2 - frontend/package-lock.json | 28 +++ frontend/package.json | 1 + 54 files changed, 696 insertions(+), 141 deletions(-) create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs delete mode 100644 backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs create mode 100644 frontend/app/features/content/pages/calendar/calendar-page.component.html create mode 100644 frontend/app/features/content/pages/calendar/calendar-page.component.scss create mode 100644 frontend/app/features/content/pages/calendar/calendar-page.component.ts diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index ff6535621..d0a9c692c 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -239,6 +239,7 @@ "common.contributors": "Contributors", "common.create": "Create", "common.created": "Created", + "common.daily": "Daily", "common.dashboard": "Dasboard", "common.date": "Date", "common.dateTimeEditor.local": "Local", @@ -295,6 +296,7 @@ "common.mapHide": "Hide map", "common.mapShow": "Show map", "common.message": "Message", + "common.monthly": "Monthly", "common.more": "More", "common.name": "Name", "common.no": "No", @@ -335,6 +337,7 @@ "common.rules": "Rules", "common.sampleCodeLabel": "Sample Code at", "common.save": "Save", + "common.schema": "Schema", "common.schemas": "Schemas", "common.search": "Search", "common.searchGoogleMaps": "Search Google Maps", @@ -363,6 +366,7 @@ "common.url": "URL", "common.users": "Users", "common.value": "Value", + "common.weekly": "Weekly", "common.width": "Width", "common.workflow": "Workflow", "common.workflows": "Workflows", @@ -385,6 +389,7 @@ "contents.assetsUpload": "Drop files or click", "contents.autotranslate": "Autotranslate from master language", "contents.bulkFailed": "Failed to delete or update content. Please reload.", + "contents.calendar": "Scheduled Contents", "contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.", @@ -445,8 +450,9 @@ "contents.removeConfirmTitle": "Remove content", "contents.saveAndPublish": "Save and Publish", "contents.scheduledAt": "at", - "contents.scheduledAtLabel": "at", + "contents.scheduledBy": "by", "contents.scheduledTo": "to", + "contents.scheduledToLabel": "Will be changed to", "contents.schemasPageTitle": "Contents", "contents.searchPlaceholder": "Fulltext search", "contents.searchSchemasPlaceholder": "Search", @@ -857,7 +863,7 @@ "schemas.schemaNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.", "schemas.schemaNameValidationMessage": "Name can only contain letters, numbers, dashes and spaces.", "schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.", - "schemas.searchPlaceholder": "Search...", + "schemas.searchPlaceholder": "Search", "schemas.showFieldFailed": "Failed to show field. Please reload.", "schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 46ff92034..ae5506236 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -239,6 +239,7 @@ "common.contributors": "Collaboratori", "common.create": "Crea", "common.created": "Creato", + "common.daily": "Daily", "common.dashboard": "Dasboard", "common.date": "Data", "common.dateTimeEditor.local": "Locale", @@ -295,6 +296,7 @@ "common.mapHide": "Nascondi la mappa", "common.mapShow": "Mostra la mappa", "common.message": "Messaggio", + "common.monthly": "Monthly", "common.more": "More", "common.name": "Nome", "common.no": "No", @@ -335,6 +337,7 @@ "common.rules": "Regole", "common.sampleCodeLabel": "Esempio di codice per", "common.save": "Salva", + "common.schema": "Schema", "common.schemas": "Schemi", "common.search": "Search", "common.searchGoogleMaps": "Cerca su Google Maps", @@ -363,6 +366,7 @@ "common.url": "URL", "common.users": "Utenti", "common.value": "Valore", + "common.weekly": "Weekly", "common.width": "Larghezza", "common.workflow": "Workflow", "common.workflows": "Workflow", @@ -385,6 +389,7 @@ "contents.assetsUpload": "Trascina i file o clicca", "contents.autotranslate": "Traduci in automatico dalla lingua principale", "contents.bulkFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", + "contents.calendar": "Scheduled Contents", "contents.changeStatusTo": "Cambia gli elementi del contenuto in {action}", "contents.changeStatusToImmediately": "Imposta {action} immediatamente.", "contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.", @@ -445,8 +450,9 @@ "contents.removeConfirmTitle": "Cancella il contenuto", "contents.saveAndPublish": "Salva e pubblica", "contents.scheduledAt": "alle", - "contents.scheduledAtLabel": "alle", + "contents.scheduledBy": "by", "contents.scheduledTo": "a", + "contents.scheduledToLabel": "Will be changed to", "contents.schemasPageTitle": "Contenuti", "contents.searchPlaceholder": "Ricerca testuale", "contents.searchSchemasPlaceholder": "Cerca schemi...", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 6d5936672..7da6ff950 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -239,6 +239,7 @@ "common.contributors": "Bijdragers", "common.create": "Maken", "common.created": "Gemaakt", + "common.daily": "Daily", "common.dashboard": "Dasboard", "common.date": "Datum", "common.dateTimeEditor.local": "Lokaal", @@ -295,6 +296,7 @@ "common.mapHide": "Verberg kaart", "common.mapShow": "Toon kaart", "common.message": "Bericht", + "common.monthly": "Monthly", "common.more": "More", "common.name": "Naam", "common.no": "Nee", @@ -335,6 +337,7 @@ "common.rules": "Regels", "common.sampleCodeLabel": "Voorbeeldcode bij", "common.save": "Opslaan", + "common.schema": "Schema", "common.schemas": "Schema's", "common.search": "Search", "common.searchGoogleMaps": "Zoeken in Google Maps", @@ -363,6 +366,7 @@ "common.url": "URL", "common.users": "Gebruikers", "common.value": "Waarde", + "common.weekly": "Weekly", "common.width": "Breedte", "common.workflow": "Workflow", "common.workflows": "Workflows", @@ -385,6 +389,7 @@ "contents.assetsUpload": "Zet bestanden neer of klik", "contents.autotranslate": "Automatisch vertalen vanuit de hoofdtaal", "contents.bulkFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", + "contents.calendar": "Scheduled Contents", "contents.changeStatusTo": "Verander inhoud item(s) in {action}", "contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.", "contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.", @@ -445,8 +450,9 @@ "contents.removeConfirmTitle": "Verwijder inhoud", "contents.saveAndPublish": "Opslaan en publiceren", "contents.scheduledAt": "bij", - "contents.scheduledAtLabel": "bij", + "contents.scheduledBy": "by", "contents.scheduledTo": "naar", + "contents.scheduledToLabel": "Will be changed to", "contents.schemasPageTitle": "Inhoud", "contents.searchPlaceholder": "Zoeken in volledige tekst", "contents.searchSchemasPlaceholder": "Zoek schema's ...", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 0f4b1e980..dc09acfbc 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -239,6 +239,7 @@ "common.contributors": "贡献者", "common.create": "创建", "common.created": "创建", + "common.daily": "Daily", "common.dashboard": "Dasboard", "common.date": "日期", "common.dateTimeEditor.local": "本地", @@ -295,6 +296,7 @@ "common.mapHide": "隐藏地图", "common.mapShow": "显示地图", "common.message": "消息", + "common.monthly": "Monthly", "common.more": "More", "common.name": "名称", "common.no": "不", @@ -335,6 +337,7 @@ "common.rules": "规则", "common.sampleCodeLabel": "示例代码在", "common.save": "保存", + "common.schema": "Schema", "common.schemas": "Schemas", "common.search": "搜索", "common.searchGoogleMaps": "搜索谷歌地图", @@ -363,6 +366,7 @@ "common.url": "URL", "common.users": "用户", "common.value": "值", + "common.weekly": "Weekly", "common.width": "宽度", "common.workflow": "工作流程", "common.workflows": "工作流程", @@ -385,6 +389,7 @@ "contents.assetsUpload": "删除文件或点击", "contents.autotranslate": "从母语自动翻译", "contents.bulkFailed": "删除或更新内容失败。请重新加载。", + "contents.calendar": "Scheduled Contents", "contents.changeStatusTo": "将内容项更改为 {action}", "contents.changeStatusToImmediately": "立即设置为 {action}。", "contents.changeStatusToLater": "在稍后的日期和时间设置为 {action}。", @@ -445,8 +450,9 @@ "contents.removeConfirmTitle": "删除内容", "contents.saveAndPublish": "保存并发布", "contents.scheduledAt": "at", - "contents.scheduledAtLabel": "at", + "contents.scheduledBy": "by", "contents.scheduledTo": "to", + "contents.scheduledToLabel": "Will be changed to", "contents.schemasPageTitle": "内容", "contents.searchPlaceholder": "全文搜索", "contents.searchSchemasPlaceholder": "搜索Schemas...", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 158e0c5e6..2d46a73b9 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -122,6 +122,7 @@ "contents.bulkInsertQueryNotUnique": "More than one content matches to the query.", "contents.draftNotCreateForUnpublished": "You can only create a new version when the content is published.", "contents.draftToDeleteNotFound": "There is nothing to delete.", + "contents.invalidAllQuery": "Either Ids or Schedule range must be defined.", "contents.invalidArrayOfObjects": "Invalid json type, expected array of objects.", "contents.invalidArrayOfStrings": "Invalid json type, expected array of strings.", "contents.invalidBoolean": "Invalid json type, expected boolean.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index ff6535621..d0a9c692c 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -239,6 +239,7 @@ "common.contributors": "Contributors", "common.create": "Create", "common.created": "Created", + "common.daily": "Daily", "common.dashboard": "Dasboard", "common.date": "Date", "common.dateTimeEditor.local": "Local", @@ -295,6 +296,7 @@ "common.mapHide": "Hide map", "common.mapShow": "Show map", "common.message": "Message", + "common.monthly": "Monthly", "common.more": "More", "common.name": "Name", "common.no": "No", @@ -335,6 +337,7 @@ "common.rules": "Rules", "common.sampleCodeLabel": "Sample Code at", "common.save": "Save", + "common.schema": "Schema", "common.schemas": "Schemas", "common.search": "Search", "common.searchGoogleMaps": "Search Google Maps", @@ -363,6 +366,7 @@ "common.url": "URL", "common.users": "Users", "common.value": "Value", + "common.weekly": "Weekly", "common.width": "Width", "common.workflow": "Workflow", "common.workflows": "Workflows", @@ -385,6 +389,7 @@ "contents.assetsUpload": "Drop files or click", "contents.autotranslate": "Autotranslate from master language", "contents.bulkFailed": "Failed to delete or update content. Please reload.", + "contents.calendar": "Scheduled Contents", "contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.", @@ -445,8 +450,9 @@ "contents.removeConfirmTitle": "Remove content", "contents.saveAndPublish": "Save and Publish", "contents.scheduledAt": "at", - "contents.scheduledAtLabel": "at", + "contents.scheduledBy": "by", "contents.scheduledTo": "to", + "contents.scheduledToLabel": "Will be changed to", "contents.schemasPageTitle": "Contents", "contents.searchPlaceholder": "Fulltext search", "contents.searchSchemasPlaceholder": "Search", @@ -857,7 +863,7 @@ "schemas.schemaNameHint": "You can only use letters, numbers and dashes and not more than 40 characters.", "schemas.schemaNameValidationMessage": "Name can only contain letters, numbers, dashes and spaces.", "schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.", - "schemas.searchPlaceholder": "Search...", + "schemas.searchPlaceholder": "Search", "schemas.showFieldFailed": "Failed to show field. Please reload.", "schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 894f90789..6844ed91b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -114,6 +114,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return await queryByIds.QueryAsync(app.Id, schemas, q, ct); } + if (q.ScheduledFrom != null && q.ScheduledTo != null) + { + return await queryScheduled.QueryAsync(app.Id, schemas, q, ct); + } + if (q.Referencing != default) { return await queryReferences.QueryAsync(app.Id, schemas, q, ct); @@ -138,6 +143,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return await queryByIds.QueryAsync(app.Id, new List { schema }, q, ct); } + if (q.ScheduledFrom != null && q.ScheduledTo != null) + { + return await queryScheduled.QueryAsync(app.Id, new List { schema }, q, ct); + } + if (q.Referencing == default) { return await queryByQuery.QueryAsync(app, schema, q, ct); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs index fcebadb09..cac31b146 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs @@ -7,11 +7,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using NodaTime; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -23,7 +25,27 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { yield return new CreateIndexModel(Index .Ascending(x => x.ScheduledAt) - .Ascending(x => x.IsDeleted)); + .Ascending(x => x.IsDeleted) + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IndexedSchemaId)); + } + + public async Task> QueryAsync(DomainId appId, List schemas, Q q, + CancellationToken ct) + { + Guard.NotNull(q, nameof(q)); + + if (q.ScheduledFrom == null || q.ScheduledTo == null) + { + return ResultList.CreateFrom(0); + } + + var filter = CreateFilter(appId, schemas.Select(x => x.Id), q.ScheduledFrom.Value, q.ScheduledTo.Value); + + var contentEntities = await Collection.Find(filter).Limit(100).ToListAsync(ct); + var contentTotal = (long)contentEntities.Count; + + return ResultList.Create(contentTotal, contentEntities); } public Task QueryAsync(Instant now, Func callback, @@ -37,5 +59,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations callback(c); }, ct); } + + private static FilterDefinition CreateFilter(DomainId appId, IEnumerable schemaIds, Instant scheduledFrom, Instant scheduledTo) + { + return Filter.And( + Filter.Gte(x => x.ScheduledAt, scheduledFrom), + Filter.Lte(x => x.ScheduledAt, scheduledTo), + Filter.Ne(x => x.IsDeleted, true), + Filter.Eq(x => x.IndexedAppId, appId), + Filter.In(x => x.IndexedSchemaId, schemaIds)); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index 886acbb2a..d4496dcce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -72,13 +72,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = q.Query; - if (!string.IsNullOrWhiteSpace(q?.JsonQueryString)) + if (!string.IsNullOrWhiteSpace(q?.QueryAsJson)) { - query = ParseJson(q.JsonQueryString); + query = ParseJson(q.QueryAsJson); } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + else if (!string.IsNullOrWhiteSpace(q?.QueryAsOdata)) { - query = ParseOData(q.ODataQuery); + query = ParseOData(q.QueryAsOdata); } return query; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index bfb14c32d..212612ffe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -127,17 +127,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var query = q.Query; - if (!string.IsNullOrWhiteSpace(q.JsonQueryString)) + if (!string.IsNullOrWhiteSpace(q.QueryAsJson)) { - query = ParseJson(context, schema, q.JsonQueryString, components); + query = ParseJson(context, schema, q.QueryAsJson, components); } else if (q?.JsonQuery != null) { query = ParseJson(context, schema, q.JsonQuery, components); } - else if (!string.IsNullOrWhiteSpace(q?.ODataQuery)) + else if (!string.IsNullOrWhiteSpace(q?.QueryAsOdata)) { - query = ParseOData(context, schema, q.ODataQuery, components); + query = ParseOData(context, schema, q.QueryAsOdata, components); } return query; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs index 54b920bd4..37b9251d8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using NodaTime; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; @@ -25,9 +26,13 @@ namespace Squidex.Domain.Apps.Entities public DomainId Reference { get; init; } - public string? ODataQuery { get; init; } + public string? QueryAsOdata { get; init; } - public string? JsonQueryString { get; init; } + public string? QueryAsJson { get; init; } + + public Instant? ScheduledFrom { get; init; } + + public Instant? ScheduledTo { get; init; } public Query? JsonQuery { get; init; } @@ -53,12 +58,12 @@ namespace Squidex.Domain.Apps.Entities public Q WithODataQuery(string? query) { - return this with { ODataQuery = query }; + return this with { QueryAsOdata = query }; } public Q WithJsonQuery(string? query) { - return this with { JsonQueryString = query }; + return this with { QueryAsJson = query }; } public Q WithJsonQuery(Query? query) @@ -86,6 +91,11 @@ namespace Squidex.Domain.Apps.Entities return this with { Ids = ids?.ToList() }; } + public Q WithSchedule(Instant from, Instant to) + { + return this with { ScheduledFrom = from, ScheduledTo = to }; + } + public Q WithIds(string? ids) { if (string.IsNullOrWhiteSpace(ids)) diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 13d4f27fc..b859186db 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -451,6 +451,9 @@ Non c'è niente da eliminare. + + Either Ids or Schedule range must be defined. + Errore nel json, atteso un array di objects. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 4835fbde0..382d31d84 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -451,6 +451,9 @@ Er is niets te verwijderen. + + Either Ids or Schedule range must be defined. + Ongeldig json-type, verwachte reeks objecten. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index eb7a5327d..da8b84c2b 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -451,6 +451,9 @@ There is nothing to delete. + + Either Ids or Schedule range must be defined. + Invalid json type, expected array of objects. diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index 353c14489..8c3e69eca 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -451,6 +451,9 @@ 没有要删除的内容。 + + Either Ids or Schedule range must be defined. + 无效的 json 类型,预期的对象数组。 diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index e5e96f344..a983ed3d3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// Queries contents. /// /// The name of the app. - /// The optional ids of the content to fetch. + /// The required query object. /// /// 200 => Contents returned. /// 404 => App not found. @@ -81,9 +81,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] - public async Task GetAllContents(string app, [FromQuery] string ids) + public async Task GetAllContents(string app, AllContentsByGetDto query) { - var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(ids), HttpContext.RequestAborted); + var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); var response = Deferred.AsyncResponse(() => { @@ -110,9 +110,9 @@ namespace Squidex.Areas.Api.Controllers.Contents [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] - public async Task GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query) + public async Task GetAllContentsPost(string app, [FromBody] AllContentsByPostDto query) { - var contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(query.Ids), HttpContext.RequestAborted); + var contents = await contentQuery.QueryAsync(Context, query?.ToQuery() ?? Q.Empty, HttpContext.RequestAborted); var response = Deferred.AsyncResponse(() => { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs new file mode 100644 index 000000000..655e0ac1c --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using NodaTime; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class AllContentsByGetDto + { + /// + /// The list of ids to query. + /// + [FromQuery(Name = "ids")] + public string? Ids { get; set; } + + /// + /// The start of the schedule. + /// + [FromQuery] + public Instant? ScheduledFrom { get; set; } + + /// + /// The end of the schedule. + /// + [FromQuery] + public Instant? ScheduledTo { get; set; } + + public Q ToQuery() + { + if (!string.IsNullOrWhiteSpace(Ids)) + { + return Q.Empty.WithIds(Ids); + } + + if (ScheduledFrom != null && ScheduledTo != null) + { + return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); + } + + throw new ValidationException(T.Get("contents.invalidAllQuery")); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs new file mode 100644 index 000000000..46b597611 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class AllContentsByPostDto + { + /// + /// The list of ids to query. + /// + public DomainId[]? Ids { get; set; } + + /// + /// The start of the schedule. + /// + public Instant? ScheduledFrom { get; set; } + + /// + /// The end of the schedule. + /// + public Instant? ScheduledTo { get; set; } + + public Q ToQuery() + { + if (Ids?.Length > 0) + { + return Q.Empty.WithIds(Ids); + } + + if (ScheduledFrom != null && ScheduledTo != null) + { + return Q.Empty.WithSchedule(ScheduledFrom.Value, ScheduledTo.Value); + } + + throw new ValidationException(T.Get("contents.invalidAllQuery")); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs deleted file mode 100644 index bd6355418..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Validation; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public sealed class ContentsIdsQueryDto - { - /// - /// The list of ids to query. - /// - [LocalizedRequired] - public List Ids { get; set; } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index bbc4e24df..08246cd6a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var asset = TestAsset.Create(DomainId.NewGuid()); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == true), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == true), A._)) .Returns(ResultList.CreateFrom(0, asset)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var asset = TestAsset.Create(DomainId.NewGuid()); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == false), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == false), A._)) .Returns(ResultList.CreateFrom(10, asset)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = TestContent.Create(contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal == true), A._)) .Returns(ResultList.CreateFrom(0, content)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -223,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = TestContent.Create(contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal == true), A._)) .Returns(ResultList.CreateFrom(0, content)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -259,7 +259,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var content = TestContent.Create(contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == false), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal == false), A._)) .Returns(ResultList.CreateFrom(10, content)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -466,7 +466,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL .Returns(ResultList.CreateFrom(1, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == true), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == true), A._)) .Returns(ResultList.CreateFrom(1, content)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -530,7 +530,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL .Returns(ResultList.CreateFrom(1, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), - A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == false), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == false), A._)) .Returns(ResultList.CreateFrom(1, content)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }); diff --git a/frontend/app-config/webpack.config.js b/frontend/app-config/webpack.config.js index 21dac8ee7..a5e739e69 100644 --- a/frontend/app-config/webpack.config.js +++ b/frontend/app-config/webpack.config.js @@ -267,6 +267,9 @@ module.exports = function calculateConfig(env) { { from: './node_modules/tinymce/themes/silver', to: 'dependencies/tinymce/themes/silver' }, { from: './node_modules/tinymce/tinymce.min.js', to: 'dependencies/tinymce' }, + { from: './node_modules/tui-code-snippet/dist', to: 'dependencies/tui-calendar' }, + { from: './node_modules/tui-calendar/dist', to: 'dependencies/tui-calendar' }, + { from: './node_modules/ace-builds/src-min/ace.js', to: 'dependencies/ace/ace.js' }, { from: './node_modules/ace-builds/src-min/ext-language_tools.js', to: 'dependencies/ace/ext/language_tools.js' }, { from: './node_modules/ace-builds/src-min/ext-modelist.js', to: 'dependencies/ace/ext/modelist.js' }, diff --git a/frontend/app/features/content/declarations.ts b/frontend/app/features/content/declarations.ts index 4f1fb6343..8c7e14f86 100644 --- a/frontend/app/features/content/declarations.ts +++ b/frontend/app/features/content/declarations.ts @@ -5,6 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ +export * from './pages/calendar/calendar-page.component'; export * from './pages/comments/comments-page.component'; export * from './pages/content/content-event.component'; export * from './pages/content/content-history-page.component'; diff --git a/frontend/app/features/content/module.ts b/frontend/app/features/content/module.ts index 24a1c1587..deba1a432 100644 --- a/frontend/app/features/content/module.ts +++ b/frontend/app/features/content/module.ts @@ -11,6 +11,7 @@ import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSche import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule as ScrollingModuleExperimental } from '@angular/cdk-experimental/scrolling'; import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; +import { CalendarPageComponent } from './pages/calendar/calendar-page.component'; const routes: Routes = [ { @@ -21,6 +22,10 @@ const routes: Routes = [ { path: '', }, + { + path: '__calendar', + component: CalendarPageComponent, + }, { path: ':schemaName', canActivate: [SchemaMustExistPublishedGuard], @@ -87,6 +92,7 @@ const routes: Routes = [ ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, + CalendarPageComponent, CommentsPageComponent, ComponentComponent, ContentCreatorComponent, diff --git a/frontend/app/features/content/pages/calendar/calendar-page.component.html b/frontend/app/features/content/pages/calendar/calendar-page.component.html new file mode 100644 index 000000000..c5d09dc01 --- /dev/null +++ b/frontend/app/features/content/pages/calendar/calendar-page.component.html @@ -0,0 +1,114 @@ + + + + + {{title}} + + + + + + + + +
+
+
+ + + + + {{ 'common.content' | sqxTranslate }} + + + +
+
+ + +
+
+ + + +
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ + +
+
+ +
+ +
+ + +
+ + +
+
+ +
+ + +
+ {{content.scheduleJob.dueTime | sqxFullDateTime}} +
+
+ +
+ + +
+ {{content.scheduleJob.scheduledBy | sqxUserNameRef}} +
+
+
+
+
+
\ No newline at end of file diff --git a/frontend/app/features/content/pages/calendar/calendar-page.component.scss b/frontend/app/features/content/pages/calendar/calendar-page.component.scss new file mode 100644 index 000000000..6f18bd92a --- /dev/null +++ b/frontend/app/features/content/pages/calendar/calendar-page.component.scss @@ -0,0 +1,24 @@ +:host ::ng-deep { + .tui-full-calendar-weekday-schedule-bullet { + top: 10px !important; + } +} + +.calendar { + @include absolute(-1px, 0, 0, 0); +} + +.form-select { + display: inline-block; + white-space: normal; + width: auto; +} + +.form-group-aligned { + align-items: center; + + .col-form-label { + padding-bottom: 0; + padding-top: 0; + } +} \ No newline at end of file diff --git a/frontend/app/features/content/pages/calendar/calendar-page.component.ts b/frontend/app/features/content/pages/calendar/calendar-page.component.ts new file mode 100644 index 000000000..6f5899a16 --- /dev/null +++ b/frontend/app/features/content/pages/calendar/calendar-page.component.ts @@ -0,0 +1,210 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; +import { AppsState, ContentDto, ContentsService, DateTime, DialogModel, getContentValue, LanguageDto, LanguagesState, LocalizerService, ResourceLoaderService } from '@app/shared'; + +declare const tui: any; + +type ViewMode = 'day' | 'week' | 'month'; + +@Component({ + selector: 'sqx-calendar-page', + styleUrls: ['./calendar-page.component.scss'], + templateUrl: './calendar-page.component.html', +}) +export class CalendarPageComponent implements AfterViewInit, OnDestroy { + private calendar: any; + private language: LanguageDto; + + @ViewChild('calendarContainer', { static: false }) + public calendarContainer: ElementRef; + + public view: ViewMode = 'month'; + + public content?: ContentDto; + public contentDialog = new DialogModel(); + + public title: string; + + public isLoading: boolean; + + constructor( + private readonly appsState: AppsState, + private readonly changeDetector: ChangeDetectorRef, + private readonly contentsService: ContentsService, + private readonly resourceLoader: ResourceLoaderService, + private readonly languagesState: LanguagesState, + private readonly localizer: LocalizerService, + ) { + } + + public ngOnDestroy() { + this.calendar?.destroy(); + } + + public ngOnInit() { + this.language = this.languagesState.snapshot.languages.find(x => x.language.isMaster)!.language; + } + + public ngAfterViewInit() { + Promise.all([ + this.resourceLoader.loadLocalStyle('dependencies/tui-calendar/tui-calendar.min.css'), + this.resourceLoader.loadLocalScript('dependencies/tui-calendar/tui-code-snippet.min.js'), + this.resourceLoader.loadLocalScript('dependencies/tui-calendar/tui-calendar.min.js'), + ]).then(() => { + const Calendar = tui.Calendar; + + this.calendar = new Calendar(this.calendarContainer.nativeElement, { + taskView: false, + scheduleView: ['time'], + defaultView: 'month', + isReadOnly: true, + ...getLocalizationSettings(), + }); + + this.calendar.on('clickSchedule', (event: any) => { + this.content = event.schedule.raw; + this.contentDialog.show(); + + this.changeDetector.detectChanges(); + }); + + this.load(); + }); + } + + public changeView(view: ViewMode) { + this.view = view; + + this.calendar?.changeView(view); + + this.load(); + } + + public goPrev() { + this.calendar?.prev(); + + this.load(); + } + + public goNext() { + this.calendar?.next(); + + this.load(); + } + + private load() { + if (!this.calendar) { + return; + } + + const scheduledFrom = new DateTime(this.calendar.getDateRangeStart().toDate()); + const scheduledTo = new DateTime(this.calendar.getDateRangeEnd().toDate()); + + this.updateRange(scheduledFrom, scheduledTo); + + if (this.isLoading) { + return; + } + + this.isLoading = true; + + this.contentsService.getAllContents(this.appsState.appName, { + scheduledFrom: scheduledFrom.toISOString(), + scheduledTo: scheduledTo.toISOString(), + }).subscribe({ + next: contents => { + this.calendar.clear(); + this.calendar.createSchedules(contents.items.map(x => ({ + id: x.id, + borderColor: x.scheduleJob!.color, + color: x.scheduleJob?.color, + calendarId: '1', + category: 'time', + end: x.scheduleJob?.dueTime.toISOString(), + raw: x, + start: x.scheduleJob?.dueTime.toISOString(), + state: 'free', + title: `[${x.schemaDisplayName}] ${this.createContentName(x)}`, + }))); + }, + complete: () => { + this.isLoading = false; + }, + }); + } + + private updateRange(from: DateTime, to: DateTime) { + switch (this.view) { + case 'month': { + this.title = from.toStringFormat('LLLL yyyy'); + break; + } + case 'day': { + this.title = from.toStringFormat('PPPP'); + break; + } + case 'week': { + this.title = `${from.toStringFormat('PP')} - ${to.toStringFormat('PP')}`; + break; + } + } + } + + public createContentName(content: ContentDto) { + const name = + content.referenceFields + .map(f => getContentValue(content, this.language, f, false)) + .map(v => v.formatted) + .filter(v => !!v) + .join(', ') + || this.localizer.getOrKey('common.noValue'); + + return name; + } +} + +let localizedValues: any; + +function getLocalizationSettings() { + if (!localizedValues) { + localizedValues = { + month: { + daynames: [], + }, + week: { + daynames: [], + }, + template: { + timegridDisplayPrimaryTime: (time: any) => { + return new DateTime(new Date(2020, 1, 1, time.hour, time.minutes, 0)).toStringFormat('p'); + }, + timegridCurrentTime: (timezone: any) => { + const templates = []; + + if (timezone.dateDifference) { + templates.push(`[${timezone.dateDifferenceSign}${timezone.dateDifference}]
`); + } + + templates.push(new DateTime(timezone.hourmarker.toDate()).toStringFormat('p')); + + return templates.join(''); + }, + }, + }; + + for (let i = 1; i <= 7; i++) { + const weekDay = new DateTime(new Date(2020, 10, i, 12, 0, 0)); + + localizedValues.month.daynames.push(weekDay.toStringFormat('EEE')); + localizedValues.week.daynames.push(weekDay.toStringFormat('EEE')); + } + } + + return localizedValues; +} diff --git a/frontend/app/features/content/pages/schemas/schemas-page.component.html b/frontend/app/features/content/pages/schemas/schemas-page.component.html index 5d033a2a8..5c6f79bbf 100644 --- a/frontend/app/features/content/pages/schemas/schemas-page.component.html +++ b/frontend/app/features/content/pages/schemas/schemas-page.component.html @@ -10,6 +10,14 @@ + + diff --git a/frontend/app/features/content/shared/list/content.component.ts b/frontend/app/features/content/shared/list/content.component.ts index 93c394236..bb921ceef 100644 --- a/frontend/app/features/content/shared/list/content.component.ts +++ b/frontend/app/features/content/shared/list/content.component.ts @@ -91,12 +91,12 @@ export class ContentComponent implements OnChanges { next: () => { this.patchForm.submitCompleted({ noReset: true }); - this.changeDetector.markForCheck(); + this.changeDetector.detectChanges(); }, error: error => { this.patchForm.submitFailed(error); - this.changeDetector.markForCheck(); + this.changeDetector.detectChanges(); }, }); } diff --git a/frontend/app/features/content/shared/references/reference-item.component.html b/frontend/app/features/content/shared/references/reference-item.component.html index 9051b0fb3..cbdfcccc2 100644 --- a/frontend/app/features/content/shared/references/reference-item.component.html +++ b/frontend/app/features/content/shared/references/reference-item.component.html @@ -21,7 +21,7 @@ - {{content.schemaDisplayName}} + {{content.schemaDisplayName}} diff --git a/frontend/app/features/content/shared/references/references-editor.component.ts b/frontend/app/features/content/shared/references/references-editor.component.ts index b8e398e7b..1da6f07dd 100644 --- a/frontend/app/features/content/shared/references/references-editor.component.ts +++ b/frontend/app/features/content/shared/references/references-editor.component.ts @@ -69,7 +69,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent x.id))) { const contentIds: string[] = obj; - this.contentsService.getContentsByIds(this.appsState.appName, contentIds) + this.contentsService.getAllContents(this.appsState.appName, { ids: contentIds }) .subscribe({ next: dtos => { this.setContentItems(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r)); diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.scss b/frontend/app/features/schemas/pages/schema/fields/field.component.scss index bd4467e1d..eaa0ecd47 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.scss @@ -31,6 +31,7 @@ $padding: 1rem; .nested-fields { background: $color-border-lighter; border: 0; + border-radius: 0 0 $border-radius $border-radius; padding: $padding; padding-left: 2 * $padding; position: relative; diff --git a/frontend/app/features/schemas/pages/schemas/schemas-page.component.html b/frontend/app/features/schemas/pages/schemas/schemas-page.component.html index 6c2449126..0ee9e96ca 100644 --- a/frontend/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/frontend/app/features/schemas/pages/schemas/schemas-page.component.html @@ -28,8 +28,8 @@ -
- + +
diff --git a/frontend/app/features/settings/pages/clients/client-connect-form.component.ts b/frontend/app/features/settings/pages/clients/client-connect-form.component.ts index fc4e30cfe..ea04cc61c 100644 --- a/frontend/app/features/settings/pages/clients/client-connect-form.component.ts +++ b/frontend/app/features/settings/pages/clients/client-connect-form.component.ts @@ -55,7 +55,7 @@ export class ClientConnectFormComponent implements OnInit { next: dto => { this.connectToken = dto; - this.changeDetector.markForCheck(); + this.changeDetector.detectChanges(); }, error: error => { this.dialogs.notifyError(error); diff --git a/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts b/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts index 029b9bafc..e5c45c0f3 100644 --- a/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts +++ b/frontend/app/framework/angular/forms/editors/date-time-editor.component.ts @@ -8,7 +8,6 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DateHelper, DateTime, StatefulControlComponent, UIOptions } from '@app/framework/internal'; -import format from 'date-fns/format'; import * as Pikaday from 'pikaday/pikaday'; import { FocusComponent } from './../forms-helper'; @@ -268,19 +267,17 @@ function getLocalizationSettings() { weekdaysShort: [], }; - const options = { locale: DateHelper.getFnsLocale() }; - for (let i = 0; i < 12; i++) { - const firstOfMonth = new Date(2020, i, 1, 12, 0, 0); + const firstOfMonth = new DateTime(new Date(2020, i, 1, 12, 0, 0)); - localizedValues.months.push(format(firstOfMonth, 'LLLL', options)); + localizedValues.months.push(firstOfMonth.toStringFormat('LLLL')); } for (let i = 1; i <= 7; i++) { - const weekDay = new Date(2020, 10, i, 12, 0, 0); + const weekDay = new DateTime(new Date(2020, 10, i, 12, 0, 0)); - localizedValues.weekdays.push(format(weekDay, 'EEEE', options)); - localizedValues.weekdaysShort.push(format(weekDay, 'EEE', options)); + localizedValues.weekdays.push(weekDay.toStringFormat('EEEE')); + localizedValues.weekdaysShort.push(weekDay.toStringFormat('EEE')); } } diff --git a/frontend/app/framework/angular/layout.component.html b/frontend/app/framework/angular/layout.component.html index 38f1d3bad..539baaa12 100644 --- a/frontend/app/framework/angular/layout.component.html +++ b/frontend/app/framework/angular/layout.component.html @@ -32,7 +32,7 @@
-
+
@@ -62,7 +62,7 @@
-
+
diff --git a/frontend/app/framework/angular/modals/modal.directive.ts b/frontend/app/framework/angular/modals/modal.directive.ts index da998abbd..7e118bd5d 100644 --- a/frontend/app/framework/angular/modals/modal.directive.ts +++ b/frontend/app/framework/angular/modals/modal.directive.ts @@ -110,7 +110,7 @@ export class ModalDirective implements OnDestroy { this.eventsModel.own(value.isOpenChanges.subscribe(isOpen => this.update(isOpen))); } else { - this.update(value === true); + this.update(!!value); } } diff --git a/frontend/app/shared/components/assets/asset-dialog.component.html b/frontend/app/shared/components/assets/asset-dialog.component.html index 253973f9c..e5bb25e76 100644 --- a/frontend/app/shared/components/assets/asset-dialog.component.html +++ b/frontend/app/shared/components/assets/asset-dialog.component.html @@ -81,7 +81,7 @@ -
+
@@ -93,7 +93,7 @@
-
+
diff --git a/frontend/app/shared/components/assets/asset.component.scss b/frontend/app/shared/components/assets/asset.component.scss index 237e33a70..ffbf38534 100644 --- a/frontend/app/shared/components/assets/asset.component.scss +++ b/frontend/app/shared/components/assets/asset.component.scss @@ -11,6 +11,7 @@ $list-height: 2.25rem; @mixin overlay { @include absolute(0, 0, 0, 0); color: $color-white; + cursor: none; display: flex; opacity: 0; transition: opacity .4s ease; @@ -18,6 +19,8 @@ $list-height: 2.25rem; &-background { @include absolute(0, 0, 0, 0); background: $color-black; + border: 0; + border-radius: 0; opacity: .7; } } @@ -31,6 +34,10 @@ $list-height: 2.25rem; text-transform: uppercase; } +:host { + width: auto; +} + :host ::ng-deep { .form-control { &.disabled, @@ -121,7 +128,7 @@ $list-height: 2.25rem; } &-icon { - background: $color-asset-bg; + background: $color-border-light; border: 0; height: $asset-image; padding: 2rem; @@ -184,7 +191,7 @@ $list-height: 2.25rem; .image { @include absolute(0, auto, 0, 4px); - background: $color-asset-bg; + background: $color-border-light; border: 0; width: $list-height + 2rem; diff --git a/frontend/app/shared/components/contents/content-list-field.component.html b/frontend/app/shared/components/contents/content-list-field.component.html index 8fc57ee59..577f456b5 100644 --- a/frontend/app/shared/components/contents/content-list-field.component.html +++ b/frontend/app/shared/components/contents/content-list-field.component.html @@ -61,7 +61,7 @@ [statusColor]="content.scheduleJob.color"> - {{ 'contents.scheduledAtLabel' | sqxTranslate }} {{content.scheduleJob?.dueTime | sqxShortDate}} + {{ 'contents.scheduledAt' | sqxTranslate }} {{content.scheduleJob?.dueTime | sqxShortDate}} diff --git a/frontend/app/shared/components/references/content-selector.component.ts b/frontend/app/shared/components/references/content-selector.component.ts index 209e7255a..7e5a2dcbb 100644 --- a/frontend/app/shared/components/references/content-selector.component.ts +++ b/frontend/app/shared/components/references/content-selector.component.ts @@ -69,7 +69,7 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit { this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents); if (this.schemaIds && this.schemaIds.length > 0) { - this.schemas = this.schemas.filter(x => this.schemaIds!.indexOf(x.id) >= 0); + this.schemas = this.schemas.filter(x => x.canReadContents && this.schemaIds!.indexOf(x.id) >= 0); } this.selectSchema(this.schemas[0]); diff --git a/frontend/app/shared/components/references/reference-dropdown.component.ts b/frontend/app/shared/components/references/reference-dropdown.component.ts index a261ed5d3..40e2fe676 100644 --- a/frontend/app/shared/components/references/reference-dropdown.component.ts +++ b/frontend/app/shared/components/references/reference-dropdown.component.ts @@ -29,7 +29,7 @@ type ContentName = { name: string; id?: string }; const NO_EMIT = { emitEvent: false }; @Component({ - selector: 'sqx-reference-dropdown[schemaId]', + selector: 'sqx-reference-dropdown[mode][schemaId]', styleUrls: ['./reference-dropdown.component.scss'], templateUrl: './reference-dropdown.component.html', providers: [ @@ -166,9 +166,10 @@ export class ReferenceDropdownComponent extends StatefulControlComponent getContentValue(content, this.language, f, false)) - .map(v => v.formatted || this.localizer.getOrKey('common.noValue')) + .map(v => v.formatted) .filter(v => !!v) - .join(', '); + .join(', ') + || this.localizer.getOrKey('common.noValue'); return { name, id: content.id }; }); diff --git a/frontend/app/shared/components/references/reference-input.component.ts b/frontend/app/shared/components/references/reference-input.component.ts index 1fedf9f29..8598a781a 100644 --- a/frontend/app/shared/components/references/reference-input.component.ts +++ b/frontend/app/shared/components/references/reference-input.component.ts @@ -63,7 +63,7 @@ export class ReferenceInputComponent extends StatefulControlComponent { this.updateContent(contents.items[0]); @@ -108,10 +108,11 @@ export class ReferenceInputComponent extends StatefulControlComponent getContentValue(content, this.language, f, false)) - .map(v => v.formatted || this.localizer.getOrKey('common.noValue')) + .map(v => v.formatted) .filter(v => !!v) - .join(', '); + .join(', ') + || this.localizer.getOrKey('common.noValue'); - return name; + return name || this.localizer.getOrKey('common.noValue'); } } diff --git a/frontend/app/shared/components/references/references-tag-converter.ts b/frontend/app/shared/components/references/references-tag-converter.ts index 75a73fc44..d2711da39 100644 --- a/frontend/app/shared/components/references/references-tag-converter.ts +++ b/frontend/app/shared/components/references/references-tag-converter.ts @@ -37,9 +37,10 @@ export class ReferencesTagsConverter implements TagConverter { const name = content.referenceFields .map(f => getContentValue(content, language, f, false)) - .map(v => v.formatted || this.localizer.getOrKey('common.noValue')) + .map(v => v.formatted) .filter(v => !!v) - .join(', '); + .join(', ') + || this.localizer.getOrKey('common.noValue'); return new TagValue(content.id, name, content.id); }); diff --git a/frontend/app/shared/components/schema-category.component.html b/frontend/app/shared/components/schema-category.component.html index ee47eb267..7141d1b2d 100644 --- a/frontend/app/shared/components/schema-category.component.html +++ b/frontend/app/shared/components/schema-category.component.html @@ -1,10 +1,10 @@ -
-
+
-
+ - +
-
+ diff --git a/frontend/app/shared/components/schema-category.component.scss b/frontend/app/shared/components/schema-category.component.scss index e9be3172e..57a7a1a98 100644 --- a/frontend/app/shared/components/schema-category.component.scss +++ b/frontend/app/shared/components/schema-category.component.scss @@ -56,28 +56,13 @@ $drag-margin: -8px; transition: none; } -.header { - align-items: center; - margin-bottom: 0; +.nav-heading { margin-left: -1rem; - - h3 { - margin: 0; - } -} - -.nav-light { - margin: 0; } -.nav-link { - @include truncate; +.nav-item { align-items: center; - margin-left: 0; - margin-right: 0; -} -.nav-item { .drag-handle { margin-right: .5rem; } diff --git a/frontend/app/shared/services/contents.service.spec.ts b/frontend/app/shared/services/contents.service.spec.ts index 91b35089f..0f4700d6b 100644 --- a/frontend/app/shared/services/contents.service.spec.ts +++ b/frontend/app/shared/services/contents.service.spec.ts @@ -154,11 +154,11 @@ describe('ContentsService', () => { req.flush({ total: 10, items: [] }); })); - it('should make get request to get contents by ids', + it('should make get request to get all contents by ids', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { const ids = ['1', '2', '3']; - contentsService.getContentsByIds('my-app', ids).subscribe(); + contentsService.getAllContents('my-app', { ids }).subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app?ids=1,2,3'); @@ -168,11 +168,11 @@ describe('ContentsService', () => { req.flush({ total: 10, items: [] }); })); - it('should make post request to get contents by ids if request limit reached', + it('should make post request to get all contents by ids if request limit reached', inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => { const ids = ['1', '2', '3']; - contentsService.getContentsByIds('my-app', ids, 5).subscribe(); + contentsService.getAllContents('my-app', { ids, maxLength: 5 }).subscribe(); const req = httpMock.expectOne('http://service/p/api/content/my-app'); diff --git a/frontend/app/shared/services/contents.service.ts b/frontend/app/shared/services/contents.service.ts index 254d25bc8..6ea95c993 100644 --- a/frontend/app/shared/services/contents.service.ts +++ b/frontend/app/shared/services/contents.service.ts @@ -124,7 +124,7 @@ export type BulkUpdateJobDto = Readonly<{ id: string; type: BulkUpdateType; status?: string; schema?: string; dueTime?: string | null; expectedVersion?: number }>; export type ContentQueryDto = - Readonly<{ ids?: ReadonlyArray; maxLength?: number; query?: Query; skip?: number; take?: number }>; + Readonly<{ ids?: ReadonlyArray; maxLength?: number; query?: Query; skip?: number; take?: number; scheduledFrom?: string | null; scheduledTo?: string | null }>; @Injectable() export class ContentsService { @@ -173,12 +173,12 @@ export class ContentsService { } } - public getContentsByIds(appName: string, ids: ReadonlyArray, maxLength?: number): Observable { - const fullQuery = `ids=${ids.join(',')}`; + public getAllContents(appName: string, q?: ContentQueryDto): Observable { + const { maxLength, ...body } = q || {}; - if (fullQuery.length > (maxLength || 2000)) { - const body = { ids }; + const { fullQuery } = buildQuery(q); + if (fullQuery.length > (maxLength || 2000)) { const url = this.apiUrl.buildUrl(`/api/content/${appName}`); return this.http.post<{ total: number; items: []; statuses: StatusInfo[] } & Resource>(url, body).pipe( @@ -337,7 +337,7 @@ export class ContentsService { } function buildQuery(q?: ContentQueryDto) { - const { ids, query, skip, take } = q || {}; + const { ids, query, scheduledFrom, scheduledTo, skip, take } = q || {}; const queryParts: string[] = []; const odataParts: string[] = []; @@ -356,6 +356,9 @@ function buildQuery(q?: ContentQueryDto) { if (skip && skip > 0) { odataParts.push(`$skip=${skip}`); } + } else if (scheduledFrom && scheduledTo) { + queryParts.push(`scheduledFrom=${encodeURIComponent(scheduledFrom)}`); + queryParts.push(`scheduledTo=${encodeURIComponent(scheduledTo)}`); } else { queryObj = { ...query }; diff --git a/frontend/app/theme/_bootstrap.scss b/frontend/app/theme/_bootstrap.scss index 835c3fc1e..bd9f2b41c 100644 --- a/frontend/app/theme/_bootstrap.scss +++ b/frontend/app/theme/_bootstrap.scss @@ -350,12 +350,13 @@ a { font-size: 1.5rem; font-weight: normal; margin-right: .5rem; + margin-bottom: .5rem; text-align: center; width: 4.5rem; } i { - color: $color-border-dark; + color: $color-border-darker; } .radio-label { @@ -593,14 +594,6 @@ $icon-size: 4.5rem; // Cards // .card { - &-header, - &-footer { - margin-left: .5rem; - margin-right: .5rem; - padding-left: .5rem; - padding-right: .5rem; - } - &-title { margin-bottom: 1rem; } diff --git a/frontend/app/theme/_mixins.scss b/frontend/app/theme/_mixins.scss index 96e9b68b7..b861250f9 100644 --- a/frontend/app/theme/_mixins.scss +++ b/frontend/app/theme/_mixins.scss @@ -129,8 +129,8 @@ @mixin circle($size) { @include force-height($size); @include force-width($size); - border: 0; border-radius: $size; + border-width: 0; display: inline-block; } diff --git a/frontend/app/theme/_panels2.scss b/frontend/app/theme/_panels2.scss index 4c809c970..0c6760c63 100644 --- a/frontend/app/theme/_panels2.scss +++ b/frontend/app/theme/_panels2.scss @@ -264,6 +264,8 @@ .nav-light { font-size: $font-small; + margin-left: -.5rem; + margin-right: -.5rem; .nav-link { border: 0; @@ -272,8 +274,6 @@ cursor: pointer; font-size: inherit; font-weight: 500; - margin-left: -.5rem; - margin-right: -.5rem; margin-bottom: .125rem; padding-left: 1rem; padding-right: 1rem; diff --git a/frontend/app/theme/_vars.scss b/frontend/app/theme/_vars.scss index 1a34e96e1..149b20c31 100644 --- a/frontend/app/theme/_vars.scss +++ b/frontend/app/theme/_vars.scss @@ -36,8 +36,6 @@ $color-theme-warning: #ffb136; $color-black: #000; $color-white: #fff; -$color-asset-bg: #f7f8fa; - $color-input: #e0e1e5; $color-input-background: #fff; $color-input-disabled: #eff1f4; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index adcf5426d..78f642fae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12241,6 +12241,34 @@ } } }, + "tui-calendar": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/tui-calendar/-/tui-calendar-1.13.1.tgz", + "integrity": "sha512-6d+/JMQyPK4fAudrbsUClIAPsjZqPoY4xrYP7d624Ix2bAkmVbFpnp6UHaBVO7wHOEh6BVzoN88sUiZPcMY8rA==", + "requires": { + "tui-code-snippet": "^1.5.0", + "tui-date-picker": "^4.0.2", + "tui-time-picker": "^2.0.1" + } + }, + "tui-code-snippet": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/tui-code-snippet/-/tui-code-snippet-1.5.2.tgz", + "integrity": "sha512-6UqTlQaaC1KLcmC0HAoq5dtl1G4Fib+R+NC7pmaV7kiIlZ7JqKhUmnOoGRcreAyzd81UTK/vCvhrw9QJskpCFQ==" + }, + "tui-date-picker": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tui-date-picker/-/tui-date-picker-4.2.2.tgz", + "integrity": "sha512-K7EzGMNTz7Q5RAbe++jWjIRtx2KooABrmS0IhttHz2t/RyVoxZ01Mw7J1f6EP0ytF2wHrnjHMXixqEy4Shbu3g==", + "requires": { + "tui-time-picker": "^2.1.2" + } + }, + "tui-time-picker": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tui-time-picker/-/tui-time-picker-2.1.2.tgz", + "integrity": "sha512-vrdPUmPu8o700C08XlJMb+GjpkkgLjiO7rTn/r3d6LWnFo3K0rMPkevEsDaRUMDjCdslTXERYHMDAtevZHr6jQ==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 043e50f05..d83414a8c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,6 +62,7 @@ "slugify": "1.6.0", "tinymce": "5.8.2", "tslib": "2.3.0", + "tui-calendar": "^1.13.1", "video.js": "7.13.3", "vis-data": "7.1.2", "vis-network": "9.0.4",