Browse Source

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.
pull/744/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
70c365be40
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      backend/i18n/frontend_en.json
  2. 8
      backend/i18n/frontend_it.json
  3. 8
      backend/i18n/frontend_nl.json
  4. 8
      backend/i18n/frontend_zh.json
  5. 1
      backend/i18n/source/backend_en.json
  6. 10
      backend/i18n/source/frontend_en.json
  7. 10
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  8. 34
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs
  9. 8
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs
  10. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  11. 18
      backend/src/Squidex.Domain.Apps.Entities/Q.cs
  12. 3
      backend/src/Squidex.Shared/Texts.it.resx
  13. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  14. 3
      backend/src/Squidex.Shared/Texts.resx
  15. 3
      backend/src/Squidex.Shared/Texts.zh.resx
  16. 10
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  17. 51
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByGetDto.cs
  18. 48
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/AllContentsByPostDto.cs
  19. 22
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs
  20. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  21. 3
      frontend/app-config/webpack.config.js
  22. 1
      frontend/app/features/content/declarations.ts
  23. 6
      frontend/app/features/content/module.ts
  24. 114
      frontend/app/features/content/pages/calendar/calendar-page.component.html
  25. 24
      frontend/app/features/content/pages/calendar/calendar-page.component.scss
  26. 210
      frontend/app/features/content/pages/calendar/calendar-page.component.ts
  27. 8
      frontend/app/features/content/pages/schemas/schemas-page.component.html
  28. 1
      frontend/app/features/content/shared/forms/field-editor.component.html
  29. 4
      frontend/app/features/content/shared/list/content.component.ts
  30. 2
      frontend/app/features/content/shared/references/reference-item.component.html
  31. 2
      frontend/app/features/content/shared/references/references-editor.component.ts
  32. 1
      frontend/app/features/schemas/pages/schema/fields/field.component.scss
  33. 4
      frontend/app/features/schemas/pages/schemas/schemas-page.component.html
  34. 2
      frontend/app/features/settings/pages/clients/client-connect-form.component.ts
  35. 13
      frontend/app/framework/angular/forms/editors/date-time-editor.component.ts
  36. 4
      frontend/app/framework/angular/layout.component.html
  37. 2
      frontend/app/framework/angular/modals/modal.directive.ts
  38. 4
      frontend/app/shared/components/assets/asset-dialog.component.html
  39. 11
      frontend/app/shared/components/assets/asset.component.scss
  40. 2
      frontend/app/shared/components/contents/content-list-field.component.html
  41. 2
      frontend/app/shared/components/references/content-selector.component.ts
  42. 7
      frontend/app/shared/components/references/reference-dropdown.component.ts
  43. 9
      frontend/app/shared/components/references/reference-input.component.ts
  44. 5
      frontend/app/shared/components/references/references-tag-converter.ts
  45. 26
      frontend/app/shared/components/schema-category.component.html
  46. 19
      frontend/app/shared/components/schema-category.component.scss
  47. 8
      frontend/app/shared/services/contents.service.spec.ts
  48. 15
      frontend/app/shared/services/contents.service.ts
  49. 11
      frontend/app/theme/_bootstrap.scss
  50. 2
      frontend/app/theme/_mixins.scss
  51. 4
      frontend/app/theme/_panels2.scss
  52. 2
      frontend/app/theme/_vars.scss
  53. 28
      frontend/package-lock.json
  54. 1
      frontend/package.json

10
backend/i18n/frontend_en.json

@ -239,6 +239,7 @@
"common.contributors": "Contributors", "common.contributors": "Contributors",
"common.create": "Create", "common.create": "Create",
"common.created": "Created", "common.created": "Created",
"common.daily": "Daily",
"common.dashboard": "Dasboard", "common.dashboard": "Dasboard",
"common.date": "Date", "common.date": "Date",
"common.dateTimeEditor.local": "Local", "common.dateTimeEditor.local": "Local",
@ -295,6 +296,7 @@
"common.mapHide": "Hide map", "common.mapHide": "Hide map",
"common.mapShow": "Show map", "common.mapShow": "Show map",
"common.message": "Message", "common.message": "Message",
"common.monthly": "Monthly",
"common.more": "More", "common.more": "More",
"common.name": "Name", "common.name": "Name",
"common.no": "No", "common.no": "No",
@ -335,6 +337,7 @@
"common.rules": "Rules", "common.rules": "Rules",
"common.sampleCodeLabel": "Sample Code at", "common.sampleCodeLabel": "Sample Code at",
"common.save": "Save", "common.save": "Save",
"common.schema": "Schema",
"common.schemas": "Schemas", "common.schemas": "Schemas",
"common.search": "Search", "common.search": "Search",
"common.searchGoogleMaps": "Search Google Maps", "common.searchGoogleMaps": "Search Google Maps",
@ -363,6 +366,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Users", "common.users": "Users",
"common.value": "Value", "common.value": "Value",
"common.weekly": "Weekly",
"common.width": "Width", "common.width": "Width",
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflows", "common.workflows": "Workflows",
@ -385,6 +389,7 @@
"contents.assetsUpload": "Drop files or click", "contents.assetsUpload": "Drop files or click",
"contents.autotranslate": "Autotranslate from master language", "contents.autotranslate": "Autotranslate from master language",
"contents.bulkFailed": "Failed to delete or update content. Please reload.", "contents.bulkFailed": "Failed to delete or update content. Please reload.",
"contents.calendar": "Scheduled Contents",
"contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.",
@ -445,8 +450,9 @@
"contents.removeConfirmTitle": "Remove content", "contents.removeConfirmTitle": "Remove content",
"contents.saveAndPublish": "Save and Publish", "contents.saveAndPublish": "Save and Publish",
"contents.scheduledAt": "at", "contents.scheduledAt": "at",
"contents.scheduledAtLabel": "at", "contents.scheduledBy": "by",
"contents.scheduledTo": "to", "contents.scheduledTo": "to",
"contents.scheduledToLabel": "Will be changed to",
"contents.schemasPageTitle": "Contents", "contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext search", "contents.searchPlaceholder": "Fulltext search",
"contents.searchSchemasPlaceholder": "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.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.schemaNameValidationMessage": "Name can only contain letters, numbers, dashes and spaces.",
"schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.", "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.showFieldFailed": "Failed to show field. Please reload.",
"schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",

8
backend/i18n/frontend_it.json

@ -239,6 +239,7 @@
"common.contributors": "Collaboratori", "common.contributors": "Collaboratori",
"common.create": "Crea", "common.create": "Crea",
"common.created": "Creato", "common.created": "Creato",
"common.daily": "Daily",
"common.dashboard": "Dasboard", "common.dashboard": "Dasboard",
"common.date": "Data", "common.date": "Data",
"common.dateTimeEditor.local": "Locale", "common.dateTimeEditor.local": "Locale",
@ -295,6 +296,7 @@
"common.mapHide": "Nascondi la mappa", "common.mapHide": "Nascondi la mappa",
"common.mapShow": "Mostra la mappa", "common.mapShow": "Mostra la mappa",
"common.message": "Messaggio", "common.message": "Messaggio",
"common.monthly": "Monthly",
"common.more": "More", "common.more": "More",
"common.name": "Nome", "common.name": "Nome",
"common.no": "No", "common.no": "No",
@ -335,6 +337,7 @@
"common.rules": "Regole", "common.rules": "Regole",
"common.sampleCodeLabel": "Esempio di codice per", "common.sampleCodeLabel": "Esempio di codice per",
"common.save": "Salva", "common.save": "Salva",
"common.schema": "Schema",
"common.schemas": "Schemi", "common.schemas": "Schemi",
"common.search": "Search", "common.search": "Search",
"common.searchGoogleMaps": "Cerca su Google Maps", "common.searchGoogleMaps": "Cerca su Google Maps",
@ -363,6 +366,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Utenti", "common.users": "Utenti",
"common.value": "Valore", "common.value": "Valore",
"common.weekly": "Weekly",
"common.width": "Larghezza", "common.width": "Larghezza",
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflow", "common.workflows": "Workflow",
@ -385,6 +389,7 @@
"contents.assetsUpload": "Trascina i file o clicca", "contents.assetsUpload": "Trascina i file o clicca",
"contents.autotranslate": "Traduci in automatico dalla lingua principale", "contents.autotranslate": "Traduci in automatico dalla lingua principale",
"contents.bulkFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", "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.changeStatusTo": "Cambia gli elementi del contenuto in {action}",
"contents.changeStatusToImmediately": "Imposta {action} immediatamente.", "contents.changeStatusToImmediately": "Imposta {action} immediatamente.",
"contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.", "contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.",
@ -445,8 +450,9 @@
"contents.removeConfirmTitle": "Cancella il contenuto", "contents.removeConfirmTitle": "Cancella il contenuto",
"contents.saveAndPublish": "Salva e pubblica", "contents.saveAndPublish": "Salva e pubblica",
"contents.scheduledAt": "alle", "contents.scheduledAt": "alle",
"contents.scheduledAtLabel": "alle", "contents.scheduledBy": "by",
"contents.scheduledTo": "a", "contents.scheduledTo": "a",
"contents.scheduledToLabel": "Will be changed to",
"contents.schemasPageTitle": "Contenuti", "contents.schemasPageTitle": "Contenuti",
"contents.searchPlaceholder": "Ricerca testuale", "contents.searchPlaceholder": "Ricerca testuale",
"contents.searchSchemasPlaceholder": "Cerca schemi...", "contents.searchSchemasPlaceholder": "Cerca schemi...",

8
backend/i18n/frontend_nl.json

@ -239,6 +239,7 @@
"common.contributors": "Bijdragers", "common.contributors": "Bijdragers",
"common.create": "Maken", "common.create": "Maken",
"common.created": "Gemaakt", "common.created": "Gemaakt",
"common.daily": "Daily",
"common.dashboard": "Dasboard", "common.dashboard": "Dasboard",
"common.date": "Datum", "common.date": "Datum",
"common.dateTimeEditor.local": "Lokaal", "common.dateTimeEditor.local": "Lokaal",
@ -295,6 +296,7 @@
"common.mapHide": "Verberg kaart", "common.mapHide": "Verberg kaart",
"common.mapShow": "Toon kaart", "common.mapShow": "Toon kaart",
"common.message": "Bericht", "common.message": "Bericht",
"common.monthly": "Monthly",
"common.more": "More", "common.more": "More",
"common.name": "Naam", "common.name": "Naam",
"common.no": "Nee", "common.no": "Nee",
@ -335,6 +337,7 @@
"common.rules": "Regels", "common.rules": "Regels",
"common.sampleCodeLabel": "Voorbeeldcode bij", "common.sampleCodeLabel": "Voorbeeldcode bij",
"common.save": "Opslaan", "common.save": "Opslaan",
"common.schema": "Schema",
"common.schemas": "Schema's", "common.schemas": "Schema's",
"common.search": "Search", "common.search": "Search",
"common.searchGoogleMaps": "Zoeken in Google Maps", "common.searchGoogleMaps": "Zoeken in Google Maps",
@ -363,6 +366,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Gebruikers", "common.users": "Gebruikers",
"common.value": "Waarde", "common.value": "Waarde",
"common.weekly": "Weekly",
"common.width": "Breedte", "common.width": "Breedte",
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflows", "common.workflows": "Workflows",
@ -385,6 +389,7 @@
"contents.assetsUpload": "Zet bestanden neer of klik", "contents.assetsUpload": "Zet bestanden neer of klik",
"contents.autotranslate": "Automatisch vertalen vanuit de hoofdtaal", "contents.autotranslate": "Automatisch vertalen vanuit de hoofdtaal",
"contents.bulkFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", "contents.bulkFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.",
"contents.calendar": "Scheduled Contents",
"contents.changeStatusTo": "Verander inhoud item(s) in {action}", "contents.changeStatusTo": "Verander inhoud item(s) in {action}",
"contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.", "contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.",
"contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.", "contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.",
@ -445,8 +450,9 @@
"contents.removeConfirmTitle": "Verwijder inhoud", "contents.removeConfirmTitle": "Verwijder inhoud",
"contents.saveAndPublish": "Opslaan en publiceren", "contents.saveAndPublish": "Opslaan en publiceren",
"contents.scheduledAt": "bij", "contents.scheduledAt": "bij",
"contents.scheduledAtLabel": "bij", "contents.scheduledBy": "by",
"contents.scheduledTo": "naar", "contents.scheduledTo": "naar",
"contents.scheduledToLabel": "Will be changed to",
"contents.schemasPageTitle": "Inhoud", "contents.schemasPageTitle": "Inhoud",
"contents.searchPlaceholder": "Zoeken in volledige tekst", "contents.searchPlaceholder": "Zoeken in volledige tekst",
"contents.searchSchemasPlaceholder": "Zoek schema's ...", "contents.searchSchemasPlaceholder": "Zoek schema's ...",

8
backend/i18n/frontend_zh.json

@ -239,6 +239,7 @@
"common.contributors": "贡献者", "common.contributors": "贡献者",
"common.create": "创建", "common.create": "创建",
"common.created": "创建", "common.created": "创建",
"common.daily": "Daily",
"common.dashboard": "Dasboard", "common.dashboard": "Dasboard",
"common.date": "日期", "common.date": "日期",
"common.dateTimeEditor.local": "本地", "common.dateTimeEditor.local": "本地",
@ -295,6 +296,7 @@
"common.mapHide": "隐藏地图", "common.mapHide": "隐藏地图",
"common.mapShow": "显示地图", "common.mapShow": "显示地图",
"common.message": "消息", "common.message": "消息",
"common.monthly": "Monthly",
"common.more": "More", "common.more": "More",
"common.name": "名称", "common.name": "名称",
"common.no": "不", "common.no": "不",
@ -335,6 +337,7 @@
"common.rules": "规则", "common.rules": "规则",
"common.sampleCodeLabel": "示例代码在", "common.sampleCodeLabel": "示例代码在",
"common.save": "保存", "common.save": "保存",
"common.schema": "Schema",
"common.schemas": "Schemas", "common.schemas": "Schemas",
"common.search": "搜索", "common.search": "搜索",
"common.searchGoogleMaps": "搜索谷歌地图", "common.searchGoogleMaps": "搜索谷歌地图",
@ -363,6 +366,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "用户", "common.users": "用户",
"common.value": "值", "common.value": "值",
"common.weekly": "Weekly",
"common.width": "宽度", "common.width": "宽度",
"common.workflow": "工作流程", "common.workflow": "工作流程",
"common.workflows": "工作流程", "common.workflows": "工作流程",
@ -385,6 +389,7 @@
"contents.assetsUpload": "删除文件或点击", "contents.assetsUpload": "删除文件或点击",
"contents.autotranslate": "从母语自动翻译", "contents.autotranslate": "从母语自动翻译",
"contents.bulkFailed": "删除或更新内容失败。请重新加载。", "contents.bulkFailed": "删除或更新内容失败。请重新加载。",
"contents.calendar": "Scheduled Contents",
"contents.changeStatusTo": "将内容项更改为 {action}", "contents.changeStatusTo": "将内容项更改为 {action}",
"contents.changeStatusToImmediately": "立即设置为 {action}。", "contents.changeStatusToImmediately": "立即设置为 {action}。",
"contents.changeStatusToLater": "在稍后的日期和时间设置为 {action}。", "contents.changeStatusToLater": "在稍后的日期和时间设置为 {action}。",
@ -445,8 +450,9 @@
"contents.removeConfirmTitle": "删除内容", "contents.removeConfirmTitle": "删除内容",
"contents.saveAndPublish": "保存并发布", "contents.saveAndPublish": "保存并发布",
"contents.scheduledAt": "at", "contents.scheduledAt": "at",
"contents.scheduledAtLabel": "at", "contents.scheduledBy": "by",
"contents.scheduledTo": "to", "contents.scheduledTo": "to",
"contents.scheduledToLabel": "Will be changed to",
"contents.schemasPageTitle": "内容", "contents.schemasPageTitle": "内容",
"contents.searchPlaceholder": "全文搜索", "contents.searchPlaceholder": "全文搜索",
"contents.searchSchemasPlaceholder": "搜索Schemas...", "contents.searchSchemasPlaceholder": "搜索Schemas...",

1
backend/i18n/source/backend_en.json

@ -122,6 +122,7 @@
"contents.bulkInsertQueryNotUnique": "More than one content matches to the query.", "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.draftNotCreateForUnpublished": "You can only create a new version when the content is published.",
"contents.draftToDeleteNotFound": "There is nothing to delete.", "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.invalidArrayOfObjects": "Invalid json type, expected array of objects.",
"contents.invalidArrayOfStrings": "Invalid json type, expected array of strings.", "contents.invalidArrayOfStrings": "Invalid json type, expected array of strings.",
"contents.invalidBoolean": "Invalid json type, expected boolean.", "contents.invalidBoolean": "Invalid json type, expected boolean.",

10
backend/i18n/source/frontend_en.json

@ -239,6 +239,7 @@
"common.contributors": "Contributors", "common.contributors": "Contributors",
"common.create": "Create", "common.create": "Create",
"common.created": "Created", "common.created": "Created",
"common.daily": "Daily",
"common.dashboard": "Dasboard", "common.dashboard": "Dasboard",
"common.date": "Date", "common.date": "Date",
"common.dateTimeEditor.local": "Local", "common.dateTimeEditor.local": "Local",
@ -295,6 +296,7 @@
"common.mapHide": "Hide map", "common.mapHide": "Hide map",
"common.mapShow": "Show map", "common.mapShow": "Show map",
"common.message": "Message", "common.message": "Message",
"common.monthly": "Monthly",
"common.more": "More", "common.more": "More",
"common.name": "Name", "common.name": "Name",
"common.no": "No", "common.no": "No",
@ -335,6 +337,7 @@
"common.rules": "Rules", "common.rules": "Rules",
"common.sampleCodeLabel": "Sample Code at", "common.sampleCodeLabel": "Sample Code at",
"common.save": "Save", "common.save": "Save",
"common.schema": "Schema",
"common.schemas": "Schemas", "common.schemas": "Schemas",
"common.search": "Search", "common.search": "Search",
"common.searchGoogleMaps": "Search Google Maps", "common.searchGoogleMaps": "Search Google Maps",
@ -363,6 +366,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Users", "common.users": "Users",
"common.value": "Value", "common.value": "Value",
"common.weekly": "Weekly",
"common.width": "Width", "common.width": "Width",
"common.workflow": "Workflow", "common.workflow": "Workflow",
"common.workflows": "Workflows", "common.workflows": "Workflows",
@ -385,6 +389,7 @@
"contents.assetsUpload": "Drop files or click", "contents.assetsUpload": "Drop files or click",
"contents.autotranslate": "Autotranslate from master language", "contents.autotranslate": "Autotranslate from master language",
"contents.bulkFailed": "Failed to delete or update content. Please reload.", "contents.bulkFailed": "Failed to delete or update content. Please reload.",
"contents.calendar": "Scheduled Contents",
"contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusTo": "Change content item(s) to {action}",
"contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToImmediately": "Set to {action} immediately.",
"contents.changeStatusToLater": "Set to {action} at a later point date and time.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.",
@ -445,8 +450,9 @@
"contents.removeConfirmTitle": "Remove content", "contents.removeConfirmTitle": "Remove content",
"contents.saveAndPublish": "Save and Publish", "contents.saveAndPublish": "Save and Publish",
"contents.scheduledAt": "at", "contents.scheduledAt": "at",
"contents.scheduledAtLabel": "at", "contents.scheduledBy": "by",
"contents.scheduledTo": "to", "contents.scheduledTo": "to",
"contents.scheduledToLabel": "Will be changed to",
"contents.schemasPageTitle": "Contents", "contents.schemasPageTitle": "Contents",
"contents.searchPlaceholder": "Fulltext search", "contents.searchPlaceholder": "Fulltext search",
"contents.searchSchemasPlaceholder": "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.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.schemaNameValidationMessage": "Name can only contain letters, numbers, dashes and spaces.",
"schemas.schemaTagsHint": "Tags to annotate your schema for automation processes.", "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.showFieldFailed": "Failed to show field. Please reload.",
"schemas.synchronized": "Schema synchronized successfully.", "schemas.synchronized": "Schema synchronized successfully.",
"schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.", "schemas.synchronizeFailed": "Failed to synchronize schema. Please reload.",

10
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); 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) if (q.Referencing != default)
{ {
return await queryReferences.QueryAsync(app.Id, schemas, q, ct); 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<ISchemaEntity> { schema }, q, ct); return await queryByIds.QueryAsync(app.Id, new List<ISchemaEntity> { schema }, q, ct);
} }
if (q.ScheduledFrom != null && q.ScheduledTo != null)
{
return await queryScheduled.QueryAsync(app.Id, new List<ISchemaEntity> { schema }, q, ct);
}
if (q.Referencing == default) if (q.Referencing == default)
{ {
return await queryByQuery.QueryAsync(app, schema, q, ct); return await queryByQuery.QueryAsync(app, schema, q, ct);

34
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs

@ -7,11 +7,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -23,7 +25,27 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{ {
yield return new CreateIndexModel<MongoContentEntity>(Index yield return new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ScheduledAt) .Ascending(x => x.ScheduledAt)
.Ascending(x => x.IsDeleted)); .Ascending(x => x.IsDeleted)
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IndexedSchemaId));
}
public async Task<IResultList<IContentEntity>> QueryAsync(DomainId appId, List<ISchemaEntity> schemas, Q q,
CancellationToken ct)
{
Guard.NotNull(q, nameof(q));
if (q.ScheduledFrom == null || q.ScheduledTo == null)
{
return ResultList.CreateFrom<IContentEntity>(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<IContentEntity, Task> callback, public Task QueryAsync(Instant now, Func<IContentEntity, Task> callback,
@ -37,5 +59,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
callback(c); callback(c);
}, ct); }, ct);
} }
private static FilterDefinition<MongoContentEntity> CreateFilter(DomainId appId, IEnumerable<DomainId> 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));
}
} }
} }

8
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; 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; return query;

8
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; 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) else if (q?.JsonQuery != null)
{ {
query = ParseJson(context, schema, q.JsonQuery, components); 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; return query;

18
backend/src/Squidex.Domain.Apps.Entities/Q.cs

@ -7,6 +7,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NodaTime;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.Queries;
@ -25,9 +26,13 @@ namespace Squidex.Domain.Apps.Entities
public DomainId Reference { get; init; } 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<IJsonValue>? JsonQuery { get; init; } public Query<IJsonValue>? JsonQuery { get; init; }
@ -53,12 +58,12 @@ namespace Squidex.Domain.Apps.Entities
public Q WithODataQuery(string? query) public Q WithODataQuery(string? query)
{ {
return this with { ODataQuery = query }; return this with { QueryAsOdata = query };
} }
public Q WithJsonQuery(string? query) public Q WithJsonQuery(string? query)
{ {
return this with { JsonQueryString = query }; return this with { QueryAsJson = query };
} }
public Q WithJsonQuery(Query<IJsonValue>? query) public Q WithJsonQuery(Query<IJsonValue>? query)
@ -86,6 +91,11 @@ namespace Squidex.Domain.Apps.Entities
return this with { Ids = ids?.ToList() }; 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) public Q WithIds(string? ids)
{ {
if (string.IsNullOrWhiteSpace(ids)) if (string.IsNullOrWhiteSpace(ids))

3
backend/src/Squidex.Shared/Texts.it.resx

@ -451,6 +451,9 @@
<data name="contents.draftToDeleteNotFound" xml:space="preserve"> <data name="contents.draftToDeleteNotFound" xml:space="preserve">
<value>Non c'è niente da eliminare.</value> <value>Non c'è niente da eliminare.</value>
</data> </data>
<data name="contents.invalidAllQuery" xml:space="preserve">
<value>Either Ids or Schedule range must be defined.</value>
</data>
<data name="contents.invalidArrayOfObjects" xml:space="preserve"> <data name="contents.invalidArrayOfObjects" xml:space="preserve">
<value>Errore nel json, atteso un array di objects.</value> <value>Errore nel json, atteso un array di objects.</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -451,6 +451,9 @@
<data name="contents.draftToDeleteNotFound" xml:space="preserve"> <data name="contents.draftToDeleteNotFound" xml:space="preserve">
<value>Er is niets te verwijderen.</value> <value>Er is niets te verwijderen.</value>
</data> </data>
<data name="contents.invalidAllQuery" xml:space="preserve">
<value>Either Ids or Schedule range must be defined.</value>
</data>
<data name="contents.invalidArrayOfObjects" xml:space="preserve"> <data name="contents.invalidArrayOfObjects" xml:space="preserve">
<value>Ongeldig json-type, verwachte reeks objecten.</value> <value>Ongeldig json-type, verwachte reeks objecten.</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.resx

@ -451,6 +451,9 @@
<data name="contents.draftToDeleteNotFound" xml:space="preserve"> <data name="contents.draftToDeleteNotFound" xml:space="preserve">
<value>There is nothing to delete.</value> <value>There is nothing to delete.</value>
</data> </data>
<data name="contents.invalidAllQuery" xml:space="preserve">
<value>Either Ids or Schedule range must be defined.</value>
</data>
<data name="contents.invalidArrayOfObjects" xml:space="preserve"> <data name="contents.invalidArrayOfObjects" xml:space="preserve">
<value>Invalid json type, expected array of objects.</value> <value>Invalid json type, expected array of objects.</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.zh.resx

@ -451,6 +451,9 @@
<data name="contents.draftToDeleteNotFound" xml:space="preserve"> <data name="contents.draftToDeleteNotFound" xml:space="preserve">
<value>没有要删除的内容。</value> <value>没有要删除的内容。</value>
</data> </data>
<data name="contents.invalidAllQuery" xml:space="preserve">
<value>Either Ids or Schedule range must be defined.</value>
</data>
<data name="contents.invalidArrayOfObjects" xml:space="preserve"> <data name="contents.invalidArrayOfObjects" xml:space="preserve">
<value>无效的 json 类型,预期的对象数组。</value> <value>无效的 json 类型,预期的对象数组。</value>
</data> </data>

10
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// Queries contents. /// Queries contents.
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="ids">The optional ids of the content to fetch.</param> /// <param name="query">The required query object.</param>
/// <returns> /// <returns>
/// 200 => Contents returned. /// 200 => Contents returned.
/// 404 => App not found. /// 404 => App not found.
@ -81,9 +81,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAllContents(string app, [FromQuery] string ids) public async Task<IActionResult> 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(() => var response = Deferred.AsyncResponse(() =>
{ {
@ -110,9 +110,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)]
[ApiPermissionOrAnonymous] [ApiPermissionOrAnonymous]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query) public async Task<IActionResult> 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(() => var response = Deferred.AsyncResponse(() =>
{ {

51
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
{
/// <summary>
/// The list of ids to query.
/// </summary>
[FromQuery(Name = "ids")]
public string? Ids { get; set; }
/// <summary>
/// The start of the schedule.
/// </summary>
[FromQuery]
public Instant? ScheduledFrom { get; set; }
/// <summary>
/// The end of the schedule.
/// </summary>
[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"));
}
}
}

48
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
{
/// <summary>
/// The list of ids to query.
/// </summary>
public DomainId[]? Ids { get; set; }
/// <summary>
/// The start of the schedule.
/// </summary>
public Instant? ScheduledFrom { get; set; }
/// <summary>
/// The end of the schedule.
/// </summary>
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"));
}
}
}

22
backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsIdsQueryDto.cs

@ -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
{
/// <summary>
/// The list of ids to query.
/// </summary>
[LocalizedRequired]
public List<DomainId> Ids { get; set; }
}
}

14
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()); var asset = TestAsset.Create(DomainId.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null,
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == true), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == true), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, asset)); .Returns(ResultList.CreateFrom(0, asset));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); 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()); var asset = TestAsset.Create(DomainId.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null,
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == false), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == false), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(10, asset)); .Returns(ResultList.CreateFrom(10, asset));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var result = await ExecuteAsync(new ExecutionOptions { Query = query });
@ -190,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = TestContent.Create(contentId); var content = TestContent.Create(contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal == true), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, content)); .Returns(ResultList.CreateFrom(0, content));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var result = await ExecuteAsync(new ExecutionOptions { Query = query });
@ -223,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = TestContent.Create(contentId); var content = TestContent.Create(contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal == true), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, content)); .Returns(ResultList.CreateFrom(0, content));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var result = await ExecuteAsync(new ExecutionOptions { Query = query });
@ -259,7 +259,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = TestContent.Create(contentId); var content = TestContent.Create(contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(),
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == false), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal == false), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(10, content)); .Returns(ResultList.CreateFrom(10, content));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var result = await ExecuteAsync(new ExecutionOptions { Query = query });
@ -466,7 +466,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.Returns(ResultList.CreateFrom(1, contentRef)); .Returns(ResultList.CreateFrom(1, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(),
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == true), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == true), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var result = await ExecuteAsync(new ExecutionOptions { Query = query });
@ -530,7 +530,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.Returns(ResultList.CreateFrom(1, contentRef)); .Returns(ResultList.CreateFrom(1, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(),
A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == false), A<CancellationToken>._)) A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == false), A<CancellationToken>._))
.Returns(ResultList.CreateFrom(1, content)); .Returns(ResultList.CreateFrom(1, content));
var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var result = await ExecuteAsync(new ExecutionOptions { Query = query });

3
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/themes/silver', to: 'dependencies/tinymce/themes/silver' },
{ from: './node_modules/tinymce/tinymce.min.js', to: 'dependencies/tinymce' }, { 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/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-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' }, { from: './node_modules/ace-builds/src-min/ext-modelist.js', to: 'dependencies/ace/ext/modelist.js' },

1
frontend/app/features/content/declarations.ts

@ -5,6 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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/comments/comments-page.component';
export * from './pages/content/content-event.component'; export * from './pages/content/content-event.component';
export * from './pages/content/content-history-page.component'; export * from './pages/content/content-history-page.component';

6
frontend/app/features/content/module.ts

@ -11,6 +11,7 @@ import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSche
import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ScrollingModuleExperimental } from '@angular/cdk-experimental/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 { 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 = [ const routes: Routes = [
{ {
@ -21,6 +22,10 @@ const routes: Routes = [
{ {
path: '', path: '',
}, },
{
path: '__calendar',
component: CalendarPageComponent,
},
{ {
path: ':schemaName', path: ':schemaName',
canActivate: [SchemaMustExistPublishedGuard], canActivate: [SchemaMustExistPublishedGuard],
@ -87,6 +92,7 @@ const routes: Routes = [
ArrayEditorComponent, ArrayEditorComponent,
ArrayItemComponent, ArrayItemComponent,
AssetsEditorComponent, AssetsEditorComponent,
CalendarPageComponent,
CommentsPageComponent, CommentsPageComponent,
ComponentComponent, ComponentComponent,
ContentCreatorComponent, ContentCreatorComponent,

114
frontend/app/features/content/pages/calendar/calendar-page.component.html

@ -0,0 +1,114 @@
<sqx-title message="i18n:contents.calendar"></sqx-title>
<sqx-layout layout="main" titleText="i18n:contents.calendar" [hideSidebar]="true">
<ng-container menu>
{{title}}
<select class="form-select ms-4" [ngModel]="view" (ngModelChange)="changeView($event)" [disabled]="isLoading">
<option ngValue="day">{{ 'common.daily' | sqxTranslate }}</option>
<option ngValue="week">{{ 'common.weekly' | sqxTranslate }}</option>
<option ngValue="month">{{ 'common.monthly' | sqxTranslate }}</option>
</select>
<button type="button" class="btn btn-text-secondary btn-navigate ms-2" (click)="goPrev()" [disabled]="isLoading">
<i class="icon-caret-left"></i>
</button>
<button type="button" class="btn btn-text-secondary btn-navigate ms-2" (click)="goNext()" [disabled]="isLoading">
<i class="icon-caret-right"></i>
</button>
</ng-container>
<ng-container content>
<div class="calendar" #calendarContainer></div>
</ng-container>
</sqx-layout>
<ng-container *sqxModal="contentDialog">
<sqx-modal-dialog (close)="contentDialog.hide()">
<ng-container title>
{{ 'common.content' | sqxTranslate }}
</ng-container>
<ng-container content>
<div *ngIf="content && content.scheduleJob">
<div class="form-group row">
<label class="col-4 col-form-label">{{ 'common.id' | sqxTranslate }}</label>
<div class="col-8">
<div class="input-group">
<input readonly class="form-control" name="id" id="id" value="{{content.id}}" #inputId>
<button type="button" class="btn btn-outline-secondary" [sqxCopy]="inputId">
<i class="icon-copy"></i>
</button>
</div>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.content' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', content.schemaName, content.id]">
{{createContentName(content)}}
</a>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.schema' | sqxTranslate }}</label>
<div class="col-8">
<a class="truncate" [routerLink]="['../', content.schemaName]">
{{content.schemaDisplayName}}
</a>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'common.status' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="content.status"
[statusColor]="content.statusColor"
[small]="true">
</sqx-content-status>
</div>
</div>
<hr />
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledToLabel' | sqxTranslate }}</label>
<div class="col-8">
<sqx-content-status
layout="text"
[status]="content.scheduleJob.status"
[statusColor]="content.scheduleJob.color"
[small]="true">
</sqx-content-status>
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledAt' | sqxTranslate }}</label>
<div class="col-8">
{{content.scheduleJob.dueTime | sqxFullDateTime}}
</div>
</div>
<div class="form-group form-group-aligned row">
<label class="col-4 col-form-label">{{ 'contents.scheduledBy' | sqxTranslate }}</label>
<div class="col-8">
<img class="user-picture" [src]="content.scheduleJob.scheduledBy | sqxUserPictureRef"> {{content.scheduleJob.scheduledBy | sqxUserNameRef}}
</div>
</div>
</div>
</ng-container>
</sqx-modal-dialog>
</ng-container>

24
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;
}
}

210
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}]<br>`);
}
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;
}

8
frontend/app/features/content/pages/schemas/schemas-page.component.html

@ -10,6 +10,14 @@
</ng-container> </ng-container>
<ng-container> <ng-container>
<ul class="nav nav-light mb-2 flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="__calendar" routerLinkActive="active">
{{ 'contents.calendar' | sqxTranslate }}
</a>
</li>
</ul>
<ng-container *ngIf="schemasState.publishedSchemas | async; let schemas"> <ng-container *ngIf="schemasState.publishedSchemas | async; let schemas">
<sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory" <sqx-schema-category *ngFor="let category of schemasState.categories | async; trackBy: trackByCategory"
[schemaCategory]="category" [schemaCategory]="category"

1
frontend/app/features/content/shared/forms/field-editor.component.html

@ -113,6 +113,7 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Dropdown'"> <ng-container *ngSwitchCase="'Dropdown'">
<sqx-reference-dropdown <sqx-reference-dropdown
mode="Array"
[formControl]="editorControl" [formControl]="editorControl"
[language]="language" [language]="language"
[schemaId]="field.rawProperties.singleId"> [schemaId]="field.rawProperties.singleId">

4
frontend/app/features/content/shared/list/content.component.ts

@ -91,12 +91,12 @@ export class ContentComponent implements OnChanges {
next: () => { next: () => {
this.patchForm.submitCompleted({ noReset: true }); this.patchForm.submitCompleted({ noReset: true });
this.changeDetector.markForCheck(); this.changeDetector.detectChanges();
}, },
error: error => { error: error => {
this.patchForm.submitFailed(error); this.patchForm.submitFailed(error);
this.changeDetector.markForCheck(); this.changeDetector.detectChanges();
}, },
}); });
} }

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

@ -21,7 +21,7 @@
</td> </td>
<td class="cell-label" *ngIf="!isCompact"> <td class="cell-label" *ngIf="!isCompact">
<span class="badge rounded-pill truncate-inline badge-primary">{{content.schemaDisplayName}}</span> <span class="badge badge-primary rounded-pill truncate-inline" [title]="content.schemaDisplayName">{{content.schemaDisplayName}}</span>
</td> </td>
<td class="cell-actions"> <td class="cell-actions">

2
frontend/app/features/content/shared/references/references-editor.component.ts

@ -69,7 +69,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent<State, R
if (!Types.equals(obj, this.snapshot.contentItems.map(x => x.id))) { if (!Types.equals(obj, this.snapshot.contentItems.map(x => x.id))) {
const contentIds: string[] = obj; const contentIds: string[] = obj;
this.contentsService.getContentsByIds(this.appsState.appName, contentIds) this.contentsService.getAllContents(this.appsState.appName, { ids: contentIds })
.subscribe({ .subscribe({
next: dtos => { next: dtos => {
this.setContentItems(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r)); this.setContentItems(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r));

1
frontend/app/features/schemas/pages/schema/fields/field.component.scss

@ -31,6 +31,7 @@ $padding: 1rem;
.nested-fields { .nested-fields {
background: $color-border-lighter; background: $color-border-lighter;
border: 0; border: 0;
border-radius: 0 0 $border-radius $border-radius;
padding: $padding; padding: $padding;
padding-left: 2 * $padding; padding-left: 2 * $padding;
position: relative; position: relative;

4
frontend/app/features/schemas/pages/schemas/schemas-page.component.html

@ -28,8 +28,8 @@
</sqx-schema-category> </sqx-schema-category>
</div> </div>
<form [formGroup]="addCategoryForm.form" (ngSubmit)="addCategory()"> <form class="mt-4" [formGroup]="addCategoryForm.form" (ngSubmit)="addCategory()">
<input class="form-control underlined" formControlName="name" placeholder="{{ 'schemas.createCategory' | sqxTranslate }}"> <input class="form-control" formControlName="name" placeholder="{{ 'schemas.createCategory' | sqxTranslate }}">
</form> </form>
</ng-container> </ng-container>
</sqx-layout> </sqx-layout>

2
frontend/app/features/settings/pages/clients/client-connect-form.component.ts

@ -55,7 +55,7 @@ export class ClientConnectFormComponent implements OnInit {
next: dto => { next: dto => {
this.connectToken = dto; this.connectToken = dto;
this.changeDetector.markForCheck(); this.changeDetector.detectChanges();
}, },
error: error => { error: error => {
this.dialogs.notifyError(error); this.dialogs.notifyError(error);

13
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 { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DateHelper, DateTime, StatefulControlComponent, UIOptions } from '@app/framework/internal'; import { DateHelper, DateTime, StatefulControlComponent, UIOptions } from '@app/framework/internal';
import format from 'date-fns/format';
import * as Pikaday from 'pikaday/pikaday'; import * as Pikaday from 'pikaday/pikaday';
import { FocusComponent } from './../forms-helper'; import { FocusComponent } from './../forms-helper';
@ -268,19 +267,17 @@ function getLocalizationSettings() {
weekdaysShort: [], weekdaysShort: [],
}; };
const options = { locale: DateHelper.getFnsLocale() };
for (let i = 0; i < 12; i++) { 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++) { 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.weekdays.push(weekDay.toStringFormat('EEEE'));
localizedValues.weekdaysShort.push(format(weekDay, 'EEE', options)); localizedValues.weekdaysShort.push(weekDay.toStringFormat('EEE'));
} }
} }

4
frontend/app/framework/angular/layout.component.html

@ -32,7 +32,7 @@
<div> <div>
<ng-container *ngTemplateOutlet="titleTemplate"></ng-container> <ng-container *ngTemplateOutlet="titleTemplate"></ng-container>
</div> </div>
<div class="flex-grow-1 text-end align-items-center"> <div class="flex-grow-1 d-flex justify-content-end align-items-center">
<ng-container *ngTemplateOutlet="menuTemplate"></ng-container> <ng-container *ngTemplateOutlet="menuTemplate"></ng-container>
</div> </div>
</div> </div>
@ -62,7 +62,7 @@
<div> <div>
<ng-container *ngTemplateOutlet="titleTemplate"></ng-container> <ng-container *ngTemplateOutlet="titleTemplate"></ng-container>
</div> </div>
<div class="flex-grow-1 text-end align-items-center"> <div class="flex-grow-1 d-flex justify-content-end align-items-center">
<ng-container *ngTemplateOutlet="menuTemplate"></ng-container> <ng-container *ngTemplateOutlet="menuTemplate"></ng-container>
</div> </div>
</div> </div>

2
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))); this.eventsModel.own(value.isOpenChanges.subscribe(isOpen => this.update(isOpen)));
} else { } else {
this.update(value === true); this.update(!!value);
} }
} }

4
frontend/app/shared/components/assets/asset-dialog.component.html

@ -81,7 +81,7 @@
</div> </div>
</div> </div>
<div class="form-group g-0"> <div class="form-group">
<label for="id">{{ 'common.id' | sqxTranslate }}</label> <label for="id">{{ 'common.id' | sqxTranslate }}</label>
<div class="input-group"> <div class="input-group">
@ -93,7 +93,7 @@
</div> </div>
</div> </div>
<div class="form-group g-0"> <div class="form-group">
<label for="url">{{ 'common.url' | sqxTranslate }}</label> <label for="url">{{ 'common.url' | sqxTranslate }}</label>
<div class="input-group"> <div class="input-group">

11
frontend/app/shared/components/assets/asset.component.scss

@ -11,6 +11,7 @@ $list-height: 2.25rem;
@mixin overlay { @mixin overlay {
@include absolute(0, 0, 0, 0); @include absolute(0, 0, 0, 0);
color: $color-white; color: $color-white;
cursor: none;
display: flex; display: flex;
opacity: 0; opacity: 0;
transition: opacity .4s ease; transition: opacity .4s ease;
@ -18,6 +19,8 @@ $list-height: 2.25rem;
&-background { &-background {
@include absolute(0, 0, 0, 0); @include absolute(0, 0, 0, 0);
background: $color-black; background: $color-black;
border: 0;
border-radius: 0;
opacity: .7; opacity: .7;
} }
} }
@ -31,6 +34,10 @@ $list-height: 2.25rem;
text-transform: uppercase; text-transform: uppercase;
} }
:host {
width: auto;
}
:host ::ng-deep { :host ::ng-deep {
.form-control { .form-control {
&.disabled, &.disabled,
@ -121,7 +128,7 @@ $list-height: 2.25rem;
} }
&-icon { &-icon {
background: $color-asset-bg; background: $color-border-light;
border: 0; border: 0;
height: $asset-image; height: $asset-image;
padding: 2rem; padding: 2rem;
@ -184,7 +191,7 @@ $list-height: 2.25rem;
.image { .image {
@include absolute(0, auto, 0, 4px); @include absolute(0, auto, 0, 4px);
background: $color-asset-bg; background: $color-border-light;
border: 0; border: 0;
width: $list-height + 2rem; width: $list-height + 2rem;

2
frontend/app/shared/components/contents/content-list-field.component.html

@ -61,7 +61,7 @@
[statusColor]="content.scheduleJob.color"> [statusColor]="content.scheduleJob.color">
</sqx-content-status> </sqx-content-status>
{{ 'contents.scheduledAtLabel' | sqxTranslate }}&nbsp;{{content.scheduleJob?.dueTime | sqxShortDate}} {{ 'contents.scheduledAt' | sqxTranslate }}&nbsp;{{content.scheduleJob?.dueTime | sqxShortDate}}
</span> </span>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="metaFields.statusColor"> <ng-container *ngSwitchCase="metaFields.statusColor">

2
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); this.schemas = this.schemasState.snapshot.schemas.filter(x => x.canReadContents);
if (this.schemaIds && this.schemaIds.length > 0) { 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]); this.selectSchema(this.schemas[0]);

7
frontend/app/shared/components/references/reference-dropdown.component.ts

@ -29,7 +29,7 @@ type ContentName = { name: string; id?: string };
const NO_EMIT = { emitEvent: false }; const NO_EMIT = { emitEvent: false };
@Component({ @Component({
selector: 'sqx-reference-dropdown[schemaId]', selector: 'sqx-reference-dropdown[mode][schemaId]',
styleUrls: ['./reference-dropdown.component.scss'], styleUrls: ['./reference-dropdown.component.scss'],
templateUrl: './reference-dropdown.component.html', templateUrl: './reference-dropdown.component.html',
providers: [ providers: [
@ -166,9 +166,10 @@ export class ReferenceDropdownComponent extends StatefulControlComponent<State,
const name = const name =
content.referenceFields content.referenceFields
.map(f => getContentValue(content, this.language, f, false)) .map(f => getContentValue(content, this.language, f, false))
.map(v => v.formatted || this.localizer.getOrKey('common.noValue')) .map(v => v.formatted)
.filter(v => !!v) .filter(v => !!v)
.join(', '); .join(', ')
|| this.localizer.getOrKey('common.noValue');
return { name, id: content.id }; return { name, id: content.id };
}); });

9
frontend/app/shared/components/references/reference-input.component.ts

@ -63,7 +63,7 @@ export class ReferenceInputComponent extends StatefulControlComponent<State, str
public writeValue(obj: any) { public writeValue(obj: any) {
if (Types.isString(obj)) { if (Types.isString(obj)) {
this.contentsService.getContentsByIds(this.appsState.appName, [obj]) this.contentsService.getAllContents(this.appsState.appName, { ids: [obj] })
.subscribe({ .subscribe({
next: contents => { next: contents => {
this.updateContent(contents.items[0]); this.updateContent(contents.items[0]);
@ -108,10 +108,11 @@ export class ReferenceInputComponent extends StatefulControlComponent<State, str
const name = const name =
content.referenceFields content.referenceFields
.map(f => getContentValue(content, this.language, f, false)) .map(f => getContentValue(content, this.language, f, false))
.map(v => v.formatted || this.localizer.getOrKey('common.noValue')) .map(v => v.formatted)
.filter(v => !!v) .filter(v => !!v)
.join(', '); .join(', ')
|| this.localizer.getOrKey('common.noValue');
return name; return name || this.localizer.getOrKey('common.noValue');
} }
} }

5
frontend/app/shared/components/references/references-tag-converter.ts

@ -37,9 +37,10 @@ export class ReferencesTagsConverter implements TagConverter {
const name = const name =
content.referenceFields content.referenceFields
.map(f => getContentValue(content, language, f, false)) .map(f => getContentValue(content, language, f, false))
.map(v => v.formatted || this.localizer.getOrKey('common.noValue')) .map(v => v.formatted)
.filter(v => !!v) .filter(v => !!v)
.join(', '); .join(', ')
|| this.localizer.getOrKey('common.noValue');
return new TagValue(content.id, name, content.id); return new TagValue(content.id, name, content.id);
}); });

26
frontend/app/shared/components/schema-category.component.html

@ -1,10 +1,10 @@
<div *ngIf="!forContent || filteredSchemas.length > 0" class="droppable category" <ul *ngIf="!forContent || filteredSchemas.length > 0" class="nav nav-light flex-column"
cdkDropList cdkDropList
cdkDropListSortingDisabled cdkDropListSortingDisabled
[cdkDropListData]="schemaCategory.name" [cdkDropListData]="schemaCategory.name"
(cdkDropListDropped)="changeCategory($event)"> (cdkDropListDropped)="changeCategory($event)">
<div class="header clearfix"> <li class="nav-item nav-heading">
<div class="row g-0 align-items-center mb-1"> <div class="row g-0 align-items-center mb-1">
<div class="col-auto"> <div class="col-auto">
<button type="button" class="btn btn-sm btn-text-secondary btn-toggle" (click)="toggle()"> <button type="button" class="btn btn-sm btn-text-secondary btn-toggle" (click)="toggle()">
@ -12,9 +12,9 @@
</button> </button>
</div> </div>
<div class="col"> <div class="col">
<h3 class="truncate"> <div class="truncate">
{{schemaCategory.displayName | sqxTranslate}} {{schemaCategory.displayName | sqxTranslate}}
</h3> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<ng-container *ngIf="schemaCategory.schemas.length > 0; else noSchemas"> <ng-container *ngIf="schemaCategory.schemas.length > 0; else noSchemas">
@ -27,32 +27,32 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
</div> </li>
<div class="nav nav-light flex-column" *ngIf="!isCollapsed" @fade [style.height]="getContainerHeight()"> <ng-container *ngIf="!isCollapsed" @fade [style.height]="getContainerHeight()">
<ng-container *ngIf="!forContent; else simpleMode"> <ng-container *ngIf="!forContent; else simpleMode">
<div *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item truncate" [style.height]="getItemHeight()" <li *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item truncate" [style.height]="getItemHeight()"
cdkDrag cdkDrag
cdkDragLockAxis="y" cdkDragLockAxis="y"
[cdkDragData]="schema" [cdkDragData]="schema"
(cdkDragStarted)="dragStarted($event)"> (cdkDragStarted)="dragStarted($event)">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left"> <a class="nav-link truncate" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left">
<i cdkDragHandle class="icon-drag2 drag-handle"></i> <i cdkDragHandle class="icon-drag2 drag-handle"></i>
<span class="item-published me-1" [class.unpublished]="!schema.isPublished"></span> {{schema.displayName}} <span class="item-published me-1" [class.unpublished]="!schema.isPublished"></span> {{schema.displayName}}
</a> </a>
</div> </li>
</ng-container> </ng-container>
<ng-template #simpleMode> <ng-template #simpleMode>
<li *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item truncate"> <li *ngFor="let schema of filteredSchemas; trackBy: trackBySchema" class="nav-item truncate">
<a class="nav-link" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left"> <a class="nav-link truncate" [routerLink]="schemaRoute(schema)" routerLinkActive="active" title="{{schema.displayName}}" titlePosition="top-left">
<span class="schema-name">{{schema.displayName}}</span> {{schema.displayName}}
</a> </a>
</li> </li>
</ng-template> </ng-template>
</div> </ng-container>
<div class="drop-indicator"></div> <div class="drop-indicator"></div>
</div> </ul>

19
frontend/app/shared/components/schema-category.component.scss

@ -56,28 +56,13 @@ $drag-margin: -8px;
transition: none; transition: none;
} }
.header { .nav-heading {
align-items: center;
margin-bottom: 0;
margin-left: -1rem; margin-left: -1rem;
h3 {
margin: 0;
}
}
.nav-light {
margin: 0;
} }
.nav-link { .nav-item {
@include truncate;
align-items: center; align-items: center;
margin-left: 0;
margin-right: 0;
}
.nav-item {
.drag-handle { .drag-handle {
margin-right: .5rem; margin-right: .5rem;
} }

8
frontend/app/shared/services/contents.service.spec.ts

@ -154,11 +154,11 @@ describe('ContentsService', () => {
req.flush({ total: 10, items: [] }); 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) => { inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2', '3']; 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'); 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: [] }); 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) => { inject([ContentsService, HttpTestingController], (contentsService: ContentsService, httpMock: HttpTestingController) => {
const ids = ['1', '2', '3']; 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'); const req = httpMock.expectOne('http://service/p/api/content/my-app');

15
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 }>; Readonly<{ id: string; type: BulkUpdateType; status?: string; schema?: string; dueTime?: string | null; expectedVersion?: number }>;
export type ContentQueryDto = export type ContentQueryDto =
Readonly<{ ids?: ReadonlyArray<string>; maxLength?: number; query?: Query; skip?: number; take?: number }>; Readonly<{ ids?: ReadonlyArray<string>; maxLength?: number; query?: Query; skip?: number; take?: number; scheduledFrom?: string | null; scheduledTo?: string | null }>;
@Injectable() @Injectable()
export class ContentsService { export class ContentsService {
@ -173,12 +173,12 @@ export class ContentsService {
} }
} }
public getContentsByIds(appName: string, ids: ReadonlyArray<string>, maxLength?: number): Observable<ContentsDto> { public getAllContents(appName: string, q?: ContentQueryDto): Observable<ContentsDto> {
const fullQuery = `ids=${ids.join(',')}`; const { maxLength, ...body } = q || {};
if (fullQuery.length > (maxLength || 2000)) { const { fullQuery } = buildQuery(q);
const body = { ids };
if (fullQuery.length > (maxLength || 2000)) {
const url = this.apiUrl.buildUrl(`/api/content/${appName}`); const url = this.apiUrl.buildUrl(`/api/content/${appName}`);
return this.http.post<{ total: number; items: []; statuses: StatusInfo[] } & Resource>(url, body).pipe( return this.http.post<{ total: number; items: []; statuses: StatusInfo[] } & Resource>(url, body).pipe(
@ -337,7 +337,7 @@ export class ContentsService {
} }
function buildQuery(q?: ContentQueryDto) { function buildQuery(q?: ContentQueryDto) {
const { ids, query, skip, take } = q || {}; const { ids, query, scheduledFrom, scheduledTo, skip, take } = q || {};
const queryParts: string[] = []; const queryParts: string[] = [];
const odataParts: string[] = []; const odataParts: string[] = [];
@ -356,6 +356,9 @@ function buildQuery(q?: ContentQueryDto) {
if (skip && skip > 0) { if (skip && skip > 0) {
odataParts.push(`$skip=${skip}`); odataParts.push(`$skip=${skip}`);
} }
} else if (scheduledFrom && scheduledTo) {
queryParts.push(`scheduledFrom=${encodeURIComponent(scheduledFrom)}`);
queryParts.push(`scheduledTo=${encodeURIComponent(scheduledTo)}`);
} else { } else {
queryObj = { ...query }; queryObj = { ...query };

11
frontend/app/theme/_bootstrap.scss

@ -350,12 +350,13 @@ a {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: normal; font-weight: normal;
margin-right: .5rem; margin-right: .5rem;
margin-bottom: .5rem;
text-align: center; text-align: center;
width: 4.5rem; width: 4.5rem;
} }
i { i {
color: $color-border-dark; color: $color-border-darker;
} }
.radio-label { .radio-label {
@ -593,14 +594,6 @@ $icon-size: 4.5rem;
// Cards // Cards
// //
.card { .card {
&-header,
&-footer {
margin-left: .5rem;
margin-right: .5rem;
padding-left: .5rem;
padding-right: .5rem;
}
&-title { &-title {
margin-bottom: 1rem; margin-bottom: 1rem;
} }

2
frontend/app/theme/_mixins.scss

@ -129,8 +129,8 @@
@mixin circle($size) { @mixin circle($size) {
@include force-height($size); @include force-height($size);
@include force-width($size); @include force-width($size);
border: 0;
border-radius: $size; border-radius: $size;
border-width: 0;
display: inline-block; display: inline-block;
} }

4
frontend/app/theme/_panels2.scss

@ -264,6 +264,8 @@
.nav-light { .nav-light {
font-size: $font-small; font-size: $font-small;
margin-left: -.5rem;
margin-right: -.5rem;
.nav-link { .nav-link {
border: 0; border: 0;
@ -272,8 +274,6 @@
cursor: pointer; cursor: pointer;
font-size: inherit; font-size: inherit;
font-weight: 500; font-weight: 500;
margin-left: -.5rem;
margin-right: -.5rem;
margin-bottom: .125rem; margin-bottom: .125rem;
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;

2
frontend/app/theme/_vars.scss

@ -36,8 +36,6 @@ $color-theme-warning: #ffb136;
$color-black: #000; $color-black: #000;
$color-white: #fff; $color-white: #fff;
$color-asset-bg: #f7f8fa;
$color-input: #e0e1e5; $color-input: #e0e1e5;
$color-input-background: #fff; $color-input-background: #fff;
$color-input-disabled: #eff1f4; $color-input-disabled: #eff1f4;

28
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": { "type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

1
frontend/package.json

@ -62,6 +62,7 @@
"slugify": "1.6.0", "slugify": "1.6.0",
"tinymce": "5.8.2", "tinymce": "5.8.2",
"tslib": "2.3.0", "tslib": "2.3.0",
"tui-calendar": "^1.13.1",
"video.js": "7.13.3", "video.js": "7.13.3",
"vis-data": "7.1.2", "vis-data": "7.1.2",
"vis-network": "9.0.4", "vis-network": "9.0.4",

Loading…
Cancel
Save